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

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)只释放一级锁定,所以你可能仍然持有锁,因此死锁,因为触发谓词并执行通知的线程无法获取锁.

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

  • 另一种选择就是说"不要在具有互斥体的类中呼叫另一个公共成员",并依赖于坚持该规则的人.如果你不这样做,你很快就会陷入测试的僵局! (5认同)
  • 一种选择是提取完全不同步的实现类,因此具有互斥锁的类只有 2 个成员 - 互斥锁和实现类 - 以及只有公共成员函数,这些函数锁定互斥锁并转发到实现类。 (3认同)
  • @NoSenseEtAl:在这个例子中(记住是"琐碎的"和"设计的"),`fetch_count`函数没有标记为`const`,因此标记互斥锁`mutable`没有任何好处.如果是的话就会有. (2认同)
  • 正如@SteveJessop所说,如果可以从`const`成员函数中使用它,那么你的互斥锁上只需要`mutable`.另外:这里的想法**是"锁定每个公共方法的开头"---区别在于你不从另一个方法调用公共方法. (2认同)
  • “我很难想象需要递归互斥体的场景”。回调呢?想象一个类,该类保存指向向量中的对象的指针,需要对每个对象调用回调方法。如果该回调方法调用原始类的公共方法,该方法也使用相同的锁(例如向量上的 pop_back() ),那么这就是死锁。 (2认同)
  • @Vassilis 是的,这会陷入僵局,所以不要这样做。让回调调用一个私有成员函数,该函数执行公共成员函数的操作,假设互斥锁已被锁定(事实确实如此)。公共成员函数只能锁定互斥体并调用新的私有成员函数。 (2认同)

Ste*_*sop 25

应该std::recursive_mutex默认并std::mutex视为性能优化?

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

你有一个相当普遍的情况:

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

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

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

但是,有时使用递归互斥锁会很方便.这种设计仍然存在一个问题:在类不变量不存在的点上remove_two_nodes调用remove_one_node(第二次调用它时,列表恰好处于我们不希望暴露的状态).但是假设我们知道remove_one_node不依赖于那个不变量,这不是设计中的一个致命错误,只是我们已经使我们的规则比理想的更复杂"所有类不变量总是在任何公共函数是进入".

所以,这个技巧偶尔会有用,而且我并不憎恨递归的互斥量.我没有历史知识来证明它们包含在Posix中的原因与文章所说的"演示互斥属性和线程扩展"不同.不过,我当然不认为它们是默认值.

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

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

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

  • 漂亮的答案,但我不同意"的东西,从而从根本上为锁是否允许已持有或不重要." TBH几乎总是我不关心,如果锁已持有或不行.我只想要线程安全. (2认同)
  • @NoSenseEtAl:每个类的全局锁,由在该类的任何实例上调用的每个函数保存?不可扩展,但是,我想你会希望它是递归的.但是,如果你有两个班,这样的锁会发生什么,并且存在A类的函数调用B类的一些功能,并在另一个线程B类的另一个函数调用A类的一些功能?僵局.在回答答案的作者时,我们没有必要回答@答案,我相信在一个"如此踌躇满志的你想要扯掉它"UI设计模式的例子中,它会默默地删除它们. (2认同)
  • "从OO的角度来看,你不能对课外的东西做任何事情"你可以做些什么,你可以记录你的功能的前提条件.因此,您需要以足够的深度分析和/或设计代码,以确定调用者何时需要在调用函数之前获取两个锁.一旦你理解了那个深度的代码,你就可以(如果你选择的话)设计出递归锁,这就是非递归锁是设计决策而不是(仅)性能优化的原因. (2认同)

MB.*_*MB. 7

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

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

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