对 std::lock 的调用是不是可以传递已被调用线程锁定的资源?
Posted
技术标签:
【中文标题】对 std::lock 的调用是不是可以传递已被调用线程锁定的资源?【英文标题】: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
似乎是不可能的。
【问题讨论】:
【参考方案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)上,我遇到了死锁。但可能在其他编译器中,我可能不会。
参考资料:
-
Using more than one mutex with a conditional variable
Condition variable waiting on multiple mutexes
What is the best way to wait on multiple condition variables in C++11?
https://en.cppreference.com/w/cpp/thread/lock
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()。
所以,很明显它可能会释放在工作时获得的锁,但它没有说明在进入之前持有的锁是否可能在退出时被释放。
大概,只要任一
-
成功并且所有可锁定对象都被锁定,或者
它抛出,并且之前被该线程锁定的那些可锁定对象仍然是(没有之前未持有的锁持有,并且现在没有释放之前锁定的项目)
它不应该对里面发生的事情有任何影响。请注意,语言“...在每个参数上的一系列调用...”当然似乎允许在进入之前锁定的东西上调用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。以上是关于对 std::lock 的调用是不是可以传递已被调用线程锁定的资源?的主要内容,如果未能解决你的问题,请参考以下文章