线程安全堆栈 C++ 中的潜在死锁
Posted
技术标签:
【中文标题】线程安全堆栈 C++ 中的潜在死锁【英文标题】:Potential deadlock in thread-safe stack C++ 【发布时间】:2020-05-04 07:59:22 【问题描述】:在“Concurrency in Action”一书中,有一个线程安全堆栈的实现,在进入 pop() 和 empty() 函数时获取/锁定互斥锁,如下所示:
class threadsafe_stack
private:
std::stack<T> data;
mutable std::mutex m;
public:
//...
void pop(T& value)
std::lock_guard<std::mutex> lock(m);
if(data.empty()) throw empty_stack();
value = std::move(data.top());
data.pop();
bool empty() const
std::lock_guard<std::mutex> lock(m);
return data.empty();
;
我的问题是,当一个在进入 pop() 时获得锁的线程正在调用也受互斥锁保护的 empty() 时,这段代码如何不会陷入死锁?如果 lock() 由已经拥有互斥锁的线程调用,那不是未定义的行为吗?
【问题讨论】:
【参考方案1】:当一个在进入 pop() 时获得锁的线程正在调用也受互斥锁保护的 empty() 时,这段代码如何不会陷入死锁?
因为你没有调用threadsafe_stack
的成员函数empty
,而是调用了std::stack<T>
类的empty()。如果代码是:
void pop(T& value)
std::lock_guard<std::mutex> lock(m);
if(empty()) // instead of data.empty()
throw empty_stack();
value = std::move(data.top());
data.pop();
那么,就是undefined behavior:
如果锁由已经拥有互斥锁的线程调用,则行为未定义:例如,程序可能死锁。鼓励可以检测到无效使用的实现抛出带有错误条件 resource_deadlock_would_occur 的 std::system_error 而不是死锁。
了解recursive 和shared 互斥锁。
【讨论】:
【参考方案2】:不是 100% 确定你的意思,我猜你的意思是在同一个线程中依次调用 pop
和 empty
?就像在
while(!x.empty()) x.pop();
std::lock_guard
跟随 RAII。这意味着构造函数
std::lock_guard<std::mutex> lock(m);
将获取/锁定互斥体,而析构函数(当lock
超出范围时)将再次释放/解锁互斥体。所以它会在下一次函数调用时解锁。
在pop
内部只调用了data.empty()
,它不受互斥体的保护。在pop
内调用this->empty()
确实会导致未定义的行为。
【讨论】:
【参考方案3】:如果pop
会调用this->empty
,那你是正确的。通过std::lock_guard
两次锁定同一个互斥锁是未定义的行为,除非锁定的互斥锁是递归的。
来自构造函数上的cppreference(示例代码中使用的那个):
有效地调用 m.lock()。如果 m 不是递归互斥体并且当前线程已经拥有 m,则行为未定义。
为了完整起见,还有第二个构造函数:
lock_guard( mutex_type& m, std::adopt_lock_t t );
哪个
获取互斥体 m 的所有权而不尝试锁定它。如果当前线程不拥有 m,则行为未定义。
但是,pop
调用data.empty
,这是私有成员的方法,而不是threadsafe_stack
的成员函数empty
。代码没有问题。
【讨论】:
以上是关于线程安全堆栈 C++ 中的潜在死锁的主要内容,如果未能解决你的问题,请参考以下文章
C++多线程1.2-线程安全的保证——互斥量mutex(锁)和原子变量atomic
C++多线程1.2-线程安全的保证——互斥量mutex(锁)和原子变量atomic