multithreading 递归锁 (Mutex) 与非递归锁 (Mutex)

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/187761/
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-09-10 00:59:08  来源:igfitidea点击:

Recursive Lock (Mutex) vs Non-Recursive Lock (Mutex)

multithreadinglockingmutexdeadlockrecursive-mutex

提问by Mecki

POSIX allows mutexes to be recursive. That means the same thread can lock the same mutex twice and won't deadlock. Of course it also needs to unlock it twice, otherwise no other thread can obtain the mutex. Not all systems supporting pthreads also support recursive mutexes, but if they want to be POSIX conform, they have to.

POSIX 允许递归互斥。这意味着同一个线程可以锁定同一个互斥锁两次并且不会死锁。当然也需要解锁两次,否则其他线程都无法获得互斥锁。并非所有支持 pthread 的系统都支持递归互斥锁,但如果它们想要符合 POSIX,则必须使用.

Other APIs (more high level APIs) also usually offer mutexes, often called Locks. Some systems/languages (e.g. Cocoa Objective-C) offer both, recursive and non recursive mutexes. Some languages also only offer one or the other one. E.g. in Java mutexes are always recursive (the same thread may twice "synchronize" on the same object). Depending on what other thread functionality they offer, not having recursive mutexes might be no problem, as they can easily be written yourself (I already implemented recursive mutexes myself on the basis of more simple mutex/condition operations).

其他 API(更高级的 API)通常也提供互斥锁,通常称为锁。一些系统/语言(例如 Cocoa Objective-C)同时提供递归和非递归互斥锁。有些语言也只提供一种或另一种。例如,在 Java 中互斥体总是递归的(同一个线程可以在同一个对象上两次“同步”)。根据它们提供的其他线程功能,没有递归互斥锁可能没有问题,因为它们可以很容易地自己编写(我已经在更简单的互斥锁/条件操作的基础上自己实现了递归互斥锁)。

What I don't really understand: What are non-recursive mutexes good for? Why would I want to have a thread deadlock if it locks the same mutex twice? Even high level languages that could avoid that (e.g. testing if this will deadlock and throwing an exception if it does) usually don't do that. They will let the thread deadlock instead.

我不太明白的是:非递归互斥锁有什么用?如果它两次锁定同一个互斥锁,为什么我会想要一个线程死锁?即使是可以避免这种情况的高级语言(例如测试这是否会死锁,如果会则抛出异常)通常也不会这样做。他们会让线程死锁。

Is this only for cases, where I accidentally lock it twice and only unlock it once and in case of a recursive mutex, it would be harder to find the problem, so instead I have it deadlock immediately to see where the incorrect lock appears? But couldn't I do the same with having a lock counter returned when unlocking and in a situation, where I'm sure I released the last lock and the counter is not zero, I can throw an exception or log the problem? Or is there any other, more useful use-case of non recursive mutexes that I fail to see? Or is it maybe just performance, as a non-recursive mutex can be slightly faster than a recursive one? However, I tested this and the difference is really not that big.

这是否仅适用于我不小心锁定两次并且只解锁一次的情况,并且在递归互斥锁的情况下,会更难找到问题,所以我立即死锁以查看错误锁出现的位置?但是我不能在解锁时返回一个锁计数器做同样的事情,在我确定我释放了最后一个锁并且计数器不为零的情况下,我可以抛出异常或记录问题吗?或者还有其他更有用的非递归互斥锁用例我没看到吗?或者它可能只是性能,因为非递归互斥锁可能比递归互斥锁稍快?但是,我对此进行了测试,差异确实没有那么大。

采纳答案by Tall Jeff

The difference between a recursive and non-recursive mutex has to do with ownership. In the case of a recursive mutex, the kernel has to keep track of the thread who actually obtained the mutex the first time around so that it can detect the difference between recursion vs. a different thread that should block instead. As another answer pointed out, there is a question of the additional overhead of this both in terms of memory to store this context and also the cycles required for maintaining it.

递归和非递归互斥锁之间的区别与所有权有关。在递归互斥锁的情况下,内核必须跟踪第一次实际获得互斥锁的线程,以便它可以检测递归与应该阻塞的不同线程之间的差异。正如另一个答案所指出的那样,在存储此上下文的内存以及维护它所需的周期方面存在额外开销的问题。

However, there are other considerations at play here too.

但是,这里还有其他考虑因素。

Because the recursive mutex has a sense of ownership, the thread that grabs the mutex must be the same thread that releases the mutex. In the case of non-recursive mutexes, there is no sense of ownership and any thread can usually release the mutex no matter which thread originally took the mutex. In many cases, this type of "mutex" is really more of a semaphore action, where you are not necessarily using the mutex as an exclusion device but use it as synchronization or signaling device between two or more threads.

因为递归互斥是有归属感的,所以抢到互斥的线程一定是释放互斥的同一个线程。在非递归互斥锁的情况下,没有归属感,任何线程通常都可以释放互斥锁,无论哪个线程最初占用了互斥锁。在许多情况下,这种类型的“互斥锁”实际上更像是一种信号量操作,您不必将互斥锁用作排除设备,而是将其用作两个或更多线程之间的同步或信号设备。

Another property that comes with a sense of ownership in a mutex is the ability to support priority inheritance. Because the kernel can track the thread owning the mutex and also the identity of all the blocker(s), in a priority threaded system it becomes possible to escalate the priority of the thread that currently owns the mutex to the priority of the highest priority thread that is currently blocking on the mutex. This inheritance prevents the problem of priority inversion that can occur in such cases. (Note that not all systems support priority inheritance on such mutexes, but it is another feature that becomes possible via the notion of ownership).

互斥锁中具有所有权的另一个属性是支持优先级继承的能力。因为内核可以跟踪拥有互斥锁的线程以及所有阻塞者的身份,所以在优先级线程系统中,可以将当前拥有互斥锁的线程的优先级提升到最高优先级线程的优先级目前在互斥锁上阻塞。这种继承可以防止在这种情况下可能发生的优先级反转问题。(请注意,并非所有系统都支持此类互斥锁的优先级继承,但这是通过所有权概念成为可能的另一个功能)。

If you refer to classic VxWorks RTOS kernel, they define three mechanisms:

如果您参考经典的 VxWorks RTOS 内核,它们定义了三种机制:

  • mutex- supports recursion, and optionally priority inheritance. This mechanism is commonly used to protect critical sections of data in a coherent manner.
  • binary semaphore- no recursion, no inheritance, simple exclusion, taker and giver does not have to be same thread, broadcast release available. This mechanism can be used to protect critical sections, but is also particularly useful for coherent signalling or synchronization between threads.
  • counting semaphore- no recursion or inheritance, acts as a coherent resource counter from any desired initial count, threads only block where net count against the resource is zero.
  • 互斥- 支持递归和可选的优先级继承。这种机制通常用于以一致的方式保护数据的关键部分。
  • 二元信号量- 没有递归,没有继承,简单的排除,接受者和给予者不必是同一个线程,广播发布可用。这种机制可用于保护临界区,但对于线程之间的连贯信号或同步也特别有用。
  • 计数信号量- 没有递归或继承,作为任何所需初始计数的一致资源计数器,线程仅在资源的净计数为零时阻塞。

Again, this varies somewhat by platform - especially what they call these things, but this should be representative of the concepts and various mechanisms at play.

同样,这因平台而有所不同 - 特别是他们称之为这些东西的东西,但这应该代表正在发挥作用的概念和各种机制。

回答by Jonathan

The answer is notefficiency. Non-reentrant mutexes lead to better code.

答案不是效率。不可重入互斥体导致更好的代码。

Example: A::foo() acquires the lock. It then calls B::bar(). This worked fine when you wrote it. But sometime later someone changes B::bar() to call A::baz(), which also acquires the lock.

示例: A::foo() 获取锁。然后它调用 B::bar()。当您编写它时,这很好用。但是稍后有人将 B::bar() 更改为调用 A::baz(),它也获取锁。

Well, if you don't have recursive mutexes, this deadlocks. If you do have them, it runs, but it may break. A::foo() may have left the object in an inconsistent state before calling bar(), on the assumption that baz() couldn't get run because it also acquires the mutex. But it probably shouldn't run! The person who wrote A::foo() assumed that nobody could call A::baz() at the same time - that's the entire reason that both of those methods acquired the lock.

好吧,如果您没有递归互斥锁,就会陷入僵局。如果你有它们,它会运行,但它可能会中断。A::foo() 可能在调用 bar() 之前使对象处于不一致状态,假设 baz() 无法运行,因为它也获取了互斥锁。但它可能不应该运行!编写 A::foo() 的人假设没有人可以同时调用 A::baz() - 这就是这两种方法都获得锁的全部原因。

The right mental model for using mutexes: The mutex protects an invariant. When the mutex is held, the invariant may change, but before releasing the mutex, the invariant is re-established. Reentrant locks are dangerous because the second time you acquire the lock you can't be sure the invariant is true any more.

使用互斥锁的正确思维模型:互斥锁保护不变量。当互斥量被持有时,不变量可能会改变,但在释放互斥量之前,不变量会重新建立。可重入锁是危险的,因为第二次获取锁时,您不能再确定不变式是否为真。

If you are happy with reentrant locks, it is only because you have not had to debug a problem like this before. Java has non-reentrant locks these days in java.util.concurrent.locks, by the way.

如果您对可重入锁感到满意,那只是因为您以前不必调试这样的问题。顺便说一下,现在 Java 在 java.util.concurrent.locks 中有不可重入锁。

回答by Chris Cleeland

As written by Dave Butenhof himself:

正如 Dave Butenhof 本人所写

"The biggest of all the big problems with recursive mutexes is that they encourage you to completely lose track of your locking scheme and scope. This is deadly. Evil. It's the "thread eater". You hold locks for the absolutely shortest possible time. Period. Always. If you're calling something with a lock held simply because you don't know it's held, or because you don't know whether the callee needs the mutex, then you're holding it too long. You're aiming a shotgun at your application and pulling the trigger. You presumably started using threads to get concurrency; but you've just PREVENTED concurrency."

“递归互斥锁的所有大问题中最大的一个是它们鼓励你完全忘记你的锁定方案和范围。这是致命的。邪恶的。它是“线程吞噬者”。你在绝对最短的时间内持有锁。期间。总是。如果你只是因为不知道它被持有,或者因为你不知道被调用者是否需要互斥锁而调用一个持有锁的东西,那么你持有它的时间太长了。你是“用霰弹枪瞄准你的应用程序并扣动扳机。你可能开始使用线程来获得并发性;但你只是阻止了并发性。”

回答by Chris Cleeland

The right mental model for using mutexes: The mutex protects an invariant.

使用互斥锁的正确思维模型:互斥锁保护不变量。

Why are you sure that this is really right mental model for using mutexes? I think right model is protecting data but not invariants.

为什么你确定这是使用互斥锁的正确思维模型?我认为正确的模型是保护数据而不是不变量。

The problem of protecting invariants presents even in single-threaded applications and has nothing common with multi-threading and mutexes.

即使在单线程应用程序中也存在保护不变量的问题,并且与多线程和互斥锁没有任何共同之处。

Furthermore, if you need to protect invariants, you still may use binary semaphore wich is never recursive.

此外,如果您需要保护不变量,您仍然可以使用从不递归的二进制信号量。

回答by avis

One main reason that recursive mutexes are useful is in case of accessing the methods multiple times by the same thread. For example, say if mutex lock is protecting a bank A/c to withdraw, then if there is a fee also associated with that withdrawal, then the same mutex has to be used.

递归互斥体有用的一个主要原因是在同一线程多次访问方法的情况下。例如,假设互斥锁正在保护银行 A/c 进行提款,那么如果该提款也有相关费用,则必须使用相同的互斥锁。

回答by DarkZeros

The only good use case for recursion mutex is when an object contains multiple methods. When any of the methods modify the content of the object, and therefore must lock the object before the state is consistent again.

递归互斥锁唯一好的用例是当一个对象包含多个方法时。当任何方法修改对象的内容时,因此必须在状态再次一致之前锁定对象。

If the methods use other methods (ie: addNewArray() calls addNewPoint(), and finalizes with recheckBounds()), but any of those functions by themselves need to lock the mutex, then recursive mutex is a win-win.

如果这些方法使用其他方法(即:addNewArray() 调用 addNewPoint(),并使用 recheckBounds() 完成),但这些函数中的任何一个本身都需要锁定互斥锁,那么递归互斥锁是双赢的。

For any other case (solving just bad coding, using it even in different objects) is clearly wrong!

对于任何其他情况(解决糟糕的编码,甚至在不同的对象中使用它)显然是错误的!