NoS*_*tAl 43 c++ mutex object-design c++11 recursive-mutex
我见过有些人讨厌recursive_mutex:
http://www.zaval.org/resources/library/butenhof1.html
但是在考虑如何实现一个线程安全的类(互斥保护)时,我觉得很难证明每个应该受互斥保护的方法都是互斥保护的,并且互斥锁最多被锁定一次.
因此,对于面向对象的设计,应该std::recursive_mutex是默认的并且std::mutex在一般情况下被视为性能优化,除非它仅在一个地方使用(仅保护一个资源)?
为了说清楚,我说的是一个私人非静态互斥体.因此每个类实例只有一个互斥锁.
在每个公共方法的开头:
{
std::scoped_lock<std::recursive_mutex> sl;
Run Code Online (Sandbox Code Playgroud)
Ant*_*ams 70
大多数情况下,如果您认为需要递归互斥锁,那么您的设计是错误的,所以它绝对不应该是默认设置.
对于具有保护数据成员的单个互斥锁的类,则应在所有public成员函数中锁定互斥锁,并且所有private成员函数都应假定互斥锁已被锁定.
如果public成员函数需要调用另一个public成员函数,则将第二个成员函数拆分为两个:private执行函数的实现函数,以及public只锁定互斥锁并调用互斥锁的成员函数private.然后,第一个成员函数也可以调用实现函数,而不必担心递归锁定.
例如
class X {
std::mutex m;
int data;
int const max=50;
void increment_data() {
if (data >= max)
throw std::runtime_error("too big");
++data;
}
public:
X():data(0){}
int fetch_count() {
std::lock_guard<std::mutex> guard(m);
return data;
}
void increase_count() {
std::lock_guard<std::mutex> guard(m);
increment_data();
}
int increase_count_and_return() {
std::lock_guard<std::mutex> guard(m);
increment_data();
return data;
}
};
Run Code Online (Sandbox Code Playgroud)
这当然是一个简单的人为设计示例,但该increment_data函数在两个公共成员函数之间共享,每个函数都锁定互斥锁.在单线程代码中,它可以内联increase_count,并increase_count_and_return可以调用它,但我们不能在多线程代码中这样做.
这只是一个良好设计原则的应用:公共成员函数负责锁定互斥锁,并将执行工作的责任委托给私有成员函数.
这样做的好处是,public当类处于一致状态时,成员函数只需处理被调用:互斥锁被解锁,一旦被锁定,则所有不变量都成立.如果您相互调用public成员函数,那么他们必须处理互斥锁已被锁定的情况,并且不变量不一定成立.
它还意味着像条件变量等待的东西将起作用:如果你将递归互斥锁上的锁传递给条件变量,那么(a)你需要使用std::condition_variable_any因为std::condition_variable不起作用,并且(b)只释放一级锁定,所以你可能仍然持有锁,因此死锁,因为触发谓词并执行通知的线程无法获取锁.
我很难想到需要递归互斥的场景.
Ste*_*sop 25
应该
std::recursive_mutex默认并std::mutex视为性能优化?
不是,不是.使用非递归锁的优点是不只是一个性能优化,这意味着你的代码是自我检查叶级的原子操作真的是叶级,他们不叫别的东西,它使用的锁.
你有一个相当普遍的情况:
为了一个具体的例子,也许第一个函数从列表中原子地删除一个节点,而第二个函数原子地从列表中删除两个节点(并且你永远不希望另一个线程看到列表中只有两个节点中的一个出).
您不需要递归互斥锁.例如,您可以将第一个函数重构为一个公共函数,该函数接受锁并调用一个"不安全"操作的私有函数.然后第二个函数可以调用相同的私有函数.
但是,有时使用递归互斥锁会很方便.这种设计仍然存在一个问题:在类不变量不存在的点上remove_two_nodes调用remove_one_node(第二次调用它时,列表恰好处于我们不希望暴露的状态).但是假设我们知道remove_one_node不依赖于那个不变量,这不是设计中的一个致命错误,只是我们已经使我们的规则比理想的更复杂"所有类不变量总是在任何公共函数是进入".
所以,这个技巧偶尔会有用,而且我并不憎恨递归的互斥量.我没有历史知识来证明它们包含在Posix中的原因与文章所说的"演示互斥属性和线程扩展"不同.不过,我当然不认为它们是默认值.
我认为可以肯定地说,如果在你的设计中你不确定你是否需要递归锁定,那么你的设计是不完整的.您稍后会后悔这样一个事实,即您正在编写代码并且您不知道某些事情是如此重要,因为锁定是否已被允许.所以不要放入递归锁"以防万一".
如果您知道需要一个,请使用一个.如果您知道自己不需要,那么使用非递归锁不仅仅是一种优化,它有助于强制执行设计约束.对于第二个失败而言,它更有用,而不是它成功并隐瞒你不小心做了一些你的设计所说永远不会发生的事情的事实.但是如果你遵循你的设计,并且永远不会双重锁定互斥锁,那么你永远不会发现它是否是递归的,因此递归的互斥锁并不是直接有害的.
这个类比可能会失败,但这是另一种看待它的方式.想象一下,您可以选择两种指针:一种是在取消引用空指针时使用堆栈跟踪中止程序,另一种是返回0(或将其扩展为更多类型:表现为指针指向一个值 -初始化对象).非递归互斥体有点像中止,并且递归互斥体有点像返回0.它们都可能有它们的用途 - 人们有时会花一些时间来实现"安静的非 - "价值".但是,在您的代码被设计为永远不会取消引用空指针的情况下,您不希望默认使用静默允许这种情况发生的版本.
我不打算直接考虑互斥锁与recursive_mutex的争论,但我认为分享一个recursive_mutex对设计绝对关键的场景会很好.
当使用Boost :: asio,Boost :: coroutine(可能还有像NT Fibers这样的东西,虽然我对它们不太熟悉)时,即使没有重新设计的设计问题,你的互斥体也必须是递归的.
原因是因为基于协程的方法通过其设计将暂停在例程中执行然后随后恢复它.这意味着类的两个顶级方法可能"在同一个线程上同时被调用"而不进行任何子调用.
| 归档时间: |
|
| 查看次数: |
22036 次 |
| 最近记录: |