多个互斥锁定策略以及库不使用地址比较的原因

时间:2012-03-21 22:49:21

标签: c++ multithreading mutex boost-thread

有一种众所周知的锁定多个锁的方法,它依赖于选择固定的线性排序和根据这种顺序获取锁。

例如,在"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?我没有看到指针比较有任何问题,使用这种“盲”锁定可以避免这种问题。

关于如何在库级别解决此问题还有其他想法吗?

4 个答案:

答案 0 :(得分:12)

来自赏金文字:

  

我甚至不确定我是否可以证明所提出的Boost解决方案的正确性,这似乎比具有线性顺序的解决方案更棘手。

Boost解决方案无法解锁,因为它在持有锁定时永远不会等待。除了第一个锁之外的所有锁都是通过try_lock获取的。如果任何try_lock调用无法获取其锁定,则释放所有先前获取的锁定。此外,在Boost实现中,新的尝试将从锁定失败以获取上一次开始,并将首先等待它可用;这是一个明智的设计决定。

作为一般规则,最好避免在持有锁定时阻止呼叫。因此,如果可能的话,使用try-lock的解决方案是首选(在我看来)。特别是在锁定顺序的情况下,整个系统可能会卡住。想象一下,最后一个锁定(例如地址最大的锁定)是由一个被阻塞的线程获得的。现在想象一些其他线程需要最后一个锁和另一个锁,并且由于订购它将首先获得另一个并且将等待最后一个锁。所有其他锁都会发生同样的情况,整个系统在释放最后一个锁之前不会有任何进展。当然这是一个极端而且不太可能的情况,但它说明了锁定顺序的固有问题:锁定数越高,锁定时获得的间接影响就越大。

基于try-lock的解决方案的缺点是它可能导致活锁,并且在极端情况下整个系统可能也会卡住至少一段时间。因此,重要的是要有一些退避模式,使锁定尝试之间的暂停时间更长,并且可能是随机的。

答案 1 :(得分:7)

有时,需要在锁定B之前获取锁定A.锁B可能有较低或较高的地址,因此在这种情况下你不能使用地址比较。

示例:当您拥有树数据结构,并且线程尝试读取和更新节点时,您可以使用每个节点的读写器锁来保护树。这仅在您的线程始终从上到下的root-to-leave获取锁时才有效。在这种情况下,锁的地址无关紧要。

如果首先获取哪个锁无关紧要,则只能使用地址比较。如果是这种情况,地址比较是一个很好的解决方案。但如果情况并非如此,你就无法做到。

我想Linux内核要求某些子系统在其他子系统之前被锁定。使用地址比较无法做到这一点。

答案 2 :(得分:1)

“地址比较”和类似方法虽然经常使用,但却是特殊情况。如果你有

,它们可以正常工作
  1. 获取无锁机制
  2. 同类层次结构的两个(或更多)“项目”
  3. 这些项目之间的任何稳定订购模式
  4. 例如:您有一种机制可以从列表中获取两个“帐户”。假设对列表的访问是无锁的。现在你有两个项目的指针,并希望锁定它们。由于他们是“兄弟姐妹”,你必须先选择锁定哪一个。这里使用地址(或任何其他稳定的排序模式,如“帐户ID”)的方法是可以的。

    但链接的Linux文本谈到了“锁定层次结构”。这意味着不要在“兄弟姐妹”(同类)之间锁定,而是在“父母”和“儿童”之间锁定,这可能来自不同类型。这可能发生在实际的树结构中以及其他场景中。 已解决的示例:要加载程序,您必须

    1. 锁定文件inode,
    2. 锁定流程表
    3. 锁定目标内存
    4. 这三个锁不是“兄弟姐妹”,不是一个明确的层次结构。锁也不是一个接一个地直接采取 - 每个子系统都会自由地取锁。如果你考虑所有用例,你会看到这三个(和更多)子系统相互作用,你可以想到没有清晰,稳定的排序。

      Boost库处于相同的情况:它致力于提供通用解决方案。所以他们不能从上面假设分数,必须回到更复杂的策略。

答案 3 :(得分:-1)

如果使用代理模式,则地址比较失败的一种情况。 您可以将锁委托给同一个对象,地址也不同。

考虑以下示例

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);

实际上会使用地址比较以下代码ca产生死锁

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

地址比较对进程间共享互斥锁(命名同步对象)不起作用。