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

12 浏览
0 Comments

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

POSIX 允许互斥锁成为递归的。这意味着同一个线程可以两次锁定同一个互斥锁而不会死锁。当然,它还需要两次解锁,否则其他线程无法获取该互斥锁。并非所有支持pthread的系统也支持递归互斥锁,但如果它们想遵循 POSIX,就要支持。

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

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

这只是针对我不小心两次锁定它并仅解锁一次的情况吗?在递归互斥锁的情况下,它会更难找到问题,因此我让它立即死锁以查看出现错误锁的位置?但是,我能否通过在解锁时返回锁计数器,在我确定已释放最后一个锁且计数器不为零的情况下,抛出异常或记录问题?或者还有其他更有用的非递归互斥锁用例,我未能看到?或者仅仅是性能问题,因为非递归互斥锁可能会稍微更快?但是,我已经测试了这一点,差异真的不是很大。

admin 更改状态以发布 2023年5月25日
0
0 Comments

答案不是效率。 非可重入的互斥锁会导致更好的代码。\n\n例如:A::foo()获取锁。 然后调用B::bar()。 当你写它时,这很好用。但是过了一段时间,有人将B::bar()更改为调用A::baz(),后者也获取锁。\n\n如果您没有递归锁,那么这将死锁。如果您有它们,它会运行,但可能会出错。 A::foo()在调用bar()之前可能会将对象留在不一致的状态下,假设baz()无法运行,因为它也获取互斥锁。 但它可能不应该运行!编写A::foo()的人假设没有人可以同时调用A::baz() - 这就是这两种方法都获取锁的原因。\n\n使用互斥锁的正确心理模型是:互斥锁保护不变量。当互斥锁持有时,不变量可能会更改,但在释放互斥锁之前,不变量会被重新建立。可重入锁很危险,因为第二次获取锁时,您无法确定不变量是否仍为真实的。\n\n如果您喜欢可重入锁,那只是因为您之前没有调试过这样的问题。顺便说一下,Java现在在java.util.concurrent.locks中具有非可重入锁。

0
0 Comments

递归和非递归互斥锁之间的区别在于所有权。对于递归互斥锁,内核必须跟踪实际获取互斥锁的线程,以便能够区分递归和应该阻塞的另一个线程。正如另一个答案指出的那样,这会增加额外的开销,既包括存储该上下文所需的内存,也包括维护它所需的周期。\n\n然而,在这里还有其他考虑因素。\n\n因为递归互斥锁具有所有权的意识,所以抓住互斥锁的线程必须是释放互斥锁的同一线程。对于非递归互斥锁,没有所有权的概念,任何线程通常都可以释放互斥锁,而不管最初采取互斥锁的线程是哪个。在许多情况下,“互斥锁”这种类型的机制实际上更像是一个信号量操作,其中您不一定使用互斥锁作为互斥设备,而是将其用作两个或多个线程之间的同步或信号设备。\n\n互斥锁中所有权的另一个属性是支持优先级继承的能力。因为内核可以跟踪拥有互斥锁的线程以及所有阻止器的身份,在优先级线程系统中,当前拥有互斥锁的线程的优先级可以提升到当前在互斥锁上阻塞的最高优先级线程的优先级。此继承可以防止在这种情况下可能发生的优先级反转问题。 (请注意,并非所有系统都支持此类互斥锁上的优先级继承,但这是通过所有权概念变得可能的另一项功能)。\n\n如果您参考经典的VxWorks RTOS内核,它们定义了三种机制:\n- 互斥锁 - 支持递归,还可以选择支持优先级继承。此机制通常用于以一致的方式保护关键数据的临界区。\n- 二元信号量 - 没有递归,没有继承,简单排除,获取者和给予者不必是同一线程,广播释放可用。此机制可用于保护关键部分,但也特别适用于线程之间的一致信号或同步。\n- 计数信号量 - 没有递归或继承,作为任何所需初始计数的一致资源计数器,线程仅在资源的净计数为零时才会阻塞。\n\n再次强调,这在平台上有所不同-特别是它们如何称呼这些东西,但这应该代表了正在发挥作用的概念和各种机制。

0