递归锁定(互斥锁)与非递归锁定(互斥锁)

Mec*_*cki 176 multithreading mutex deadlock locking recursive-mutex

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

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

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

这只适用于我意外锁定它两次并且只解锁一次的情况,并且在递归互斥锁的情况下,它会更难找到问题,所以相反我立即死锁以查看错误锁定出现在哪里?但是我不能在解锁时返回一个锁定计数器并且在某种情况下,我确定我释放了最后一个锁并且计数器不为零,我可以抛出异常或记录问题吗?或者是否有其他更有用的非递归互斥体用例我看不到?或者它可能只是性能,因为非递归互斥体可能比递归互斥体略快?但是,我对此进行了测试,差异确实不大.

Tal*_*eff 147

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

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

因为递归互斥锁具有所有权感,所以抓取互斥锁的线程必须与释放互斥锁的线程相同.在非递归互斥锁的情况下,没有所有权感,任何线程通常都可以释放互斥锁,无论哪个线程最初使用互斥锁.在许多情况下,这种类型的"互斥体"实际上更像是一种信号量动作,您不必将互斥体用作排除设备,而是将其用作两个或多个线程之间的同步或信号设备.

另一个在互斥体中具有所有权感的属性是支持优先级继承的能力.因为内核可以跟踪拥有互斥锁的线程以及所有阻塞程序的标识,所以在优先级线程系统中,可以将当前拥有互斥锁的线程的优先级升级到最高优先级线程的优先级目前阻止互斥锁.这种继承防止了在这种情况下可能发生的优先级倒置问题.(请注意,并非所有系统都支持此类互斥锁上的优先级继承,但它是通过所有权概念实现的另一个功能).

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

  • mutex - 支持递归和可选的优先级继承
  • 二进制信号量 - 没有递归,没有继承,简单排除,接受者和给予者不必是相同的线程,广播发布可用
  • 计数信号量 - 没有递归或继承,充当来自任何所需初始计数的连贯资源计数器,线程仅阻止对资源的净计数为零.

同样,这在某种程度上因平台而异 - 特别是他们称之为的东西,但这应该代表了概念和各种机制.

  • @Pacerier相关标准.这个答案是错误的,例如对POSIX(并行线程),其中在除锁定它是不确定的行为的线程以外的线程解锁互斥量正常,同时做一个错误检查或在可预见的错误代码递归互斥结果相同.其他系统和标准可能表现得非常不同. (9认同)
  • 你对非递归互斥体的解释听起来更像是一个信号量.互斥(无论是递归还是非递归)都具有所有权概念. (8认同)
  • 那真是敬畏 - 真的有些......谢谢 (2认同)
  • @curiousguy - 广播释放会释放信号量上阻塞的所有线程,而无需显式给出信号量(保持为空),而普通的二进制给出只会释放等待队列头部的线程(假设有一个线程被阻塞)。 (2认同)

Jon*_*han 119

答案不是效率.不可重入的互斥体可以带来更好的代码.

示例:A :: foo()获取锁定.然后调用B :: bar().当你写它时,这很好用.但有时候有人改变B :: bar()来调用A :: baz(),它也获得了锁.

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

使用互斥锁的正确心理模型:互斥锁保护不变量.保持互斥锁时,不变量可能会发生变化,但在释放互斥锁之前,会重新建立不变量.重入锁定是危险的,因为第二次获得锁定时,您无法确定不变量是否为真.

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

  • 我花了一段时间才得到你所说的关于当你第二次抓住锁时不变的无效性.好点子!如果它是一个读写锁(如Java的ReadWriteLock),你获得了读锁,然后在同一个线程中第二次重新获取读锁.获得读锁后,你不会使一个不变量无效吗?所以当你获得第二个读锁时,不变量仍然是真的. (4认同)

Chr*_*and 88

正如Dave Butenhof本人所写:

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

  • 另请注意Butenhof回应的最后一部分:`...你不会完成,直到它们[递归互斥]都消失了......或者坐下来让别人去做设计. (9认同)
  • 他还告诉使用一个单一的全球递归互斥体(他的观点是,你只需要一个)是好的,作为拐杖自觉推迟了解外部库的不变性的辛勤工作,当你开始在多线程代码中使用它.但是你不应该永远使用拐杖,但最终会投入时间来理解和修复代码的并发不变量.所以我们可以解释一下,使用递归互斥是技术债务. (2认同)

小智 14

使用互斥锁的正确心理模型:互斥锁保护不变量.

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

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

此外,如果您需要保护不变量,您仍然可以使用二进制信号量,它永远不会递归.

  • 这应该是对提供该陈述的答案的评论.互斥体不仅可以保护数据,还可以保护不变量.尝试用原子(数据保护自己)而不是互斥体来编写一些简单的容器(最简单的是堆栈),你就会理解这个语句. (7认同)

小智 6

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


Dar*_*ros 5

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

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

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