C++提供了两种常用的锁,std::lock_guard<Lockable &T>和std::unique_lock<Lockable &T>。通常使用场景下,这两个锁用法一致。即,在构造锁对象时上锁,在析构锁对象时解锁。使用户从上锁/解锁操作中解放出来,有效地避免死锁。
1,考虑如下一般场景:两个线程访问同一个共享资源,使用lock_guard或者unique_lock都可以实现资源访问互斥。
#include <iostream>
#include <thread>
#include <mutex>
int var = 0;
std::mutex gMutex;
void threadFun(int a) {
while (true) {
std::lock_guard<std::mutex> lock(gMutex); // 构造时上锁
// std::unique_lock<std::mutex> lock(gMutex); // 使用 unique_lock 也能达到同样的效果
var = a;
std::cout << "var=" << var << std::endl;
} // 析构时解锁
}
int main() {
std::thread th1(threadFun, 1);
std::thread th2(threadFun, 2);
th1.join();
th2.join();
return 0;
}
2,std::lock(Lockable &lock1, Lockable &lock2, Lockable &... lockn)
考虑到一个线程可能需要获取多个锁,为了避免类似哲学家就餐死锁的问题,C++11提供了std::lock()函数,该函数同时锁定多个锁,从而能够有效地避免死锁问题。注意:
(1)该函数要求调用者至少提供两个锁。
(2)标准库只提供了std::lock()上锁方法,没有提供对应的解锁函数。该函数主要目的是配合 std::lock_guard() 和 std::unique_lock() ,由 std::lock_guard() 和 std::unique_lock() 来自动解锁。详细解释见下面几节。
3,std::lock_guard
C++11 提供的std::lock_guard比较简单。构造函数有两个:
(1)lock_guard( mutex_type& m ):该构造函数需要提供一个mutex m。构造时,即上锁。如果m不是递归锁,并且在构造前,当前线程已经拥有该互斥锁,则行为是未定义的。
(2)lock_guard( mutex_type& m, std::adopt_lock_t t ):该构造函数需要提供一个mutex m和一个锁定策略。
锁定策略有如下三种:
defer_lock:std::defer_lock_t类型,不要求当前线程拥有mutex
try_to_lock:std::try_to_lock_t类型,非阻塞式获取mutex
adopt_lock:std::adopt_lock_t类型,假定当前线程已经拥有mutex
所以,在使用这个构造函数前,需要先把mutex上锁。
示例如下:
#include <iostream>
#include <thread>
#include <mutex>
int var_a = 0;
int var_b = 0;
std::mutex mutex_a;
std::mutex mutex_b;
void threadFun(int a, int b) {
while (true) {
std::lock(mutex_a, mutex_b); // 先上锁,同时锁定两个mutex
std::lock_guard<std::mutex> lock_a(mutex_a, std::adopt_lock); // std::adopt_lock表明当前线程已经给mutex_a上锁,退出时解锁
std::lock_guard<std::mutex> lock_b(mutex_b, std::adopt_lock); //std::adopt_lock表明当前线程已经给mutex_b上锁,退出时解锁
var_a = a;
var_b = b;
std::cout << "a=" << var_a << std::endl;
std::cout << "b=" << var_b << std::endl << std::endl;
}
}
int main() {
std::thread th1(threadFun, 1, 1);
std::thread th2(threadFun, 100, 100);
th1.join();
th2.join();
return 0;
}
上述示例展示了 std::lock_guard和std::lock配合锁定和解锁多个mutex。std::lock负责同时锁定多个锁,std::lock_guard负责解锁。
4,std::unique_lock
之前提到过,unique_lock在通常情况下跟lock_guard没有不同。lock_guard很简单,只有两个构造函数。而unique_lock却要复杂得多,unique_lock提供了很多种构造函数,用来进行更加精细和细粒度的控制。无论从内存还是运行效率上来讲,unique_lock都要比lock_guard开销稍大。unique_lock构造函数如下:
(1)unique_lock():默认构造函数。构造一个无mutex关联的unique_lock。析构不会解锁。
(2)unique_lock( unique_lock&& other ):移动构造函数。以other的内容初始化当前unique_lock,使得other无mutex关联。
(3)unique_lock( mutex_type& m ):关联mutex,内部锁定mutex m,析构解锁。如果mutex m非递归,且已经被当前线程占用,则行为是未定义的。
(4)unique_lock( mutex_type& m, std::defer_lock_t t ):关联mutex m,内部不锁定mutex m,析构不解锁。
(5)unique_lock( mutex_type& m, std::try_to_lock_t t ):关联mutex m,非阻塞地尝试锁定mutex m,析构解锁。如果mutex m非递归,且已经被当前线程占用,则行为是未定义的。
(6)unique_lock( mutex_type& m, std::adopt_lock_t t ):关联mutex m,内部不锁定mutex m,但是假定当前线程已经锁定mutex,析构解锁。
重点关注(4)和(6)。这两个构造函数都关联mutex m,但内部都不会调用m.lock()去锁定。唯一的区别是,adopt_lock会假定当前线程已经锁定mutex m,因此其析构函数会调用m.unlock()去解锁;而defer_lock会假定当前线程也没有锁定mutex m,因此其析构函数不会调用m.unlock()去解析。示例如下:
#include <iostream>
#include <thread>
#include <mutex>
int var_a = 0;
int var_b = 0;
std::mutex mutex_a;
std::mutex mutex_b;
void threadFun(int a, int b) {
while (true) {
std::lock(mutex_a, mutex_b);
std::unique_lock<std::mutex> lock_a(mutex_a, std::defer_lock); // defer_lock 构造时不锁定,析构时不解锁,循环会死锁。
std::unique_lock<std::mutex> lock_b(mutex_b, std::defer_lock); // 同上
// std::unique_lock<std::mutex> lock_a(mutex_a, std::adopt_lock); // adopt_lock构造时不锁定,析构时解锁,循环正常。
// std::unique_lock<std::mutex> lock_b(mutex_b, std::adopt_lock); // 同上
var_a = a;
var_b = b;
std::cout << "a=" << var_a << std::endl;
std::cout << "b=" << var_b << std::endl << std::endl;
}
}
int main() {
std::thread th1(threadFun, 1, 1);
std::thread th2(threadFun, 100, 100);
th1.join();
th2.join();
return 0;
}