std :: mutex vs std :: recursive_mutex作为类成员

时间:2013-01-24 10:19:52

标签: c++ c++11 mutex object-design recursive-mutex

我看到有些人讨厌recursive_mutex

http://www.zaval.org/resources/library/butenhof1.html

但是在考虑如何实现一个线程安全的类(互斥保护)时,我觉得很难证明每个应该受互斥保护的方法都是互斥保护的,并且互斥锁最多被锁定一次。

因此,对于面向对象的设计,std::recursive_mutex是默认的,std::mutex在一般情况下被视为性能优化,除非它仅在一个地方使用(仅保护一个资源)?

为了说清楚,我说的是一个私人非静态互斥体。因此每个类实例只有一个互斥锁。

在每个公共方法的开头:

{
    std::scoped_lock<std::recursive_mutex> sl;

3 个答案:

答案 0 :(得分:66)

大多数情况下,如果您认为需要递归互斥锁,那么您的设计是错误的,所以它绝对不应该是默认设置。

对于具有保护数据成员的单个互斥锁的类,则应在所有public成员函数中锁定互斥锁,并且所有private成员函数都应假定互斥锁已被锁定。

如果public成员函数需要调用另一个public成员函数,则将第二个成员函数拆分为两个:执行工作的private实现函数和{{1只锁定互斥锁并调用public的成员函数。然后,第一个成员函数也可以调用实现函数,而不必担心递归锁定。

e.g。

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; } }; 函数在两个公共成员函数之间共享,每个函数都锁定互斥锁。在单线程代码中,它可以内联到increment_data,而increase_count可以调用它,但我们不能在多线程代码中执行此操作。

这只是一个良好设计原则的应用:公共成员函数负责锁定互斥锁,并将执行工作的责任委托给私有成员函数。

这样做的好处是increase_count_and_return成员函数只需要在类处于一致状态时处理被调用:互斥锁被解锁,一旦被锁定,则所有不变量都成立。如果你彼此调用public个成员函数,那么他们必须处理互斥锁已被锁定的情况,并且不变量不一定保持不变。

这也意味着像条件变量等待的东西会起作用:如果你将递归互斥锁上的锁定传递给条件变量,那么(a)你需要使用public,因为std::condition_variable_any不会工作,以及(b)只释放一级锁定,因此你仍然可以保持锁定,从而死锁,因为触发谓词并执行通知的线程无法获取锁定。

我很难想到需要递归互斥的场景。

答案 1 :(得分:24)

  

std::recursive_mutex应该是默认值,std::mutex是否应被视为效果优化?

不是,不。使用非递归锁的优点是只是性能优化,这意味着你的代码是自我检查叶级原子操作真的是叶级别的,它们不会调用别的东西使用锁。

你有一个相当常见的情况:

  • 一个实现需要序列化的操作的函数,因此需要使用互斥量并执行此操作。
  • 另一个实现更大的序列化操作的函数,并希望调用第一个函数来执行它的一个步骤,同时它为更大的操作保持锁定。

为了一个具体的例子,也许第一个函数以原子方式从列表中删除一个节点,而第二个函数以原子方式从列表中删除两个节点(并且你永远不希望另一个线程看到只取出两个节点中的一个的列表。)

你没有需要递归互斥锁。例如,您可以将第一个函数重构为一个公共函数,该函数接受锁并调用一个“不安全”操作的私有函数。然后第二个函数可以调用相同的私有函数。

但是,有时使用递归互斥锁会很方便。这个设计仍然存在一个问题:remove_two_nodes在类不变量不存在的位置调用remove_one_node(第二次调用它时,列表恰好处于我们不想要的状态揭露)。但是假设我们知道remove_one_node不依赖于那个不变量,这不是设计中的一个致命错误,只是我们使得我们的规则比理想的“所有类不变量总是持有的要复杂得多”。无论何时输入任何公共功能“。

所以,这个技巧偶尔会有用,而且我并不讨厌递归的互斥锁,就像那篇文章那样。我没有历史知识来证明它们包含在Posix中的原因与文章所说的“展示互斥属性和线程扩展”不同。不过,我当然不认为它们是默认值。

我认为可以肯定地说,如果在您的设计中您不确定是否需要递归锁定,那么您的设计就不完整了。您稍后会后悔这样一个事实:您正在编写代码并且您不知道某些根本重要的事情,因为锁是否被允许已经被保留。所以不要放入递归锁“以防万一”。

如果您知道需要一个,请使用一个。如果您知道自己不需要,那么使用非递归锁不仅仅是一种优化,它有助于强制执行设计约束。对于第二个失败而言,它更有用,而不是它成功并隐瞒你不小心做了一些你的设计所说永远不会发生的事情的事实。但是如果你遵循你的设计,并且永远不会双重锁定互斥锁,那么你永远不会发现它是否是递归的,因此递归的互斥锁不会直接有害。

这个类比可能会失败,但这是另一种看待它的方式。想象一下,您可以在两种指针之间进行选择:一种是在取消引用空指针时使用堆栈跟踪中止程序,另一种是返回0(或者将其扩展为更多类型):就像指针一样指的是一个值初始化的对象)。非递归互斥体有点像中止,并且递归互斥体有点像返回0。它们都可能有它们的用途 - 人们有时会花一些时间来实现“安静的不是 - ”价值“。但是,如果您的代码设计为永远不会取消引用空指针,则您不希望默认情况下使用 静默允许这种情况发生的版本。

答案 2 :(得分:6)

我不会直接考虑互斥锁与recursive_mutex争论,但我认为分享recursive_mutex对设计绝对关键的场景会很好。

当使用Boost :: asio,Boost :: coroutine(可能还有NT Fibers之类的东西,虽然我对它们不太熟悉)时,即使没有重新设计的设计问题,你的互斥体也必须是递归的。

原因是因为其设计的基于协程的方法将暂停执行 in 例程,然后再恢复它。这意味着类的两个顶级方法可能“在同一个线程上同时被调用”而不进行任何子调用。

相关问题