C++ std::mutex 与 std::recursive_mutex 作为类成员

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/14498892/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-27 18:23:09  来源:igfitidea点击:

std::mutex vs std::recursive_mutex as class member

c++c++11mutexobject-designrecursive-mutex

提问by NoSenseEtAl

I have seen some people hate on recursive_mutex:

我看到有些人讨厌recursive_mutex

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

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

But when thinking about how to implement a class that is thread safe (mutex protected), it seems to me excruciatingly hard to prove that every method that should be mutex protected is mutex protected and that mutex is locked at most once.

但是,在考虑如何实现线程安全(受互斥锁保护)的类时,在我看来,要证明每个应该受互斥锁保护的方法都是受互斥锁保护的,并且该互斥锁最多被锁定一次,这似乎非常困难。

So for object oriented design, should std::recursive_mutexbe default and std::mutexconsidered as an performance optimization in general case unless it is used only in one place (to protect only one resource)?

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

To make things clear, I'm talking about one private nonstatic mutex. So each class instance has only one mutex.

为了清楚起见,我说的是一个私有的非静态互斥锁。所以每个类实例只有一个互斥锁。

At the beginning of each public method:

在每个公共方法的开头:

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

回答by Anthony Williams

Most of the time, if you think you need a recursive mutex then your design is wrong, so it definitely should not be the default.

大多数时候,如果你认为你需要一个递归互斥锁,那么你的设计是错误的,所以它绝对不应该是默认的。

For a class with a single mutex protecting the data members, then the mutex should be locked in all the publicmember functions, and all the privatemember functions should assume the mutex is already locked.

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

If a publicmember function needs to call another publicmember function, then split the second one in two: a privateimplementation function that does the work, and a publicmember function that just locks the mutex and calls the privateone. The first member function can then also call the implementation function without having to worry about recursive locking.

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

e.g.

例如

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;
    } 
};

This is of course a trivial contrived example, but the increment_datafunction is shared between two public member functions, each of which locks the mutex. In single-threaded code, it could be inlined into increase_count, and increase_count_and_returncould call that, but we can't do that in multithreaded code.

这当然是一个简单的人为例子,但该increment_data函数在两个公共成员函数之间共享,每个函数都锁定互斥锁。在单线程代码中,它可以被内联到 中increase_count,并且increase_count_and_return可以调用它,但在多线程代码中我们不能这样做。

This is just an application of good design principles: the public member functions take responsibility for locking the mutex, and delegate the responsibility for doing the work to the private member function.

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

This has the benefit that the publicmember functions only have to deal with being called when the class is in a consistent state: the mutex is unlocked, and once it is locked then all invariants hold. If you call publicmember functions from each other then they have to handle the case that the mutex is already locked, and that the invariants don't necessarily hold.

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

It also means that things like condition variable waits will work: if you pass a lock on a recursive mutex to a condition variable then (a) you need to use std::condition_variable_anybecause std::condition_variablewon't work, and (b) only one level of lock is released, so you may still hold the lock, and thus deadlock because the thread that would trigger the predicate and do the notify cannot acquire the lock.

这也意味着条件变量等待之类的事情会起作用:如果您将递归互斥锁上的锁传递给条件变量,那么 (a) 您需要使用,std::condition_variable_any因为std::condition_variable不起作用,并且 (b) 仅释放一级锁,因此您可能仍然持有锁,从而导致死锁,因为将触发谓词并执行通知的线程无法获取锁。

I struggle to think of a scenario where a recursive mutex is required.

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

回答by Steve Jessop

should std::recursive_mutexbe default and std::mutexconsidered as an performance optimization?

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

Not really, no. The advantage of using non-recursive locks is notjust a performance optimization, it means that your code is self-checking that leaf-level atomic operations really are leaf-level, they aren't calling something else that uses the lock.

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

There's a reasonably common situation where you have:

有一种相当常见的情况,您有:

  • a function that implements some operation that needs to be serialized, so it takes the mutex and does it.
  • another function that implements a larger serialized operation, and wants to call the first function to do one step of it, while it is holding the lock for the larger operation.
  • 一个实现一些需要序列化的操作的函数,所以它需要互斥锁并执行它。
  • 另一个实现更大序列化操作的函数,并且想要调用第一个函数来执行其中的一个步骤,同时它持有更大操作的锁。

For the sake of a concrete example, perhaps the first function atomically removes a node from a list, while the second function atomically removes twonodes from a list (and you never want another thread to see the list with only one of the two nodes taken out).

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

You don't needrecursive mutexes for this. For example you could refactor the first function as a public function that takes the lock and calls a private function that does the operation "unsafely". The second function can then call the same private function.

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

However, sometimes it's convenient to use a recursive mutex instead. There's still an issue with this design: remove_two_nodescalls remove_one_nodeat a point where a class invariant doesn't hold (the second time it calls it, the list is in precisely the state we don't want to expose). But assuming we know that remove_one_nodedoesn't rely on that invariant this isn't a killer fault in the design, it's just that we've made our rules a little more complex than the ideal "all class invariants always hold whenever any public function is entered".

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

So, the trick is occasionally useful and I don't hate recursive mutexes to quite the extent that article does. I don't have the historical knowledge to argue that the reason for their inclusion in Posix is different from what the article says, "to demonstrate mutex attributes and thread extensons". I certainly don't consider them the default, though.

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

I think it's safe to say that if in your design you're uncertain whether you need a recursive lock or not, then your design is incomplete. You will later regret the fact that you're writing code and you don't knowsomething so fundamentally important as whether the lock is allowed to be already held or not. So don't put in a recursive lock "just in case".

我认为可以肯定地说,如果在您的设计中您不确定是否需要递归锁,那么您的设计是不完整的。稍后您会后悔您正在编写代码,而您不知道一些根本重要的事情,例如是否允许已经持有锁。所以不要“以防万一”放入递归锁。

If you know that you need one, use one. If you know that you don't need one, then using a non-recursive lock isn't just an optimization, it's helping to enforce a constraint of the design. It's more useful for the second lock to fail, than for it to succeed and conceal the fact that you've accidentally done something that your design says should never happen. But if you follow your design, and never double-lock the mutex, then you'll never find out whether it's recursive or not, and so a recursive mutex isn't directlyharmful.

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

This analogy might fail, but here's another way to look at it. Imagine you had a choice between two kinds of pointer: one that aborts the program with a stacktrace when you dereference a null pointer, and another one that returns 0(or to extend it to more types: behaves as if the pointer refers to a value-initialized object). A non-recursive mutex is a bit like the one that aborts, and a recursive mutex is a bit like the one that returns 0. They both potentially have their uses -- people sometimes go to some lengths to implement a "quiet not-a-value" value. But in the case where your code is designed to never dereference a null pointer, you don't want to use by defaultthe version that silently allows that to happen.

这个类比可能会失败,但这里有另一种看待它的方式。想象一下,您可以在两种指针之间进行选择:一种在取消引用空指针时使用堆栈跟踪中止程序,另一种返回0(或将其扩展为更多类型:表现得好像指针指向一个值 -初始化对象)。非递归互斥量有点像中止的互斥量,而递归互斥量有点像返回 0 的互斥量。它们都有自己的用途——人们有时会不遗余力地实现一个“安静的非-a” -值”值。但是,在您的代码设计为永远不会取消引用空指针的情况下,您不希望默认情况下使用允许这种情况发生的版本。

回答by MB.

I'm not going to directly weigh in on the mutex versus recursive_mutex debate, but I thought it would be good to share a scenario where recursive_mutex'es are absolutely critical to the design.

我不会直接权衡 mutex 与 recursive_mutex 的争论,但我认为最好分享一个场景,其中 recursive_mutex 对设计绝对至关重要。

When working with Boost::asio, Boost::coroutine (and probably things like NT Fibers although I'm less familiar with them), it is absolutely essential that your mutexes be recursive even without the design problem of re-entrancy.

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

The reason is because the coroutine based approach by its very design will suspend execution insidea routine and then subsequently resume it. This means that two top level methods of a class might "be being called at the same time on the same thread" without any sub calls being made.

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