带有重复参数的 Scoped_lock

Fed*_*dor 5 c++ mutex deadlock locking

我使用 std::scoped_lock 来保护多线程环境中的对象对。但我发现 scoped_lock 如果两个参数相同,会导致死锁(在 Visual Studio 和 gcc 中)。例如,

#include <mutex>

struct S
{
    mutable std::mutex m;
    int v = 0;
    S & operator = ( const S & b )
    {
        std::scoped_lock l( m, b.m );
        v = b.v;
        return * this;
    }
};

int main()
{
    S a;
    a = a; //deadlock here!
}
Run Code Online (Sandbox Code Playgroud)

我看到标准要求“如果 MutexTypes 之一不是递归互斥体,并且当前线程已经在 ... 中拥有相应的参数,则行为未定义”,请参阅 https://en.cppreference.com/w/cpp/线程/scoped_lock/scoped_lock

但是在我的示例中,在形式上,互斥锁在 scoped_locked 之前没有锁定。那么这是预期的程序行为吗?

小智 1

你真正害怕的现象是“自我僵局”。当同时满足 2 个条件时,就会发生这种情况:

  • 一个线程在同一个互斥对象上重复调用mutex.lock() ,
  • 并且该互斥体属于非递归类型,不支持这种递归锁定。

在 C++ 中,有两种类型的互斥体:

  • std::mutex是非递归类型 - 它更快并且需要更少的资源,
  • std::recursive_mutex是递归类型 - 它更安全,但需要更多资源。

现在让我们将这些知识应用到您的具体示例中:

  1. 在您的情况下,避免“自死锁”的明显方法是避免“自分配”。为此,只需添加额外的检查:

     S & operator = ( const S & b ) 
     {
         if (this != &b) // check it is not self assignment
         {
             std::scoped_lock l( m, b.m );
             v = b.v;
         }
         return * this;
     }
    
    Run Code Online (Sandbox Code Playgroud)
  2. 如果这是互斥锁的唯一用法,则可以确定永远不会发生递归“自死锁”。因此,这是最好且最便宜的解决方案。

  3. 如果您要添加其他“同步”方法,该方法在调用线程已持有互斥体时调用当前方法,那么(并且仅在这种情况下)您确实需要将 std ::mutex类型替换为std::recursive_mutex