多个互斥锁策略以及为啥库不使用地址比较
Posted
技术标签:
【中文标题】多个互斥锁策略以及为啥库不使用地址比较【英文标题】:Multiple mutex locking strategies and why libraries don't use address comparison多个互斥锁策略以及为什么库不使用地址比较 【发布时间】:2012-04-06 12:37:29 【问题描述】:有一种广为人知的锁定多个锁的方法,它依赖于选择固定的线性顺序并根据该顺序获取锁。
例如,在"Acquire a lock on two mutexes and avoid deadlock" 的答案中提出了这一建议。尤其是基于地址比较的解决方案似乎相当优雅和明显。
当我尝试检查它的实际实现方式时,令我惊讶的是,我发现这种解决方案并未得到广泛使用。
引用Kernel Docs - Unreliable Guide To Locking:
教科书会告诉你,如果你总是按相同的顺序锁定,你 永远不会陷入这种僵局。实践会告诉你,这 方法无法扩展:当我创建一个新锁时,我不明白 足够的内核来确定它在 5000 个锁层次结构中的位置 会适合。
PThreads 似乎根本没有内置这样的机制。
Boost.Thread 想出了
完全不同的解决方案,lock()
用于多个(2 到 5 个)互斥锁是基于尝试和锁定尽可能多的互斥锁。
这是 Boost.Thread 源代码的片段(Boost 1.48.0,boost/thread/locks.hpp:1291):
template<typename MutexType1,typename MutexType2,typename MutexType3>
void lock(MutexType1& m1,MutexType2& m2,MutexType3& m3)
unsigned const lock_count=3;
unsigned lock_first=0;
for(;;)
switch(lock_first)
case 0:
lock_first=detail::lock_helper(m1,m2,m3);
if(!lock_first)
return;
break;
case 1:
lock_first=detail::lock_helper(m2,m3,m1);
if(!lock_first)
return;
lock_first=(lock_first+1)%lock_count;
break;
case 2:
lock_first=detail::lock_helper(m3,m1,m2);
if(!lock_first)
return;
lock_first=(lock_first+2)%lock_count;
break;
lock_helper
在成功时返回 0
,否则返回未成功锁定的互斥锁的数量。
为什么这个解决方案比比较地址或任何其他类型的 id 更好?我没有看到指针比较有任何问题,使用这种“盲”锁定可以避免。
关于如何在图书馆层面解决这个问题还有其他想法吗?
【问题讨论】:
我在这里发现了一个有趣的话题:groups.google.com/d/topic/comp.programming.threads/iyZ-0UcR7bw/… 真正的死锁是由于某个函数在很久以前获得了锁而导致的。这个方案没有提供任何保护。 【参考方案1】:有时,需要在锁 B 之前获取锁 A。锁 B 可能有较低或较高的地址,因此在这种情况下您不能使用地址比较。
示例:当您有一个树数据结构,并且线程尝试读取和更新节点时,您可以使用每个节点的读写锁来保护树。这仅在您的线程始终获取自上而下的 root-to-leave 锁定时才有效。在这种情况下,锁的地址无关紧要。
只有在首先获得哪个锁并不重要的情况下,您才能使用地址比较。如果是这种情况,地址比较是一个很好的解决方案。但如果不是这种情况,你就不能这样做。
我猜 Linux 内核需要先锁定某些子系统。这不能使用地址比较来完成。
【讨论】:
"这仅在您的线程始终获取自上而下 root-to-leave 的锁时才有效。"这不一定是真的,只要你等到你拥有所有必需的锁。您可以按地址顺序锁定所有这些锁,只要您同时锁定所有这些并且在您完成之前不要返回。 恕我直言不,因为如果不确保从根到节点的路径稳定,您甚至无法安全地访问这些锁。 这很好;如果您在锁定另一个锁之前无法知道锁的存在,那么您将无法使用它。【参考方案2】:地址比较失败的一种情况是您使用代理模式。 您可以将锁委托给同一个对象,地址将不同。
考虑以下示例
template<typename MutexType>
class MutexHelper
MutexHelper(MutexType &m) : _m(m)
void lock()
std::cout <<"locking ";
m.lock();
void unlock()
std::cout <<"unlocking ";
m.unlock();
MutexType &_m;
;
如果函数
template<typename MutexType1,typename MutexType2,typename MutexType3>
void lock(MutexType1& m1,MutexType2& m2,MutexType3& m3);
实际上会使用地址比较下面的代码会产生死锁
Mutex m1;
Mutex m1;
线程1
MutexHelper hm1(m1);
MutexHelper hm2(m2);
lock(hm1, hm2);
线程2:
MutexHelper hm2(m2);
MutexHelper hm1(m1);
lock(hm1, hm2);
编辑:
这是一个有趣的线程,分享了一些关于 boost::lock 实现的知识 thread-best-practice-to-lock-multiple-mutexes
地址比较不适用于进程间共享互斥体(命名同步对象)。
【讨论】:
好吧,这个问题是有原因的,如果你正在实现一个库,这个问题可能是一个有效的问题。例子可能很愚蠢,但想想像采用锁这样的情况。 另外,如果你真的想做这样的事情(你通常不这样做),你可以要求,每个 Lockable 对象都有一个返回互斥体 id 的方法(例如互斥体地址) .【参考方案3】:来自赏金文字:
我什至不确定我是否可以证明所提出的 Boost 解决方案的正确性,这似乎比线性顺序解决方案更棘手。
Boost 解决方案不能死锁,因为它在已经持有锁的情况下从不等待。除了第一个之外的所有锁都是用 try_lock 获取的。如果任何 try_lock 调用未能获取其锁,则释放所有先前获取的锁。此外,在 Boost 实现中,新的尝试将从上一次获取锁失败开始,并且会先等待直到它可用;这是一个明智的设计决策。
作为一般规则,最好避免在持有锁时阻塞调用。因此,如果可能,最好使用 try-lock 的解决方案(在我看来)。作为一个特殊的结果,在锁定排序的情况下,整个系统可能会卡住。想象一下最后一个锁(例如地址最大的锁)被一个线程获取,然后被阻塞。现在想象一些其他线程需要最后一个锁和另一个锁,并且由于排序,它将首先获得另一个并等待最后一个锁。所有其他锁也可能发生同样的情况,整个系统在最后一个锁被释放之前没有任何进展。当然,这是一种极端且不太可能发生的情况,但它说明了锁排序的内在问题:锁编号越高,获得锁时的间接影响就越大。
基于try-lock的解决方案的缺点是会导致活锁,极端情况下整个系统也可能会卡住至少一段时间。因此,重要的是要有一些退避模式,使锁定尝试之间的停顿随着时间的推移而变长,并且可能是随机的。
【讨论】:
"所有其他锁都可能发生同样的情况,整个系统在最后一个锁被释放之前没有任何进展。"由于“整个系统”现在都在等待该锁,因此该线程下一个运行的机会几乎是 1。除了严格优先级的系统会死锁。 @dascandy:如果该线程只是被抢占,我同意。但它也可能由于不同的原因而处于非活动状态,例如在 I/O 操作中,或等待解决页面错误。在某些情况下,例如涉及操作系统加载程序锁,甚至会导致死锁。 +1 :这里有一些测量值和图表来支持这个答案:htmlpreview.github.io/?https://github.com/HowardHinnant/papers/…【参考方案4】:“地址比较”和类似方法虽然经常使用,但属于特殊情况。如果你有,它们可以正常工作
-
获取无锁机制
同类或层级的两个(或更多)“项目”
这些项目之间的任何稳定排序模式
例如:您有一种机制可以从列表中获取两个“帐户”。假设对列表的访问是无锁的。现在您有了指向这两个项目的指针并想要锁定它们。由于它们是“兄弟姐妹”,因此您必须选择先锁定哪一个。这里使用地址(或任何其他稳定的排序模式,如“帐户 ID”)的方法是可以的。
但链接的 Linux 文本谈到了“锁定层次结构”。这意味着不是在“兄弟姐妹”(同类)之间锁定,而是在可能来自不同类型的“父母”和“孩子”之间锁定。这可能发生在实际的树结构中,也可能发生在其他场景中。 人为的示例:要加载程序,您必须
-
锁定文件inode,
锁定进程表
锁定目标内存
这三个锁不是没有明确层次结构的“兄弟”。锁也不是一个接一个地直接获取的——每个子系统都会随意获取锁。如果您考虑 所有 个用例,您会看到这三个(以及更多)子系统交互的用例,您无法想到清晰、稳定的顺序。
Boost 库处于相同的情况:它努力提供通用 解决方案。所以他们不能从上面假设要点,必须退回到更复杂的策略。
【讨论】:
以上是关于多个互斥锁策略以及为啥库不使用地址比较的主要内容,如果未能解决你的问题,请参考以下文章