(5条消息) C++锁的管理

前言

锁管理遵循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();}

原文地址:

http://www.modernescpp.com/index.php/prefer-locks-to-mutexes
(0)

相关推荐