递归锁(Mutex) vs 非递归锁(Mutex)

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

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

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

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

129506 次浏览

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

然而,这里还有其他的考虑因素。

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

互斥锁的另一个所有权属性是支持优先级继承的能力。因为内核可以跟踪拥有互斥锁的线程以及所有阻塞器的标识,所以在优先级线程系统中,可以将当前拥有互斥锁的线程的优先级提升到当前阻塞互斥锁的最高优先级线程的优先级。这种继承可以防止在这种情况下发生优先级倒置的问题。(注意,并不是所有的系统都支持这种互斥对象上的优先级继承,但这是通过所有权概念而成为可能的另一个特性)。

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

  • 互斥锁 -支持递归和可选的优先级继承。这种机制通常用于以一致的方式保护数据的关键部分。
  • 二进制信号量 -没有递归,没有继承,简单的排除,接受者和给予者不必是同一个线程,广播释放可用。此机制可用于保护临界区,但对于线程之间的一致信号或同步也特别有用。
  • 计数信号量 -没有递归或继承,作为来自任何所需初始计数的连贯资源计数器,线程只阻塞对资源的净计数为零的地方。

同样,这在不同的平台上有所不同——特别是他们如何称呼这些东西,但这应该是发挥作用的概念和各种机制的代表。

答案是效率。不可重入互斥会带来更好的代码。

例如:A::foo()获取锁。然后调用B::bar()。你写的时候效果很好。但过了一段时间,有人更改了B::bar()来调用A::baz(),这也获得了锁。

如果你没有递归互斥,这个就会死锁。如果你有它们,它会运行,但可能会坏掉。答::foo()可能在调用bar()之前让对象处于不一致的状态,假设baz()不能运行,因为它也获得了互斥量。但它可能不应该运行!编写A::foo()的人假设没有人可以同时调用A::baz()——这就是这两个方法获得锁的全部原因。

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

如果您喜欢可重入锁,那只是因为您以前没有调试过这样的问题。Java现在在Java .util.concurrent中有不可重入锁。顺便说一下,是锁。

正确的心智模式 互斥锁:互斥锁保护一个 不变的。< / p >

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

保护不变量的问题甚至出现在单线程应用程序中,而与多线程和互斥没有任何共同之处。

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

由Dave Butenhof亲自撰写:

递归互斥锁最大的问题是 它们鼓励您完全失去锁定方案的跟踪 范围。这是致命的。邪恶的。它是“食线者”。你握着锁 绝对最短的时间。时期。总是这样。如果你打电话 锁被锁住的东西只是因为你不知道它被锁住了,还是 因为你不知道被调用者是否需要互斥量,那么你 握得太久了。你把猎枪对准了你的应用程序 扣动扳机。您可能开始使用线程来获取 并发性;但你只是阻止了并发。" < / p >

递归互斥是有用的一个主要原因是在同一线程多次访问方法的情况下。例如,如果互斥锁是为了保护银行的a /c取款,那么如果取款也有相关的费用,那么必须使用相同的互斥锁。

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

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

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

非递归互斥对象有什么好处?

当你在做某事之前必须确保互斥量是解锁时,它们绝对是很好的。这是因为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是递归的,就会有没有办法来确保它在调用之前被解锁。

恕我冒犯,大多数反对递归锁的争论(在20年的并发编程中,我99.9%的时间都在使用递归锁)都把递归锁是好是坏与其他软件设计问题混合在一起,而这些问题是完全不相关的。举个例子,“回拨”;问题,这是详尽的阐述,没有任何多线程相关的观点,例如在书组件软件——超越面向对象编程

一旦发生控制反转(例如触发事件),就会面临重新进入的问题。与是否涉及互斥和线程无关。

class EvilFoo {
std::vector<std::string> data;
std::vector<std::function<void(EvilFoo&)> > changedEventHandlers;
public:
size_t registerChangedHandler( std::function<void(EvilFoo&)> handler) { // ...
}
void unregisterChangedHandler(size_t handlerId) { // ...
}
void fireChangedEvent() {
// bad bad, even evil idea!
for( auto& handler : changedEventHandlers ) {
handler(*this);
}
}
void AddItem(const std::string& item) {
data.push_back(item);
fireChangedEvent();
}
};
现在,使用像上面这样的代码,您将得到所有错误情况,这些错误情况通常会在递归锁上下文中命名-只是没有任何错误。事件处理程序一旦被调用,就可以注销自己,这将导致在天真编写的fireChangedEvent()中出现错误。或者它可以调用EvilFoo的其他成员函数,这会导致各种各样的问题。根本原因是再进入。 最糟糕的是,这甚至不可能是非常明显的,因为它可能在整个事件链上触发事件,最终我们回到了我们的EvilFoo(非本地) 所以,重入是根本问题,而不是递归锁。 现在,如果您觉得使用非递归锁更安全,那么这样的错误会如何表现呢?在死锁中发生意外的重新进入。 用递归锁呢?以同样的方式,它将在没有任何锁的代码中显示自己

因此,EvilFoo的邪恶部分是事件及其实现方式,而不是递归锁。首先,fireChangedEvent()需要创建一个changedEventHandlers的副本,并将其用于迭代。

经常出现在讨论中的另一个方面是锁首先应该做什么的定义:

  • 保护一段代码不被重新进入
  • 保护资源不被并发使用(被多个线程)。

在我进行并行编程的方式中,我对后者有一个心理模型(保护资源)。这就是我擅长使用递归锁的主要原因。如果某个(成员)函数需要锁定某个资源,它就会锁定。如果它在做它所做的事情时调用了另一个(成员)函数,并且该函数也需要锁定—它锁定。我不需要一个“替代方法”,因为递归锁的引用计数是完全相同的,就好像每个函数写的是这样的:

void EvilFoo::bar() {
auto_lock lock(this); // this->lock_holder = this->lock_if_not_already_locked_by_same_thread())
// do what we gotta do
   

// ~auto_lock() { if (lock_holder) unlock() }
}

一旦事件或类似的结构(访问者?!)开始发挥作用,我不希望通过一些非递归锁来解决所有随之而来的设计问题。