C++并发编程之二 在线程间共享数据
文章目录
- 1.1 互斥锁(mutex)保护共享数据
- 1.1.1 std::mutex 的成员函数 std::mutex::lock() 和std::mutex::unlock() (不推荐使用)
- 1.1.2 使用std::lock_guard保护共享数据
- 1.1.3 使用std::unique_lock保护共享数据
- 1.2 保护共享数据的其他方式
- 1.2.1 初始化过程中保护共享数据
- 1.2.2 保护甚少更新的数据结构
- 1.2.3 递归加锁
前情回顾: 在前一篇文章中,我们了解了创建线程、向线程中传递参数、移交线程的归属权,识别线程id等等。那么我们在该篇文章中将要讨论一下线程之间数据共享的问题。
当多线程同时存在访问共享资源的时候,其结果依赖于这些线程的执行顺序,这样的一种状况叫做条件竞争。但如果多线程都只是读取共享资源时,那么条件竞争并不会产生坏的影响。但当条件竞争会因执行顺序不同而结果不同,执行顺序不是程序员所期待的,这就是一种恶性的条件竞争。
恶性条件竞争:是指在多线程编程中,程序的正确性依赖于线程的执行顺序,如果线程的执行顺序不是程序员所期望的,那么就会导致程序出现错误。恶性条件竞争通常是由于锁的使用不当、共享变量的访问不同步、多线程访问同一个资源等原因导致的。这种竞争非常难以发现和修复,而且可能会导致程序崩溃或产生不可预测的结果,因此应该尽量避免。
#include <iostream>
#include <thread>int counter = 0;void increment_counter()
{counter++;
}int main()
{std::thread t1(increment_counter);std::thread t2(increment_counter);t1.join();t2.join();std::cout << "Counter = " << counter << std::endl;return 0;
}
该程序创建了两个线程 t1 和 t2,并让它们同时执行 increment_counter 函数,该函数将全局计数器 counter 加 1。如果两个线程同时访问 counter,那么就可能出现条件竞争。在某些情况下,两个线程可能同时读取 counter 的旧值,然后将其加 1,导致最终结果不是预期的 2。
那么我们该如何防止在多线程开发中出现的恶行条件竞争呢?肯定是由办法的!
我们首先提出第一种方法:使用互斥锁(mutex)保护共享数据!
1.1 互斥锁(mutex)保护共享数据
1.1.1 std::mutex 的成员函数 std::mutex::lock() 和std::mutex::unlock() (不推荐使用)
std::mutex 是 C++11 标准库提供的一个互斥锁类,用于保护多个线程共享访问的数据。其定义如下:
class mutex {
public:void lock(); // 上锁,如果已经被上锁了则阻塞当前线程bool try_lock(); // 尝试上锁,如果已经被上锁了则立即返回 false,否则返回 true 并上锁void unlock(); // 解锁,如果没有被上锁则行为未定义
};
写一个成员函数上锁实例:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
int shared_data = 10;void func()
{mtx.lock();shared_data++;std::cout << "shared_data is " << share_data << std::endl;mtx.unlock();}int main()
{std::thread t(func);std::thread t1(func);t.join();t1.join();return 0;
}
lock() 和 unlock() 方法分别用于上锁和解锁,可以在多个线程之间进行同步。在多个线程访问共享资源的时候,需要先上锁,访问完成之后再解锁,以确保同一时间只有一个线程访问该资源。
当某个线程对共享资源上锁之后,其他线程对同一共享资源的访问就会被阻塞,直到该线程解锁。如果没有及时解锁,则其他线程将一直被阻塞,直到该线程解锁。在 C++11 中,可以使用 std::lock_guard 或 std::unique_lock 等 RAII 封装类来自动管理互斥锁的上锁和解锁,避免手动调用 lock() 和 unlock() 方法导致的代码错误。
1.1.2 使用std::lock_guard保护共享数据
std::lock_guard 是 C++ 标准库中提供的一个互斥锁管理类,它是一个模板类,需要传入互斥锁对象作为模板参数。它使用了 RAII(资源获取即初始化)技术,能够自动的进行加锁和解锁,从而防止忘记解锁导致的死锁问题。
当 std::lock_guard 对象被创建时,它会尝试对互斥锁进行加锁,当该对象被销毁时,它会自动调用 std::mutex 对象的 unlock 方法来解锁。
使用 std::lock_guard 可以有效地简化代码,避免手动进行加锁和解锁操作,从而减少出错的可能性。
以下是使用 std::lock_guard 进行互斥锁保护的一个示例代码:
#include <iostream>
#include <thread>
#include <mutex>std::mutex mtx;void print(std::string msg)
{std::lock_guard<std::mutex> lock(mtx);std::cout << msg << std::endl;
}int main()
{std::thread t1(print, "hello");std::thread t2(print, "world");t1.join();t2.join();return 0;
}
在上述示例中,我们使用 std::lock_guard 对象 lock 来保护了共享资源 std::cout 的访问。每当线程调用 print 函数时,就会自动加锁 mtx,并输出相应的信息。在 print 函数执行完毕后,std::lock_guard 对象 lock 会自动被销毁,从而释放 mtx 的所有权并解锁它。
1.1.3 使用std::unique_lock保护共享数据
std::unique_lock 是 C++11 标准中提供的一种锁类型,它提供了比 std::lock_guard 更为灵活的锁管理方式。与 std::lock_guard 类似,std::unique_lock 也是 RAII(资源获取即初始化)机制的实现方式之一,可以保证在作用域结束时自动释放锁资源。
与 std::lock_guard 不同的是,std::unique_lock 对象在构造时可以选择是否锁定关联的互斥量,同时在对象生命周期内可以多次获得或释放锁,从而提供了更为灵活的锁管理方式。另外,std::unique_lock 对象也提供了更多的锁操作方法,例如延迟锁定、锁定超时等。
下面是一个使用 std::unique_lock 的示例:
#include <iostream>
#include <thread>
#include <mutex>std::mutex mtx;void func()
{std::unique_lock<std::mutex> lck(mtx);std::cout << "Thread " << std::this_thread::get_id() << " is executing..." << std::endl;// 在作用域内,unique_lock 对象已经自动加锁了,可以安全地访问共享资源
}int main()
{std::thread t1(func);std::thread t2(func);t1.join();t2.join();return 0;
}
在上面的示例中,std::unique_lock 对象 lck 的构造函数中传入了互斥量 mtx,在作用域内 lck 对象已经自动加锁了。可以通过 std::unique_lock 对象的 unlock() 方法手动释放锁,或者通过 std::unique_lock 对象的 lock() 方法再次加锁。
- 关于std::unique_lock的延迟锁定
std::unique_lock可以指定锁的defer_lock属性,这意味着构造函数并不会立即锁住锁,而是在后面需要锁住锁的地方进行锁定。这对于某些情况非常有用,例如在多个条件变量上等待的线程需要获得同一个锁的所有权,但是直到等待之前才能获得锁。通过使用std::unique_lock的defer_lock属性,可以确保线程不会在没有准备好的情况下持有锁。
#include <iostream>
#include <thread>
#include <mutex>int shared_data = 10;void func(std::mutex& m)
{std::unique_lock<std::mutex> lock(m, std::defer_lock);lock.lock(); // 明确地调用lock获取锁std::cout << "Thread " << std::this_thread::get_id() << " is doing some work" << std::endl;// 在这里进行一些需要互斥访问的工作shared_data++;std::cout << "shared_data is " << shared_data << std::endl;std::cout << "Thread " << std::this_thread::get_id() << " has released the lock" << std::endl;lock.unlock();
}int main()
{std::mutex m;std::thread t1(func, std::ref(m));std::thread t2(func, std::ref(m));t1.join();t2.join();return 0;
}
- 关于锁定超时
std::unique_lock支持超时,可以指定一个时间段,在这段时间内如果无法获得锁则会放弃锁并返回。这对于需要限制锁的持有时间的情况非常有用,可以防止死锁和资源争用的问题。
std::condition_variable::wait_for() 是 C++11中提供的等待函数,用于等待一个条件变量在一定时间内变为 true。它常常和std::unique_lock 一起使用,可以实现多线程的同步和互斥操作。 它的函数原型如下:
template< class Rep, class Period > std::cv_status wait_for( std::unique_lock<std::mutex>& lock, const std::chrono::duration<Rep,Period>& timeout_duration );
其中,lock 表示互斥锁,timeout_duration 表示等待的时间。
wait_for() 的返回值有以下几种:
std::cv_status::no_timeout: 表示条件变量在等待时间内被唤醒;
std::cv_status::timeout: 表示等待超时。
下面是一个超时锁定的实例:
#include <iostream>
#include <mutex>
#include <thread>
#include <condition_variable>std::mutex mtx;
std::condition_variable cv;
bool data_ready = false;void consumer()
{std::unique_lock<std::mutex> lock(mtx);while(!data_ready){if(cv.wait_for(lock, std::chrono::seconds(1)) == std::cv_status::timeout){std::cout << "timeout\n";return;}}std::cout << "data ready\n";
}void producer()
{std::this_thread::sleep_for(std::chrono::seconds(3));data_ready = true;cv.notify_one();
}int main()
{std::thread t1(consumer);std::thread t2(producer);t1.join();t2.join();return 0;
}
上述示例中,consumer() 是消费者线程,它等待 data_ready 变为 true,如果超时则输出 timeout,否则输出 data ready。producer() 是生产者线程,它在3秒后将 data_ready 设为 true,并调用 cv.notify_one() 唤醒消费者线程。
1.2 保护共享数据的其他方式
1.2.1 初始化过程中保护共享数据
比如当我们在多线程开发的初始化过程中用到一些共享数据,但是我们只想让这个初始化过程只运行一次,那么该怎么协调让多个线程对初始化函数只运行一次呢?
在C++11标准中,为了支持一些只需要被执行一次的线程安全操作,引入了std::call_once()函数和std::once_flag()函数。它们通常被用来实现一些全局初始化和单例模式等场景。该函数在<mutex>头文件中。
std::call_once()函数的声明如下:
template <class Callable, class... Args>
void call_once(std::once_flag& flag, Callable&& func, Args&&... args);
其中,std::once_flag是一个标志位类型,用于保证func只会被执行一次。Callable是一个可调用对象类型,可以是函数指针、函数对象、Lambda表达式等,args是func的参数列表。
std::call_once()函数的使用方法比较简单,只需要传入一个std::once_flag对象和一个可调用对象,就可以保证该可调用对象只会被执行一次,例如:
#include <iostream>
#include <thread>std::once_flag flag;void initialize()
{std::cout << "initialize" << std::endl;
}void thread_func()
{std::call_once(flag, initialize);
}int main()
{std::thread t1(thread_func);std::thread t2(thread_func);t1.join();t2.join();return 0;
}
上面的代码中,我们在initialize()函数中输出一条消息,然后在thread_func()函数中调用std::call_once()函数保证initialize()函数只会被执行一次。最后,在main()函数中创建了两个线程,并分别执行thread_func()函数。
由于std::call_once()函数的存在,我们可以放心地使用initialize()函数进行全局初始化,而不用担心它会被重复执行。
需要注意的是,std::once_flag对象只能用于一次调用std::call_once()函数。如果需要多次执行一些只需要被执行一次的操作,可以创建多个std::once_flag对象。
1.2.2 保护甚少更新的数据结构
但也有这样一种情况,比如有个数据表,这个数据表里面的数据很少会进行修改,那么如果还使用std::mutex的话,当一个线程进行访问的时候,其他线程需要等待访问线程释放锁了之后才能进行访问。这样未免也太浪费时间了。但不用着急,也有解决的办法!
std::shared_mutex和std::shared_timed_mutex是C++11标准库中<shared_mutex>引入的多线程同步原语,用于实现读写锁。当一个线程需要对某个共享资源进行读取操作时,可以使用std::shared_mutex或std::shared_timed_mutex进行加锁,以允许其他线程也能够同时读取该资源。而当一个线程需要对该共享资源进行写入操作时,则需要进行排它访问,此时必须等待其他所有线程释放读取锁和写入锁,才能获得写入锁进行写入。
std::shared_mutex和std::shared_timed_mutex的主要区别在于前者不支持超时等待,而后者支持。因此,在需要对共享资源进行读写访问的场景中,如果需要实现超时等待的功能,则可以使用std::shared_timed_mutex;否则,可以使用std::shared_mutex。
那什么场合下比较使用使用这种方法呢?
当多个线程需要同时读取共享数据,而只有少数线程需要写入数据时,可以使用std::shared_mutex和std::shared_timed_mutex来提高并发性能。这是因为,读取操作可以并发进行,而写入操作需要互斥执行。
- 使用场景:
具体来说,std::shared_mutex和std::shared_timed_mutex的使用场景包括:
-
读多写少的情况,例如在多线程的服务器应用程序中,多个线程需要读取某些共享数据,但只有少数线程需要修改这些数据。
-
需要对共享数据进行时间限制的情况,例如在超时机制中,需要对共享数据进行读取和写入,并且需要限制读取和写入操作的时间。
需要注意的是,std::shared_mutex和std::shared_timed_mutex适用于读多写少的场景,如果读写操作的比例接近1:1,建议使用std::mutex或std::timed_mutex等其他的同步原语。
- 使用方法
- 通常情况下,std::shared_lock 和 std::shared_mutex 是一起使用的。
- std::shared_mutex 是 C++标准库中提供的一种多读单写的互斥量,它允许多个线程同时获取共享锁,但只允许一个线程获取独占锁。因此,std::shared_mutex 适用于多个线程同时读取同一个数据结构,但只有一个线程写入数据结构的场景。
- 而 std::shared_lock 则是用于获取共享锁的一种锁类型,它可以让多个线程同时持有共享锁,而不会互相阻塞。当共享锁被多个线程持有时,其它线程只能获取共享锁,而不能获取独占锁。
- 因此,通常情况下,当我们需要保护一个数据结构时,可以使用 std::shared_mutex 作为互斥量,并使用 std::shared_lock 来获取共享锁,以实现多线程的读操作。同时,当我们需要写入数据结构时,可以使用 std::unique_lock 来获取独占锁,以保证数据的一致性和正确性。
- 综上所述,std::shared_lock 和 std::shared_mutex 的搭配使用可以实现多读单写的场景,提高了程序的并发性能。
使用std::shared_mutex的实例:
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <shared_mutex>std::vector<int> vec;
std::shared_mutex smutex;void writer()
{for(int i = 0; i < 100; ++i){std::unique_lock<std::shared_mutex> lock(smutex);vec.push_back(i);}
}void reader(int id)
{for(int i = 0; i < 10; ++i){std::shared_lock<std::shared_mutex> lock(smutex);std::cout << "Reader " << id << " is reading: ";for(int j = 0; j < vec.size(); ++j){std::cout << vec[j] << " ";}std::cout << std::endl;}
}int main()
{std::thread t1(writer);std::vector<std::thread> threads;for(int i = 0; i < 5; ++i){threads.push_back(std::thread(reader, i+1));}t1.join();for(int i = 0; i < 5; ++i){threads[i].join();}return 0;
}
在上述代码中,我们定义了一个全局的std::vector类型的vec变量,同时定义了一个std::shared_mutex类型的smutex变量,用于对vec变量进行读写保护。在writer函数中,我们对vec变量进行100次写入操作,而在reader函数中,我们对vec变量进行10次读取操作。在reader函数中,我们使用std::shared_lock类型的锁来对vec进行读取保护,而在writer函数中,我们使用std::unique_lock类型的锁来对vec进行写入保护。
在 reader() 函数中,我们使用 std::shared_lock 来获取共享锁,而不使用 std::unique_lock来获取独占锁,是因为共享锁可以被多个线程同时获取,而独占锁只能被一个线程获取。
在这个示例中,我们希望多个线程可以同时读取变量 data 的值,因此我们需要使用共享锁来保护这个变量。如果我们使用独占锁来保护变量data,那么只有一个线程可以获取锁,其他线程将被阻塞,无法读取变量 data。
使用 std::shared_lock的另一个好处是它可以允许多个线程同时获取锁,从而提高并发性能。当多个线程都只需要读取数据时,使用共享锁可以避免线程之间的竞争,提高程序的效率。
因此,在这个示例中,使用 std::shared_lock 是正确的选择,可以保证多个线程可以同时读取变量 data的值,而不会导致线程之间的竞争和阻塞。
- std::shared_lock和std::unique_lock的区别:
std::shared_lock 和 std::unique_lock 是 C++标准库中两种不同类型的锁,它们之间的主要区别在于锁的所有权和线程的并发性。
1. 锁的所有权
std::unique_lock 拥有独占锁,它允许一个线程独占锁,并且可以在锁定期间多次释放和获取锁。std::shared_lock 拥有共享锁,它允许多个线程同时获取共享锁,但不能独占锁。在锁定期间,std::shared_lock 和 std::unique_lock 都可以释放和获取锁。
2. 线程的并发性 std::unique_lock 允许线程独占锁,因此只能有一个线程同时持有 std::unique_lock。而 std::shared_lock 允许多个线程同时获取共享锁,因此可以有多个线程同时持有std::shared_lock。这样可以提高程序的并发性能。
3. 另外,std::unique_lock 和 std::shared_lock 还有一些其他的区别:
- 构造函数的参数
std::unique_lock 的构造函数可以接受一个 std::defer_lock 参数,这意味着它可以在构造时不立即获取锁。而 std::shared_lock 没有这个参数,它必须在构造时获取锁。- 释放锁的方式
std::unique_lock 可以通过调用 unlock() 方法来释放锁,也可以在其作用域结束时自动释放锁。而 std::shared_lock 只能在其作用域结束时自动释放锁。总的来说,std::unique_lock 适用于需要独占锁的场景,而 std::shared_lock适用于需要共享锁的场景。在使用这两种锁时,需要根据实际需求选择合适的锁类型。
使用std::shared_timed_mutex的实例:
#include <iostream>
#include <queue>
#include <thread>
#include <mutex>
#include <shared_mutex>std::queue<int> q;
std::shared_timed_mutex mutex;void read_queue(int id)
{std::shared_lock<std::shared_timed_mutex> lock(mutex, std::chrono::seconds(1));if (lock.owns_lock()){while (!q.empty()){int val = q.front();q.pop();std::cout << "Thread " << id << " read value: " << val << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(10));}}else{std::cout << "Thread " << id << " failed to read queue." << std::endl;}
}void write_queue()
{std::unique_lock<std::shared_timed_mutex> lock(mutex);for (int i = 0; i < 10; ++i){q.push(i);std::cout << "Write value: " << i << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(100));}
}int main()
{std::thread t1(read_queue, 1);std::thread t2(read_queue, 2);std::thread t3(write_queue);t1.join();t2.join();t3.join();return 0;
}
我们在read_queue()函数里面使用 std::shared_timed_lock,可以设置一个超时时间。这样,如果在指定的时间内无法获得共享锁,则会返回 false,从而避免了线程长时间等待的问题。在本例中,超时时间设置为 1 秒钟。
lock.owns_lock() 是一个成员函数,用于判断当前线程是否拥有一个互斥量的所有权。
在使用 std::unique_lock 或 std::shared_lock 时,可以使用该函数来检查当前线程是否持有锁,从而避免出现未拥有锁的情况下的操作。该函数返回一个 bool类型的值,如果当前线程持有锁,则返回 true,否则返回 false。
在多线程环境下,如果线程试图对一个已经被其他线程持有的锁进行操作,可能会出现死锁等问题,使用 owns_lock()函数可以避免这种情况的发生。例如,在使用 std::unique_lock 时,可以使用如下代码来检查当前线程是否持有锁:
std::unique_lock<std::mutex> lock(mtx); if (lock.owns_lock()) {// 当前线程持有锁,可以进行相关操作 } }
如果当前线程没有持有锁,则不能对锁进行相关操作。
1.2.3 递归加锁
递归锁(recursive lock)是一种特殊的锁类型,它允许同一个线程多次获取锁而不会出现死锁的情况。当线程尝试再次获取已经拥有的递归锁时,锁的计数器会增加,当线程释放锁时,计数器会减少,只有当计数器为 0 时,锁才会被完全释放。
递归锁通常用于以下情况:
-
嵌套调用:在一个函数中调用了另一个函数,这两个函数都需要获取同一个锁。如果不使用递归锁,那么在第二个函数中获取同一个锁时就会出现死锁的情况。而使用递归锁则可以避免这种情况。
-
递归算法:在一些递归算法中,同一个线程需要多次获取同一个锁,如果不使用递归锁,就会出现死锁的情况。而使用递归锁则可以避免这种情况。
需要注意的是,递归锁虽然可以避免死锁问题,但是由于它需要维护锁的计数器,所以会增加一些额外的开销。因此,在使用递归锁时需要考虑这种额外开销是否会影响性能。
如下是一个实例:
#include <iostream>
#include <thread>
#include <mutex>std::recursive_mutex recmtx;void func(int depth)
{std::lock_guard<std::recursive_mutex> lock(recmtx);std::cout << "depth: " << depth << std::endl;if (depth > 0) {func(depth - 1);}
}int main()
{func(3);return 0;
}
在上面的例子中,我们定义了一个递归函数 func,它接受一个参数 depth 表示递归深度。在 func 中,我们使用了std::lock_guard<std::recursive_mutex> 来创建了一个递归锁,并在每次递归时使用了这个递归锁来保护共享数据(这里共享数据只是简单地输出了一个递归深度)。
在 main 函数中,我们调用了 func(3),这会使 func 函数递归地调用自身三次,并在每次调用时输出当前的递归深度。
由于 func 函数在递归调用时使用了递归锁,所以即使在递归调用时多次获取同一个锁,也不会出现死锁的情况。
但是要注意的是:如果在使用递归锁时没有正确地处理计数器,也可能会导致死锁的问题。例如,在下面这个例子中:
#include <iostream>
#include <thread>
#include <mutex>std::recursive_mutex mtx;void func(int depth)
{mtx.lock();std::cout << "depth: " << depth << std::endl;if (depth > 0) {func(depth - 1);mtx.unlock();std::cout << " unlock" << std::endl;}//这里漏掉一次解锁
}int main()
{func(3);std::thread t(func, 3);t.join();return 0;
}
在这个例子中,我们使用了 std::recursive_mutex 创建了一个递归锁,并在 func 函数中多次获取和释放锁。然而,由于在最后一次递归调用后,我们漏掉了一次解锁操作,导致锁无法完全释放,从而出现了死锁的情况。
因此,在使用递归锁时,我们需要仔细地处理锁的计数器,确保锁的计数器在每次获取和释放锁时都能正确地增加和减少。另外,在使用递归锁时,为了保证程序的可读性和可维护性,建议使用 C++11 中提供的 std::lock_guard 和 std::unique_lock 等 RAII 类型来管理锁的获取和释放,这样可以避免忘记解锁的情况。
最后再来回顾一下std::recursive_mutex这个锁:
- 当多个线程需要访问共享资源时,通常需要使用锁来保护共享资源。在 C++11 中,标准库提供了 std::mutex类来实现互斥锁的功能。但是,如果在一个线程中多次获取同一个互斥锁,就会出现死锁的情况,因为同一个线程在持有锁的同时,又试图获取这个锁,这种情况下,我们需要使用递归锁(std::recursive_mutex)。std::recursive_mutex是一个可递归的互斥锁,它可以在同一个线程中多次获取锁而不会出现死锁的情况。每个递归锁对象内部有一个计数器来记录这个锁的拥有者是同一个线程,还是不同的线程。如果同一个线程多次获取锁,计数器会递增,而每次释放锁,计数器会递减,只有当计数器降为0 时,锁才被真正释放,其他线程才能够获取到这个锁。
- 递归锁的特点是可以在同一个线程中多次获取和释放锁,但是要注意,每次获取锁的次数和释放锁的次数必须相同,否则会出现死锁的情况。
- 使用递归锁的代码和使用普通锁的代码类似,不同的是,如果需要在同一个线程中多次获取锁,就需要使用 std::recursive_mutex类来创建递归锁对象。
相关文章:
生活为什么总是对我动手?
睡衣出门取外卖,不想遇见熟人,偏偏被好多熟人看见;看电影时昏昏欲睡,中途上个厕所,就错过高光片段;在景区排队买票,Tips:当你看到这个提示的时候,说明当前的文章是由原emlog博客系统搬迁至此的,文章发布时间已过于久远,编排和内容不一定完整,还请谅解` 生活为什么总…...
如何理解“先学会爱自己,再学着爱他人”这句话?
这句话传达了一种非常重要的人生智慧和价值观。我们可以从以下几个方面来理解它: 1. 自尊自爱是健康生活的基础如果一个人连自己都不爱,缺乏对自我的尊重和接纳,很难去真正关爱、理解和包容别人。自我否定的人往往更加孤僻、冷淡,难以与他人建立良好的人际关系。 2. 平衡自我和…...
《Object Detection Using ClusteringAlgorithm Adaptive Searching Regions in Aerial Images》论文10问
《Object Detection Using ClusteringAlgorithm Adaptive Searching Regions in Aerial Images》论文10问 Q1 论文试图解决什么问题?小物体分布不均匀,主要问题是分辨率低、信息量小,导致特征表达能力弱;传统方法如放大图像,会增加处理时间和存储大型特征图所需的内存,图…...
Unity遮挡剔除使用
(1)设置对象为遮挡物or被遮挡物 选中某个对象后,在其Occlusion窗口可以设置其Occluder Static和Occludee Static属性:遮挡物通过勾选一个对象的Occluder Static 可将其设置为静态遮挡物。理想的遮挡物应该是实心的,体积较大的物体。 原则1:可能阻挡到其它对象的对象才应该…...
asdf
asf本文版权归作者和博客园共有,欢迎转载,转载请注明原文链接:https://www.cnblogs.com/lllliuxiaoxia/p/18200863另外欢迎关注公众号,一起讨论学习...
hdu1025java
1:dp+二分 NlogN的复杂度 2:注意 road 与roads区别 3:注意输入不能用Scanner 4:注意格式 最后是要输出两个空行 假设存在一个序列d[1..9] = 2 1 5 3 6 4 8 9 7,可以看出来它的LIS长度为5。下面一步一步试着找出它。我们定义一个序列B,然后令 i = 1 to 9 逐个考察这个序列…...
C++并发编程之二 在线程间共享数据
文章目录1.1 互斥锁(mutex)保护共享数据1.1.1 std::mutex 的成员函数 std::mutex::lock() 和std::mutex::unlock() (不推荐使用)1.1.2 使用std::lock_guard保护共享数据1.1.3 使用std::unique_lock保护共享数据1.2 保护共享数据的其他方式1.2.1 初始化过…...
FL Studio和Cubase哪个容易一些 FL Studio和Cubase修音哪个好
FL Studio和Cubase哪个容易一些?FL Studio是很适合新手使用的宿主软件。FL Studio和Cubase修音哪个好?FL Studio和Cubase在修音方面各有千秋。 一、FL Studio和Cubase哪个容易一些 FL Studio是很适合新手上手的宿主软件,这得益于FL Studio独…...
限定学校|在站博士后省公派新加坡国立大学从事博后研究
Y博士为国内在站博士后,我们向其推荐了人社部博管办国外博士后派出项目及所在省的相关项目,最终助其获得新加坡国立大学的博士后邀请函,鉴于该导师名列全球高被引科学家榜单,顺利获批省国际培养博士后资助项目,如期出国…...
信息技术最全总结(备考教资)
信息技术 备考教资信息技术知识点总结,欢迎收藏!需要xmind和备考书籍的可以评论区留言。 第一部分-学科专业知识 第一章-信息技术基础知识 信息与信息技术概述 信息概述 信息的定义 信息本身不是实体信息是通过文字、数字、图像、图形、声音、视频等方…...
spring5(三):IOC操作Bean管理(基于xml方式)
IOC操作Bean管理(基于xml方式)前言一、基于 xml 方式创建对象二、基于 xml 方式注入属性1. 使用 set 方法进行属性注入2. 使用有参数构造进行属性注入3. p 名称空间注入简化操作(了解)三、xml 注入其它类型属性1. 字面量2. 注入属…...
Vue的快速上手
一、创建一个 Vue 应用 前提条件 熟悉命令行已安装 16.0 或更高版本的 Node.js在本篇中,我们将介绍如何在本地搭建 Vue 单页应用。创建的项目将使用基于 Vite 的构建设置,并允许我们使用 Vue 的单文件组件 (SFC)。 确保你安装了最新版本的 Node.js&…...
通过ELK+kafka采集微服务日志
在springboot微服务中采集日志推送kafka背景整体流程图快速搭建kafkazk开发环境通过logback记录日志到kafka快速搭建ELK环境Kibana查看,统计日志背景 在分布式的项目中,各功能模块产生的日志比较分散,同时为满足性能要求,同一个微…...
开启新航路,拓尔思发力AIGC市场 | 爱分析调研
2022年,随着AI聊天机器人GhatGPT在世界范围内持续火爆,极具创意、表现力、个性化且能快速迭代的AIGC技术成功破圈,成为全民讨论热点。 AIGC是指在确定主题下,由算法模型自动生成内容,包括单模态内容如文本、图像、音频…...
01-死磕QNX someip
1. vsomeip3.1.20版本 环境配置 export COMMONAPI_CONFIG/etc/commonapi.ini export LD_LIBRARY_PATH/sdcard/someip:$LD_LIBRARY_PATH export VSOMEIP_CONFIGURATION/etc/vsomeip-service.json export VSOMEIP_APPLICATION_NAMEHelloWorldSomeIPService sysctl -w net.ine…...
OTFS输入输出关系
目录 1. OTFS输入输出关系的矩阵表示 1.1 OTFS:矩阵表示 1.2 OTFS发射机的实现 1.2.1 传统的OTFS调制 ISFFT 海森堡变换 1.2.2 基于IDZT的OTFS调制 1.3 OTFS接收机的实现 1.3.1 传统的OTFS解调 维格纳变换 SFFT 1.3.2 基于DZT的OTFS解调 Appendix-Matlab C…...
Java的抽象类和接口
目录 一 、抽象类 1、抽象类概念 2、抽象类语法 3、抽象类特性 4、抽象类的作用 二、接口 1、接口的概念 2、语法规则 3、接口使用 4、接口特性 5、实现多个接口 6、接口间的继承 7、抽象类和接口的区别 8、接口使用实例 9、Clonable 接口和深拷贝 三、Object类…...
作为一个女测试员是什么样的体验?
面试时极度紧张,语无伦次,觉得肯定没戏,最后却拿到高薪offer。 工作之后我听同事们讲,测试总监面试官并没打算要我,但身边的人都问他: 那个小姐姐什么时候来报道?... 于是在众人的期待的目光…...
移动端 REM 适配
Vant 中的样式默认使用 px 作为单位,如果需要使用 rem 单位,推荐使用以下两个工具: postcss-pxtorem 是一款 postcss 插件,用于将单位转化为 rem lib-flexible 用于设置 rem 基准值 下面我们分别将这两个工具配置到项目中完成 R…...
哈希表【leetcode】
笔记:代码随想录 理论 概念 哈希表(hash table,散列表):是根据关键码的值而直接访问的数据结构,说白,数组就是一张哈希表。 哈希函数:把变量直接映射为表上的索引。 哈希碰撞&a…...
【Gem5】有关gem5模拟器的资料导航
网上有关gem5模拟器的资料、博客良莠不齐,这里记录一些总结的很好的博客与自己的学习探索。 一、gem5模拟器使用入门 官方的教程: learning_gem5:包括gem5简介、修改扩展gem5的示例、Ruby相关的缓存一致性等。gem5 Documentation࿱…...
R语言中apply系列函数详解
文章目录applylapply, sapply, vapplyrapplytapplymapplyR语言的循环效率并不高,所以并不推荐循环以及循环嵌套。为了实现循环功能的情况下,兼顾效率,R语言提供了apply系列函数,用于对规则的数据进行函数式的迭代处理。 apply a…...
AOP的另类用法 (权限校验自定义注解)
👳我亲爱的各位大佬们好😘😘😘 ♨️本篇文章记录的为 AOP的另类用法 (权限校验&&自定义注解) 相关内容,适合在学Java的小白,帮助新手快速上手,也适合复习中,面试中的大佬🙉🙉…...
【机器学习】机器学习建模调参方法总结
文章目录一、前言1.1 数据来源1.2 理论简介二、知识总结2.1 回归分析2.2 长尾分布2.3 欠拟合与过拟合2.4 正则化2.5 调参方法2.5.1 贪心调参 (坐标下降)2.5.2 网格调参GridSearchCV2.5.3 贝叶斯调参三、建模与调参3.1 线性回归3.1.1 模型建立3.1.2 查看训…...
Python数据分析师|Pandas之基础知识
版权声明:原创不易,本文禁止抄袭、转载,侵权必究! 目录一、数据分析简介二、数据分析简介三、数据查看四、知识总结五、作者Info一、数据分析简介 随着科技的发展,数据变得尤为重要,甚至有着“数据为王”&…...
macOS 13.3(22E252)/12.6.4/11.7.5正式版发布
系统介绍 3 月 28 日消息,苹果今日向 Mac 电脑用户推送了 macOS 13.3 更新(内部版本号:22E252)苹果今天还发布了macOS Monterey 12.6.4和macOS Big Sur 11.7.5,本次更新距离上次发布隔了 42 天。 macOS Ventura 带来…...
速度与兼容性功能大比拼:7款浏览器测评,哪一款更好用
还在为使用哪款浏览器而发愁吗?电脑配置低,又想浏览网页顺畅、下载速度快,那么就要挑选功能齐全、速度快的浏览器。话不多说,给大家做了7款浏览器的最全测评,看看2023年哪个浏览器更好用,更适合自己。 本次…...
【建议收藏】2023年中高级软件测试大厂面试秘籍,为你保驾护航金三银四,直通大厂
前言 从几十份顶级面试仓库和300多篇高质量面经中总结出一份全面成体系化的软件测试高级面试题集。 随着软件测试岗位技术发展的成熟,自动化、性能、框架等一下子就进入了我们的视野内,同时,软件测试自身的技术栈也正在不断扩展,…...
Cinema 4D 2023.1.3安装包下载及安装教程
[软件名称]:Cinema 4D 2023.1.3 [软件大小]: 1.0GB [安装环境]: Win11/Win 10 [软件安装包下载]: https://pan.quark.cn/s/c2324deaa028 CINEMA 4D字面意思是4D电影,不过其本身就是3D的表现软件,由德国Maxon Computer开发,以极高的运算速度和…...
IDEA vs Eclipse:使用体验对比
1. 概述 IDEA 和 Eclipse 都是常见的集成开发环境(IDE),用于编写和调试代码。它们都有一些共同的功能,例如代码编辑器、调试器、版本控制等等。但是在具体的使用体验上,它们有很多不同之处。 本文将对 IDEA 和 Eclip…...
新手学SpringCloud前需知道的5点
目录 第一点: 什么是微服务架构 第二点:为什么需要学习Spring Cloud 第三点: Spring Cloud 是什么 第四点: SpringCloud的优缺点 1、SpringCloud优点 2、SpringCloud缺点 第五点: SpringCloud由什么组成 1&…...
Java集合—HashMap为什么2倍扩容 、HashMap的key允许空值而Hashtable却不允许
目录 1. Java集合—HashMap为什么2倍扩容 1.1 从源码分析 2. 为什么HashMap的key允许空值,而Hashtable却不允许 2.1 从源码分析 1. Java集合—HashMap为什么2倍扩容 HashMap的初始容量都是2的n次幂的形式存在的,而扩容也是2倍的原来的容量进行扩容&a…...
国内、外(翻)的新闻网站推荐
也许有很多朋友和我一样,小小螺丝天天关注国家大事,总喜欢在茶余饭后关注下国内外新闻,除了新闻广播和电视之外还能有哪些方式呢?今天就给大家盘点总结一下。 一、国内 1.今日头条:链接:今日头条 推荐等…...
PHP初级教程------------------(2)
目录 运算符 赋值运算符 算术运算符 比较运算符 逻辑运算符 连接运算符 错误抑制符 三目运算符 自操作运算符 编辑 计算机码 位运算符 运算符优先级 流程控制 控制分类 顺序结构 分支结构 If分支 Switch分支 循环结构 For循环 while循环 do-while循环 循环控制 …...
js的递归函数——实现可收放的树形菜单
递归函数实现树形菜单创建假数据或者请求接口数据定义递归函数,处理数据调用函数,渲染页面效果展示完整代码树形菜单是一种常见的网站导航方式,它通常由多个层级的菜单项组成,每个菜单项可以有子菜单项。在JavaScript中࿰…...
如何高效搭建资产管理平台?众安科技告诉你答案是图技术
本⽂整理⾃ NebulaGraph x 阿⾥云计算巢专场中众安保险的⼤数据应⽤⾼级专家曾⼒带来的《众安资产在 NebulaGraph 的应⽤实践》分享,视频⻅链接。 ⼤家好,我是众安数据科学应⽤中⼼的曾⼒,今天很⾼兴在这⾥可以跟⼤家分享 NebulaGraph 在众安…...
使用mybatis-plus-generator配置一套适合你的CRUD
1、maven引入 mybatis-plus-generator 和模板引擎,你也可以使用freemarker之类的,看个人 <!-- mybatisplus代码生成器 --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-generator</artifactI…...
vue门户网站,滚动到可视化区域展示动画效果方案
1.准备两个工具库: (1.1) animate.css:动画库 (动画效果展示:Animate.css | A cross-browser library of CSS animations.) (1.2)wowjs: 负责滚动到可视化区域ÿ…...
【0180】PG内核读取pg_hba.conf并创建HbaLine记录(1)
文章目录 1. pg_hba.conf文件是什么?2. postmaster何时读取pg_hba.conf?2.1 pg内核使用pg_hba.conf完成客户端认证的原理2.2 读取pg_hba.conf的几个模块3. pg内核读取pg_hba.conf过程3.1 VFD机制获取文件描述符3.2 根据fd读取文件内容相关阅读: 【0178】DBeaver、pgAdmin I…...
【五】线程安全VS线程不安全
1. Java内存模型的特征 Java内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性这三个特征来建立。下面逐个看下哪些操作实现这三个特性: 1.1 原子性(Atomicity) 由Java内存模型来直接保证的原子性变量操作包括 read、load、assig…...
【大屏设计方案】
大屏设计方案一、非等比放大(填充满整个屏幕)目的屏幕比例大小和设计稿的差的不多目的屏幕比例大小和设计稿的差很多二、等比放大(比如16:9)解决方案之后就可以用rem了,有两种便利的方式:也可以用media 根据不同的屏幕…...
input 标签原生实现数字选择器
在使用开源的UI框架时,实现数字选择器很容易,直接拷贝就行,如iview、elementui...等。但有时项目不使用第三方样式库,这样的话UI控件只能自己实现。笔者就遇到过这样的情况,自己实现数字选择器,下面分享下实…...
大数据项目实战之数据仓库:用户行为采集平台——第3章 用户行为日志
第3章 用户行为日志 3.1 用户行为日志概述 用户行为日志的内容,主要包括用户的各项行为信息以及行为所处的环境信息。收集这些信息的主要目的是优化产品和为各项分析统计指标提供数据支撑。收集这些信息的手段通常为埋点。 目前主流的埋点方式,有代码…...
zookeeper 学习(一):简单认识zookeeper
zookeeper 学习(一):简单认识zookeeper zk官网 zk是什么? highly reliable distributed coordination. 来自官网,高可用的分布式协调服务,应用最多的场景就是用来做其他服务中间件的协调者,我最开始接触zk ,…...
IP 归属用 Ip2region 就够了
文章目录Ip2region 简介是什么特性支持的编程语言案例实操依赖获取IP输入流转化解析IP测试抖音、微博、小红书等各平台相继上线" 网络用户IP地址显示功能", 境外显示 国家, 境内显示到 省市,且该功能无法关闭,IP地址为强…...
最小生成树kruskal-修建公路1
题目描述 L 城一共有 N 个小区。 小明是城市建设的规划者,他计划在城市修 M 条路,每修建一条路都要支付工人们相应的工钱(需要支付的工钱 = 路的长度)。 然而小明所拿到的经费并不够支付修建 M 条路的工钱,于是迫于无奈,他只能将计划改变为修建若干条路,使得 N 个小区之…...
Linux常用命令汇总
一、文件和文件夹操作 1、进入某一文件夹: cd xxx/yyy/简便记忆:改变目录,change directory,cd 2、复制文件到另一个文件: cp xxx/xxx.yyy ddd/rrr.zzz简便记忆:复制,copy,cp 第…...
vue部署包可配置后台接口地址
有时候一个部署包可能要应用于不同服务器,这就需要频繁更换后台地址。 由此引申出了部署包可直接配置后台地址的需求,这样就不需要频繁打包了。 方法 1. public 新增 config.js 文件(会被恶意修改地址,不安全) publi…...
企业电子采购系统:采购过程更规范,更透明
满足采购业务全程数字化, 实现供应商管理、采购需求、全网寻源、全网比价、电子招 投标、合同订单执行的全过程管理。 电子招标采购,是指在网上寻源和采购产品和服务的过程。对于企业和企业主来说,这是个既省钱又能提高供应链效率的有效方法…...
php 修改服务器文件上传大小限制
输入docker cp mlfnginx:/etc/nginx/conf.d/pl.conf .输入vimpl.conf 修改nginx配置文件移动到图中所示位置client_max_body_size 按键盘”i”对图中的xxM修改成需要的大小,然后按”esc”,在按”:wq”,最后按回车键输入docker cp ./pl.con…...
PICO640红外探测器配置模块verilog
目录 一、PICO640 Introduction 二、实现代码 一、PICO640 Introduction PICO640 Gen2™ is a high resolution (640x480 pixels) infrared image sensor for military, surveillance or thermography applications. It makes use of the ULIS latest, state o…...
简单易懂 关于nextTick()的理解
前言 学习Vue的过程中,大家肯定都使用过nextTick(),关于nextTick()的使用时机,大家肯定都知道,那么它的工作原理是怎样的呢,本文就简单说一下它的nextTick()的工作原理。 nextTick的使用时机 首先记住:n…...
第一节:auto.js开发环境搭建
相关学习文档:http://doc.autoxjs.com 一、下载并安装 Visual Studio Code 下载地址:https://code.visualstudio.com/ 二、安装 auto.js 插件 启动 Visual Studio Code按住 ctrlshift x输入Auto.js-Autox.js-VSCodeExt点击安装即可 三、启动 auto.…...
看他人代码小总结
针对几个功能类似的函数: 1.需要经常调试则定义一个参数比如is_debug来选择是否在调试,定义一些参数专门用于调试用,不用每次都修改这些参数,只需要修改is_debug这个参数; 2.把其中的变量(常量)单独拎出来放到一个文件…...
第四季新星计划即将开启,博客之星取消拉票你怎么看?
catalogue🌟 写在前面🌟 线下创机遇🌟 新星计划🌟 做导师可以得到什么🌟 新星计划跟原力计划有何不同?🌟 博客之星新玩法你怎么看?🌟 写在前面 哈喽,大家好&…...
AI绘画关键词网站推荐 :轻松获取百万个提示词!完全免费
一、lexica.art 该网站拥有数百万Stable Diffusion案例的文字描述和图片,可以为大家提供足够的创作灵感。 使用上也很简单,只要在搜索框输入简单的关键词或上传图片,就能为你提供大量风格不同的照片。点击照片就能看到完整的AI关键词&#…...
计算机网络实验3:路由器安全防控配置
实验目的和要求 理解标准IP访问控制列表的原理及功能理解CHAP、DHCP配置原理了解家用式无线路由配置方法实验项目内容 标准IP访问控制列表配置 CHAP验证路由器上配置DHCP网络地址转换NAT配置无线路由实现实验环境 1. 硬件:PC机; 2. 软件:W…...
现代R语言【Tidyverse、Tidymodel】的机器学习
机器学习已经成为继理论、实验和数值计算之后的科研“第四范式”,是发现新规律,总结和分析实验结果的利器。机器学习涉及的理论和方法繁多,编程相当复杂,一直是阻碍机器学习大范围应用的主要困难之一,由此诞生了Python…...
图像归一化处理
归一化 归一化是一种简化计算的方式,即将有量纲的表达式,经过变换,化为无量纲的表达式,成为标量。 在多种计算中都经常用到这种方法。 简单介绍 归一化是一种无量纲处理手段,使物理系统数值的绝对值变成某种相对值关…...
GPT-4o:融合文本、音频和图像的全方位人机交互体验
引言: GPT-4o(“o”代表“omni”)的问世标志着人机交互领域的一次重要突破。它不仅接受文本、音频和图像的任意组合作为输入,还能生成文本、音频和图像输出的任意组合。这一全新的模型不仅在响应速度上达到了惊人的水平࿰…...
[NOI Online #2 入门组] 未了
[NOI Online #2 入门组] 未了 题目描述 由于触犯天神,Sisyphus 将要接受惩罚。 宙斯命 Sisyphus 推一块巨石上长度为 L L L 的山坡。Sisyphus 匀速向上推的速度为每年 v v v 的长度(由于是匀速,故经过 1 2 \frac{1}{2} 21 年将能向上…...
win的开发环境配置(Java开发必备)
文章目录 日常app工具类app开发类app环境类app 由于每次换新工作、用一台临时或者新的电脑时总是要想着要下载什么软件,这次就一次性全部记录下来,将必须下载的内容做个记录。 日常app 百度网盘、微信、网易云、搜狗 工具类app office、bandizip&…...
VUE中,图片正常显示但是触发了@error
我的项目中图片是从后端获取的,不能确定后端返回的图片链接是否能正常访问,并且希望当链接异常时直接不显示图片,而不是裂口的图片,所以我的代码如下: <img :src"logoUrl" class"logo" id&quo…...
584. 寻找用户推荐人
584. 寻找用户推荐人 题目链接:584. 寻找用户推荐人 代码如下: # Write your MySQL query statement below select name from Customer where referee_id is null or referee_id<>2;...
C++ STL概念之 仿函数(函数对象)/ 空间配置器 / 适配器 / 理解STL
仿函数(函数对象) 什么是仿函数 仿函数,或称为函数对象,在C中是通过重载operator()的类实例,使得类的实例能够像函数一样被调用。 可调用对象 函数指针(Function Pointers): 这是指向函数的指…...
Qt+C++串口调试工具
程序示例精选 QtC串口调试工具 如需安装运行环境或远程调试,见文章底部个人QQ名片,由专业技术人员远程协助! 前言 这篇博客针对《QtC串口调试工具》编写代码,代码整洁,规则,易读。 学习与应用推荐首选。 …...
太极图形学——高级数据结构——稀疏
我们在之前学习的稠密数据结构中主要可以分为root,dense,和field三个,而实际上我们还可以定义一个bitmasked和pointer这两个就是用来帮助我们维护空间稀疏性用的 举一个例子,首先是一个稠密结构,它的数据利用率很低 那…...