我有这门课(简化):
// thing.h
#include <mutex>
class Thing
{
public:
void process();
void inner();
private:
std::mutex lock;
};
// thing.cpp
#include "Thing.h"
using namespace std;
void Thing::process()
{
lock_guard<mutex> locking(lock);
inner();
}
void Thing::inner()
{
lock_guard<mutex> locking(lock);
}
如果我调用 process,我会得到一个异常:
Microsoft C++ exception: std::system_error at memory location 0x006FF16C.
在同一个线程中锁定同一个锁会导致此异常。我怎样才能做到这一点而没有例外?我想过添加一个标志:
volatile bool alreadyLocked;
将内部更改为:
void Thing::inner()
{
if (!alreadyLocked)
{
lock_guard<mutex> locking(lock);
alreadyLocked = true;
...something magic happens here...
alreadyLocked = false;
}
}
但是这感觉很脆弱......有正确的方法吗?
首先,
volatile
变量不是线程安全的。您必须使用 std::atomic<T>
才能拥有线程安全变量。 volatile
与线程安全无关。
std::recursive_mutex
,它可以从同一线程多次锁定/解锁。
来自 cppreference:
调用线程在成功调用
或recursive_mutex
时开始一段时间内拥有lock
。在此期间,线程可能会额外调用try_lock
或lock
。当线程进行匹配数量的解锁调用时,所有权期限结束。try_lock
当一个线程拥有
时,如果所有其他线程尝试声明recursive_mutex
的所有权,则所有其他线程都将阻塞(对于调用lock
)或收到错误的返回值(对于try_lock
)。recursive_mutex
此外,请考虑重构您的代码,以便不需要两次锁定互斥锁。改进您的设计可能可以避免这个问题。
有一个编码技巧可以解决这个设计问题;它称为递归互斥体。但你确实应该解决设计问题,而不是试图解决它。将代码分为两层:类中的所有工作都应该由不锁定任何内容的私有成员函数完成;外部接口应该通过公共成员函数来实现,并且它们锁定互斥锁。
所以:
class Thing {
public:
void process();
void inner();
private:
void do_process();
void do_inner();
std::mutex mtx;
};
void Thing::process() {
std::lock_guard<std::mutex> lock(mtx);
do_process();
}
void Thing::inner() {
std::lock_guard<std::mutex> lock(mtx);
do_inner();
}
void Thing::do_process() {
do_inner();
}
解决方案是使用
std::recursive_mutex
代替std::mutex
。
其他答案已经给出了正确的解决方案,但我想指出另外两点:
仅仅因为
volatile
不是为线程安全而设计的,并不意味着它与线程安全无关。在原来的帖子中,volatile bool alreadyLocked;
是正确且足够的。
这绝不是“代码味”!当系统变得复杂时,互斥锁的递归锁有时是不可避免的。如果这是“代码味道”或者可以通过更好的设计来解决,那么
std::recursive_mutex
将是一个笑话,应该从 C++ 标准库中转储出来。但它仍然存在,为什么?