前言
锁管理遵循RAII习语来处理资源。锁管理器在构造函数中自动绑定它的互斥体,并在析构函数中释放它。这大大减少了死锁的风险,因为运行时会处理互斥体。。
锁管理器在C++ 11中有两种:
用于简单的std::lock_guard,以及用于高级用例的std::unique_lock。
std::lock_guard
先来个小例子吧:
mutex m;
m.lock();
sharedVariable= getVar();
m.unlock();
在这点代码中,互斥体m确保关键部分sharedVariable= getVar();的访问是顺序的。
顺序意味着:在这种特殊情况下,每个线程按顺序获得对关键部分的访问。
代码很简单,但容易出现死锁。如果关键部分抛出异常或程序员只是忘记解锁互斥锁,则会出现死锁。
使用std::lock_guard,我们可以做到更优雅:
{
std::mutex m,
std::lock_guard<std::mutex> lockGuard(m);
sharedVariable= getVar();
}
这很容易。但是开括号 { 和闭括号 }是啥?
为了保证std::lock_guard生命周期只在这{}里面有效。
也就是说,当生命周期离开临界区时,它的生命周期就结束了。
确切地说,在那个时间点,std::lock_guard的析构函数被调用,是的,互斥体被释放了。过程是全自动的,此外,如果getVar()在sharedVariable = getVar()抛出异常时也会发生。当然,函数体范围或循环范围也限制了对象的生命周期。
std::unique_lock
std::unique_lock比它的小兄弟std::lock_guard更强大 。
它在lock_guard的基础上还能:
—— 没有关联互斥体时创建
—— 没有锁定的互斥体时创建
—— 显式和重复设置或释放关联互斥锁
—— 移动互斥体 move
—— 尝试锁定互斥体
—— 延迟锁定关联互斥体
但为什么需要这样做呢?
有些死锁的原因是互斥体被锁定在不同的序列中,就像上一篇文章举的例子一样。锁定在不同的顺序,需要能编辑下。
呐,这个例子:
// deadlock.cpp
#include <iostream>
#include <chrono>
#include <mutex>
#include <thread>
struct CriticalData{
std::mutex mut;
};
void deadLock(CriticalData& a, CriticalData& b){
a.mut.lock();
std::cout << "get the first mutex" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(1));
b.mut.lock();
std::cout << "get the second mutex" << std::endl;
// do something with a and b
a.mut.unlock();
b.mut.unlock();
}
int main(){
CriticalData c1;
CriticalData c2;
std::thread t1([&]{deadLock(c1,c2);});
std::thread t2([&]{deadLock(c2,c1);});
t1.join();
t2.join();
}
这个解决起来也很容易。
函数deadLock必须以原子方式锁定互斥体,这就是下面例子中干的事儿:
// deadlockResolved.cpp
#include <iostream>
#include <chrono>
#include <mutex>
#include <thread>
struct CriticalData{
std::mutex mut;
};
void deadLock(CriticalData& a, CriticalData& b){
std::unique_lock<std::mutex>guard1(a.mut,std::defer_lock);
std::cout << "Thread: " << std::this_thread::get_id() << " first mutex" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(1));
std::unique_lock<std::mutex>guard2(b.mut,std::defer_lock);
std::cout << " Thread: " << std::this_thread::get_id() << " second mutex" << std::endl;
std::cout << " Thread: " << std::this_thread::get_id() << " get both mutex" << std::endl;
std::lock(guard1,guard2);
// do something with a and b
}
int main(){
std::cout << std::endl;
CriticalData c1;
CriticalData c2;
std::thread t1([&]{deadLock(c1,c2);});
std::thread t2([&]{deadLock(c2,c1);});
t1.join();
t2.join();
std::cout << std::endl;
}
如果您用参数std::defer_lock调用std::unique_lock 的构造函数,锁不会自动锁定。
锁定操作是通过使用可变参数模板std::lock以原子方式执行锁定操作,具体就是std::lock(guard1,guard2);
这句代码。
可变模板是一个模板,它可以接受任意数量的参数。
这里,参数是guard1,guard2。std::lock试图在原子步骤中获得guard1和guard2。但是,它要么失败,或者得到了全部。
在这个例子中,std::unique_lock负责资源的生命周期,std::lock负责锁定相关的互斥锁。
但是,你也可以反过来做。在第一步中,锁定互斥体,在第二步中用std::unique_lock处理资源的生命周期。这里是第二种方法的示例:
std::lock(a.mut, b.mut);
std::lock_guard<std::mutex> guard1(a.mut, std::adopt_lock);
std::lock_guard<std::mutex> guard2(b.mut, std::adopt_lock);
现在一切都OK啦,程序运行就不会死锁啦。
注意:特殊死锁
认为只有互斥会产生死锁是一种错觉。每次线程在占用一个资源,并且还在等待一个资源时,死锁就潜伏在附近。
甚至线程也是一种资源。
// blockJoin.cpp
#include <iostream>
#include <mutex>
#include <thread>
std::mutex coutMutex;
int main(){
std::thread t([]{
std::cout << "Still waiting ..." << std::endl;
std::lock_guard<std::mutex> lockGuard(coutMutex);
std::cout << std::this_thread::get_id() << std::endl;
}
);
{
std::lock_guard<std::mutex> lockGuard(coutMutex);
std::cout << std::this_thread::get_id() << std::endl;
t.join();
}
}
程序立即静止不动了:
发生啥了?输出流std::cout和等待子线程t的主线程是死锁的原因。通过观察输出,您可以很容易地看到,语句将按哪个顺序执行。
首先,主线程执行打印id后,它使用调用t.join()来等待它的子线程执行完成。但是主线程在等待的同时,锁定了输出流。但这正是子线程等待的资源。。。。
解决这一死锁的方法有两种:
1、 主线程调用t.join后再锁定输出流
{
t.join();
std::lock_guard<std::mutex> lockGuard(coutMutex);
std::cout << std::this_thread::get_id() << std::endl;
}
2、 主线程通过一个额外的的区域释放它的锁,在t.join()调用之前完成:
{ { std::lock_guard<std::mutex> lockGuard(coutMutex); std::cout << std::this_thread::get_id() << std::endl; } t.join(); }