1、概述
锁类型
c11提供了跨平台的线程同步手段,用来保护多线程同时访问的共享数据。
std::mutex,最基本的 Mutex 类,独占的互斥量,不能递归使用。
std::time_mutex,带超时的独占互斥量,不能递归使用。
std::recursive_mutex,递归互斥量,不带超时功能。
std::recursive_timed_mutex,带超时的递归互斥量。
lock类型
std::lock_guard,与 Mutex RAII 相关,方便线程对互斥量上锁。
std::unique_lock,与 Mutex RAII 相关,方便线程对互斥量上锁,但提供了更好的上锁和解锁控制。
锁函数
std::try_lock,尝试同时对多个互斥量上锁。
std::lock,可以同时对多个互斥量上锁。
std::unlock,解锁。
2、独占互斥量std::mutex
互斥量使用都很简单,接口用法也很简单。一般是通过lock()来阻塞线程,直到获取到互斥量为止。在获取互斥量完成之后,使用unlock()来解除互斥量的占用。lock()和unlock()必须成对出现。
std::mutex不允许拷贝构造,也不允许 move 拷贝,最初产生的 mutex 对象是处于 unlocked 状态的。
lock(),调用线程将锁住该互斥量。线程调用该函数会发生下面 3 种情况:(1). 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁。(2). 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。(3). 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
unlock(), 解锁,释放对互斥量的所有权。
try_lock(),尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。线程调用该函数也会出现下面 3 种情况,(1). 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。(2). 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉。(3). 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
#include <iostream> #include <thread> #include <chrono> #include <mutex> std::mutex g_lock; void vFunc() { g_lock.lock(); std::cout << "entered thread:" << std::this_thread::get_id() << std::endl; std::this_thread::sleep_for(std::chrono::seconds(1)); std::cout << "leave thread:" << std::this_thread::get_id() << std::endl; g_lock.unlock(); } int main() { std::thread t(vFunc); std::thread t1(vFunc); std::thread t2(vFunc); t.join(); t1.join(); t2.join(); return 0; } //输出结果 entered thread:4420 leave thread:4420 entered thread:5288 leave thread:5288 entered thread:1432 leave thread:1432
3、std::lock_guard和std::unique_lock
使用lock_guard和std::unique_lock可以简化lock/unlock的写法,同时也更安全,因为lock_guard使用了RAII技术,在构造分配资源,在析构释放资源,会在构造的时候自动锁定互斥量,在退出作用域之后进行析构自动解锁。所以不用担心没有解锁的情况,更加安全。
std::lock_guard与 Mutex RAII 相关,方便线程对互斥量上锁,std::unique_lock与 Mutex RAII 相关,方便线程对互斥量上锁,但提供了更好的上锁和解锁控制,它可以自由的释放mutex,而std::lock_guard需要等生命周期结束才能释放。
void vFunc() { std::lock_guard<std::mutex> locker(g_lock);//出作用域自动解锁 std::cout << "entered thread:" << std::this_thread::get_id() << std::endl; std::this_thread::sleep_for(std::chrono::seconds(1)); std::cout << "leave thread:" << std::this_thread::get_id() << std::endl; }
3、递归互斥锁std::recuisive_mutex
同一个线程不能多次获取同一个独占互斥锁,一个线程多次获取同一个互斥锁发生死锁。
//std::mutex mutex; void funcA() { std::lock_guard<std::mutex> lock(mutex); //do something } void funcB() { std::lock_guard<std::mutex> lock(mutex); //do something } void funcC() { std::lock_guard<std::mutex> lock(mutex); funcA(); funcB(); } int main() { //发生死锁,funcC已经获取,无法释放,funcA无法获取 funcC(); return 0; }
为了解决死锁的问题,C11有了递归锁std::recursive_mutex,递归锁允许一个线程多次获取该锁。
//std::recursive_mutex mutex; void funcA() { std::lock_guard<std::recursive_mutex> lock(mutex); //do something } void funcB() { std::lock_guard<std::recursive_mutex> lock(mutex); //do something } void funcC() { std::lock_guard<std::recursive_mutex> lock(mutex); funcA(); funcB(); } int main() { //同一个线程可以多次获取同一互斥量,不会发生死锁 funcC(); return 0; }
需要注意的是,尽量少用递归锁,原因如下:
允许递归互斥容易放纵复杂逻辑的产生,从而导致一些多线程同步引起的晦涩问题;
递归锁比非递归锁效率低;
递归锁虽然可以在同一线程多次获取,但是获取次数过多容易发生问题,引发std::system错误。
4、超时锁
std::time_mutex是超时的独占锁,std::recursive_timed_mutex是超时的递归锁,主要用于在获取锁时增加超时等待功能,设置一个等待获取锁的时间,超时后做其他的事情。超时锁多了两个获取锁的接口,try_lock_for和try_lock_until,这两个接口用来获取互斥量的超时时间。
try_lock_for 函数接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与 std::mutex 的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回 false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。
try_lock_until函数则接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。
#include <iostream> // std::cout #include <chrono> // std::chrono::milliseconds #include <thread> // std::thread #include <mutex> // std::timed_mutex std::timed_mutex mtx; void fireworks() { // waiting to get a lock: each thread prints "-" every 200ms: while (!mtx.try_lock_for(std::chrono::milliseconds(200))) { std::cout << "-"; } // got a lock! - wait for 1s, then this thread prints "*" std::this_thread::sleep_for(std::chrono::milliseconds(1000)); std::cout << "* "; mtx.unlock(); } int main() { std::thread threads[10]; // spawn 10 threads: for (int i = 0; i<10; ++i) threads[i] = std::thread(fireworks); for (auto& th : threads) th.join(); return 0; }