第一次锁定和创建 lock_guard(adopt_lock) 和创建 unique_lock(defer_lock) 和锁定有啥区别?

Posted

技术标签:

【中文标题】第一次锁定和创建 lock_guard(adopt_lock) 和创建 unique_lock(defer_lock) 和锁定有啥区别?【英文标题】:What's the difference between first locking and creating a lock_guard(adopt_lock) and creating a unique_lock(defer_lock) and locking?第一次锁定和创建 lock_guard(adopt_lock) 和创建 unique_lock(defer_lock) 和锁定有什么区别? 【发布时间】:2014-11-23 13:25:50 【问题描述】:

我找到了以下两段代码:

    http://en.cppreference.com/w/cpp/thread/lock

    void assign_lunch_partner(Employee &e1, Employee &e2)                                                                                                  
       
        // use std::lock to acquire two locks without worrying about 
        // other calls to assign_lunch_partner deadlocking us
           
            // m is the std::mutex field
            std::unique_lock<std::mutex> lk1(e1.m, std::defer_lock);
            std::unique_lock<std::mutex> lk2(e2.m, std::defer_lock);
            std::lock(lk1, lk2);
            // ...
           
    
    

    http://www.amazon.com/C-Concurrency-Action-Practical-Multithreading/dp/1933988770

    void swap(X& lhs, X&rhs)                                                                                                                              
      if(&lhs == &rhs)
        return;
      // m is the std::mutex field
      std::lock(lhs.m, rhs.m);
      std::lock_guard<std::mutex> lock_a(lhs.m, std::adopt_lock);
      std::lock_guard<std::mutex> lock_b(rhs.m, std::adopt_lock);
      swap(lhs.some_detail, rhs.some_detail);
    
    

我想问一下使用两个版本中的任何一个有什么区别和后果? (先锁定还是先创建std::lock_guardstd::unique_lock?)

【问题讨论】:

【参考方案1】:

1) 第一个代码示例

   
    static std::mutex io_mutex;
    std::lock_guard<std::mutex> lk(io_mutex);
    std::cout << e1.id << " and " << e2.id << " are waiting for locks" << std::endl;
   

这是一个标准的锁守卫,当退出作用域时,锁lk被释放

   
    std::unique_lock<std::mutex> lk1(e1.m, std::defer_lock);
    std::unique_lock<std::mutex> lk2(e2.m, std::defer_lock);
    std::lock(lk1, lk2);
    std::cout << e1.id << " and " << e2.id << " got locks" << std::endl;
    // ...
 

这里我们首先创建锁而不获取它们(这就是std::defer_lock 的重点),然后,同时在两个锁上使用std::lock 确保它们被获取而不会出现死锁的风险,如果另一个函数调用者交错(如果您将其替换为对 std::lock 的两次连续调用,我们可能会出现死锁:

   
    std::unique_lock<std::mutex> lk1(e1.m, std::defer_lock);
    std::unique_lock<std::mutex> lk2(e2.m, std::defer_lock);
    std::lock(lk1);
    std::lock(lk2); // Risk of deadlock !
    std::cout << e1.id << " and " << e2.id << " got locks" << std::endl;
    // ...
 

2) 第二个代码示例

void swap(X& lhs, X&rhs)                                                                                                                              
  if(&lhs == &rhs)
    return;
  // m is the std::mutex field
  std::lock(lhs.m, rhs.m);
  std::lock_guard<std::mutex> lock_a(lhs.m, std::adopt_lock);
  std::lock_guard<std::mutex> lock_b(rhs.m, std::adopt_lock);
  swap(lhs.some_detail, rhs.some_detail);

现在,我们首先获取锁(仍然避免死锁),然后然后创建锁守卫以确保它们被正确释放。

注意std::adopt_lock 要求当前线程拥有互斥锁(这是因为我们刚刚锁定了它们)


结论

这里有两种模式:

1) 同时锁定两个互斥体,然后创建守卫

2) 创建守卫,然后同时锁定两个互斥锁

两种模式是等价的,目的是相同的:同时安全地锁定两个互斥体,并确保解锁总是发生在两者上。

至于std::lock_guardstd::unique_lock的区别,应该看this other SO post,大部分时候std::lock_guard就够了。

【讨论】:

从结论部分你会推荐哪一个?如果两者是等价的,那么应该有人认为这两种模式都存在。你能指出我们可以在哪些情况下使用一个而不是另一个?【参考方案2】:

book 中实际上有一段 (3.2.6) 解释了代码实际上是等效的,您可以用另一个替换一个。唯一的区别是std::unique_lock 往往占用更多空间并且比std::lock_guard 慢一点。

底线是当您不需要std::unique_lock 提供的额外灵活性时,请使用std::lock_guard

【讨论】:

【参考方案3】:

不同之处在于对未来变化的稳健性。在adopt_lock 版本中有一个窗口,其中互斥锁被锁定但不属于清理处理程序:

std::lock(lhs.m, rhs.m);
// <-- Bad news if someone adds junk here that can throw.
std::lock_guard<std::mutex> lock_a(lhs.m, std::adopt_lock);
std::lock_guard<std::mutex> lock_b(rhs.m, std::adopt_lock);

也有可能意外删除/省略其中一个保护声明而不会出现任何编译时错误。当死锁发生时,问题在运行时会很明显,但是将死锁追溯到其源头并不有趣。

defer_lock 版本不会遇到这些问题。由于保护对象是在锁定发生之前声明的,因此没有不安全的窗口。当然,如果您省略/删除其中一个保护声明,您将在 std::lock 调用时收到编译器错误。

【讨论】:

在 C++17 中,std::scoped_lock 可以更优雅地解决这两个问题。

以上是关于第一次锁定和创建 lock_guard(adopt_lock) 和创建 unique_lock(defer_lock) 和锁定有啥区别?的主要内容,如果未能解决你的问题,请参考以下文章

std::mutex 锁定函数和 std::lock_guard<std::mutex> 的区别?

将 std::lock_guard 与 try_lock 一起使用

unique_lock 类模板

C++11多线程 互斥量的概念用法死锁演示及解决详解

二.共享数据的保护

std::mutex 锁定的顺序