【问题标题】:May a call to std::lock pass a resource that is already locked by the calling thread?对 std::lock 的调用是否可以传递已被调用线程锁定的资源?
【发布时间】:2017-12-04 16:29:57
【问题描述】:

我正在研究std::lock 函数的可能实现,偶然发现了an implementation posted on the code review community

引用已接受的答案(强调我的):

不,这不符合 std::lock() 的定义

它 (std::lock) 保证无论您指定什么顺序 锁定参数列表不会陷入死锁 情况。

[...]

这也意味着如果列表中的一个锁已经被锁定,它必须 被释放,以便以正确的顺序获取锁。

无论最后一个陈述是否正确,我都找不到一个确定的答案。

我的问题:是否允许(即定义的行为)将调用线程拥有的锁定资源作为参数传递给标准 std::lock 函数?

std::mutex m1, m2;
m1.lock();
std::lock(m1, m2);

我的直觉告诉我这实际上是不允许的。该函数需要两个或更多 Lockable 对象,并且无法检查 Lockable 对象是否已被当前执行线程锁定。所以用这种方式实现std::lock 似乎是不可能的。

【问题讨论】:

    标签: c++ multithreading


    【解决方案1】:

    是否允许(即定义的行为)传递锁定的资源?

    没有。它不是。我刚刚对此进行了测试,但遇到了僵局。传递给std::lock 的两个互斥锁都必须被释放。如果它们是递归互斥锁,那么它们可能已经被当前线程锁定。否则,如果它们被不同的线程锁定,就会出现死锁。

    如果您知道互斥锁何时被锁定,则可以使用自定义可锁定对象仅锁定其中一个互斥锁,例如:

    class CustomDualLock {
        bool first_time;
        std::mutex& _mutex1;
        std::mutex& _mutex2;
    
    public:
        CustomDualLock(std::mutex& mutex1, std::mutex& mutex2)
            : first_time(true),
            _mutex1(mutex1), 
            _mutex2(mutex2) {
            lock();
        }
    
        ~CustomDualLock() {
            unlock();
        }
    
        CustomDualLock(const CustomDualLock&) = delete;
        CustomDualLock& operator =(const CustomDualLock&) = delete;
        CustomDualLock(const CustomDualLock&&) = delete;
        CustomDualLock& operator =(const CustomDualLock&&) = delete;
    
        void lock() {
            if( first_time ) {
                first_time = false;
                _mutex1.lock();
            } 
            else {
                std::lock(_mutex1, _mutex2);
            }
        }
    
        void unlock() {
            _mutex1.unlock(); 
            _mutex2.unlock();
        }
    };
    

    更新

    我刚刚发现一些关于行为的更清楚的东西(它谈论的是作用域锁 (C++ 17),当超过 1 个锁被传递时,它会调用 std::lock):https://en.cppreference.com/w/cpp/thread/scoped_lock/scoped_lock

    如果 MutexTypes 之一不是递归互斥体并且当前线程已经拥有 m 中的相应参数,则行为未定义...

    正如它所说,行为是未定义的。至少在我的编译器(GCC 8.4)上,我遇到了死锁。但可能在其他编译器中,我可能不会。

    参考资料:

    1. Using more than one mutex with a conditional variable
    2. Condition variable waiting on multiple mutexes
    3. What is the best way to wait on multiple condition variables in C++11?
    4. https://en.cppreference.com/w/cpp/thread/lock
    5. https://en.cppreference.com/w/cpp/thread/condition_variable_any

    【讨论】:

      【解决方案2】:

      我的本​​地标准草案在 30.4.3/5 中提到 lock

      效果:所有参数都通过一系列对 lock()、try_lock() 或 unlock() 的调用被锁定 争论。调用顺序不应导致死锁,但未指定。 [注:A 必须使用try-and-back-off等死锁避免算法,但具体算法不是 指定以避免过度约束的实现。 — 尾注] 如果调用 lock() 或 try_lock() 抛出异常,对于任何已被调用锁定的参数都应调用 unlock() lock() 或 try_lock()。

      所以,很明显它可能会释放在工作时获得的锁,但它没有说明在进入之前持有的锁是否可能在退出时被释放。

      大概,只要任一

      1. 成功并且所有可锁定对象都被锁定,或者
      2. 它抛出,并且之前被该线程锁定的那些可锁定对象仍然是(没有之前未持有的锁持有,并且现在没有释放之前锁定的项目)

      不应该对里面发生的事情有任何影响。请注意,语言“...在每个参数上的一系列调用...”当然似乎允许在进入之前锁定的东西上调用unlock

      【讨论】:

      • 因此,必须在任何 Lockable 对象上工作的通用实现必须假定它们都没有被调用线程锁定。我可以传递一个互斥锁,它是一个可锁定对象,并且尝试锁定当前线程持有的互斥锁是未定义的行为。但是可以为可公开其状态的可锁定对象提供专门化,例如对于std::unique_lock,它具有owns_lock 方法,利用这些额外信息。我想这不是太奇怪了!
      【解决方案3】:

      我猜你问你关于第二个 std::lock 在互斥锁上的线程中的问题,该互斥锁之前已经锁定在同一个线程中。如果一个已经锁定的资源是 recursive_mutex,它是允许的。如果是通用互斥体,就会陷入死锁。

      【讨论】:

      • 您的回答让我明白,我必须更详细地表达我的问题。但是您推断出了我要问的内容:该场景确实是锁定了一个互斥体,然后将其作为参数传递给std::lock。我更新了我的问题并添加了一个示例 sn-p。
      • 您引用了标准中关于 std::lock 的部分,其中提到了 std::try_lock。为什么您不继续阅读有关 std::try_lock 的标准?标准说 std::try_lock 尝试锁定互斥锁,不阻塞并且在最后(对不起,我无法在移动网络版本中输入新行): - 如果互斥锁当前被调用此函数的同一线程锁定,它会产生死锁(具有未定义的行为)。有关允许来自同一线程的多个锁的互斥类型,请参见 recursive_mutex。
      猜你喜欢
      • 2014-09-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多