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


183

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

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

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

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

Answers:


153

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

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

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

互斥锁中具有所有权感的另一个属性是支持优先级继承的能力。因为内核可以跟踪拥有互斥量的线程以及所有阻止程序的身份,所以在优先级线程系统中,可以将当前拥有互斥量的线程的优先级提升为最高优先级线程的优先级。当前在互斥对象上受阻。这种继承避免了在这种情况下可能发生的优先级倒置问题。(请注意,并非所有系统都支持此类互斥量的优先级继承,但这是通过所有权概念变为可能的另一个功能)。

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

  • 互斥锁 -支持递归,并可以选择优先级继承。该机制通常用于以连贯的方式保护数据的关键部分。
  • 二进制信号量 -没有递归,没有继承,简单的排除,接受者和给予者不必是同一线程,可以发布广播。该机制可用于保护关键部分,但对于线程间的连贯信令或同步也特别有用。
  • 计数信号量 -没有递归或继承,从任何所需的初始计数起连贯的资源计数器的作用,线程仅在资源的净计数为零的地方阻塞。

同样,这在平台上有所不同-尤其是它们所说的这些东西,但这应该代表概念和起作用的各种机制。


9
您对非递归互斥的解释听起来更像是一个信号量。互斥锁(无论是递归的还是非递归的)都有所有权的概念。
杰伊D

@JayD当人们争论诸如此类的事情时,这非常令人困惑。那么,定义这些事物的实体是谁?
Pacerier 2011年

13
@Pacerier相关标准。例如,对于posix(pthreads),此答案是错误的,在posix(pthreads)中,在锁定该线程的线程之外的线程中解锁普通互斥锁是未定义的行为,而在进行错误检查或递归互斥锁时执行此操作会导致可预测的错误代码。其他系统和标准的行为可能大不相同。
2012年

也许这很幼稚,但是我给人的印象是,互斥锁的中心思想是锁定线程将互斥锁解锁,然后其他线程也可以这样做。从computing.llnl.gov/tutorials/pthreads
user657862

2
@curiousguy-广播发布释放了信号量上阻塞的所有线程,而没有明确给予它(保持为空),而普通的二进制给定只会释放等待队列开头的线程(假设有一个阻塞)。
高杰夫

122

答案不是效率。非重入互斥锁可产生更好的代码。

示例:A :: foo()获取锁。然后,它调用B :: bar()。当您编写它时,它工作正常。但是有一段时间以后,有人将B :: bar()更改为调用A :: baz(),这也获得了锁。

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

使用互斥锁的正确思维模式:互斥锁可保护不变式。持有互斥量时,不变式可能会发生变化,但是在释放互斥量之前,将重新建立不变式。可重入锁很危险,因为第二次获得锁后,您将无法确定不变式是否正确。

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


4
我花了一段时间才得到您所说的关于当您第二次抓住锁时不变式无效的信息。好点子!如果它是一个读写锁(例如Java的ReadWriteLock),并且您获得了读取锁,然后又在同一线程中第二次重新获得了读取锁,该怎么办?获得读锁后,您不会使不变式无效吗?因此,当您获得第二个读锁时,不变性仍然为真。
dgrant

1
@Jonathan 这些天,Java在java.util.concurrent.locks中是否具有不可重入的锁
user454322 2012年

1
+1我猜想,可重入锁的最常见用法是在单个类内部,可以从受保护和不受保护的代码段中调用某些方法。实际上,可以始终将其排除在外。@ user454322当然可以Semaphore
maaartinus 2014年

1
请原谅我的误会,但我不知道这与互斥有何关系。假设不涉及多线程和锁定,A::foo()在调用之前可能仍然使对象处于不一致状态A::bar()。这种情况下的互斥与否与递归有什么关系?
任思远2015年

1
@SiyuanRen:问题是能够在本地对代码进行推理。人们(至少是我)受过训练,可以将锁定区域识别为不变的维持状态,也就是说,在您获取锁时,没有其他线程在修改状态,因此关键区域上的不变性成立。这不是一个硬规则,您可以在不考虑不变量的情况下进行编码,但这只会使您的代码难以推理和维护。在没有互斥体的单线程模式下,也会发生同样的情况,但是我们没有受过训练,无法在保护区域附近进行本地推理。
大卫·罗德里格斯(DavidRodríguez)-dribeas,2015年

92

如Dave Butenhof本人所写

“递归互斥锁最大的最大问题是它们鼓励您完全忘记锁定方案和作用域。这是致命的。邪恶的。它是“线程吞噬者”。您持有锁的时间绝对是最短的。句号,总是这样。如果您只是因为不知道某个被锁定的东西或不知道被呼叫者是否需要该互斥锁而正在呼叫某个持有锁定的东西,那么您所持有的互斥时间就太长了。将a弹枪对准您的应用程序并触发触发器。您大概已经开始使用线程来获取并发;但是您只是预防了并发。”


9
还要注意Butenhof的回复的最后一部分: ...you're not DONE until they're [recursive mutex] all gone.. Or sit back and let someone else do the design.
user454322

2
他还告诉您,使用单个全局递归互斥体(他的意见是只需要一个)是可以的,因为当您开始在多线程代码中使用它时,有意识地推迟了理解外部库不变性的艰苦工作。但是,您不应永远使用拐杖,而应最终花费时间来理解和修复代码的并发不变量。因此,我们可以解释为使用递归互斥是技术债务。
FooF,2015年

13

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

为什么确定使用互斥锁确实是正确的心理模型?我认为正确的模型可以保护数据,但不能保护不变性。

即使在单线程应用程序中也存在保护不变性的问题,而在多线程和互斥锁中却没有什么共同点。

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


真正。有更好的机制来保护不变式。
ActiveTrayPrntrTagDataStrDrvr 2014年

8
这应该是对提供该声明的答案的评论。互斥不仅保护数据,还保护不变式。尝试用原子(数据保护自身)而不是互斥体来编写一些简单的容器(最简单的是堆栈),您将理解该语句。
大卫·罗德里格斯(DavidRodríguez)-dribeas 2015年

互斥对象不保护数据,它们保护不变量。不过,该不变式可用于保护数据。
乔恩·汉纳

4

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


3

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

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

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


1

非递归互斥锁有什么用处?

当您必须在执行某些操作之前确保互斥锁已解锁时,它们绝对是不错的选择。这是因为pthread_mutex_unlock可以确保互斥锁仅在非递归时才被解锁。

pthread_mutex_t      g_mutex;

void foo()
{
    pthread_mutex_lock(&g_mutex);
    // Do something.
    pthread_mutex_unlock(&g_mutex);

    bar();
}

如果g_mutex是非递归的,则保证上面的代码bar()在互斥体已解锁的情况下调用。

因此,消除了万一bar()碰巧是未知的外部函数的情况下发生死锁的可能性,该外部函数很可能会执行某些操作,从而导致另一个线程尝试获取相同的互斥量。这种情况在基于线程池的应用程序和分布式应用程序中并不罕见,在这种情况下,进程间调用可能会产生一个新线程,而客户端程序员甚至没有意识到这一点。在所有此类情况下,最好仅在释放锁定后才调用所述外部函数。

如果g_mutex是递归的,则根本无法在拨打电话之前确保已将其解锁。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.