Linux 3.0:futex-lock死锁bug?

时间:2012-03-08 16:35:06

标签: c++ c linux concurrency futex

// SubFetch(x,y) = atomically x-=y and return x (__sync_sub_and_fetch)
// AddFetch(x,y) = atomically x+=y and return x (__sync_add_and_fetch)
// CompareWait(x, y) = futex(&x, FUTEX_WAIT, y) wait on x if x == y
// Wake(x, y) = futex(&x, FUTEX_WAKE, y) wake up y waiters

struct Lock
{
Lock() : x(1) {}

void lock()
{
    while (true)
    {
        if (SubFetch(x, 1) == 0)
            return;

        x = -1;

        CompareWait(x, -1);
    }
}

void unlock()
{
    if (AddFetch(x, 1) == 1)
        return;

    x = 1;

    Wake(x, 1);
}

private:
    int x;
};

Linux 3.0提供了一个名为futex的系统调用,许多并发实用程序都基于此调用,包括最近的pthread_mutex实现。无论何时编写代码,都应该考虑使用现有的实现或自己编写是否是项目的最佳选择。

上面是一个Lock(互斥锁,1个允许计数信号量)的实现,它基于futex和man futex(7)中的语义描述

它似乎包含一个死锁错误,因此在多个线程试图锁定并解锁它几千次之后,线程可以进入一个状态,其中x == -1并且所有线程都被卡在CompareWait中,但是没有人拿着锁。

任何人都可以看到错误的位置吗?

更新:我对futex(7)/语义如此破碎感到有些惊讶。我完全重写了Lock如下......现在这是正确的吗?

// CompareAssign(x,y,z) atomically: if (x == y) {x = z; ret true; } else ret false;

struct Lock
{
Lock() : x(0) {}

void lock()
{
    while (!CompareAssign(x, 0, 1))
        if (x == 2 || CompareAssign(x, 1, 2))
            CompareWait(x, 2);
}

void unlock()
{
    if (SubFetch(x, 1) == 0)
        return;

    x = 0;

    Wake(x, 1);
}

private:
int x;
};

这里的想法是x有以下三种状态:

0: unlocked
1: locked & no waiters
2: locked & waiters

3 个答案:

答案 0 :(得分:4)

问题是如果x无法获取锁定,则显式地-1分配给SubFetch。这与解锁比赛。

  1. 线程1获取锁定。 x==0
  2. 线程2尝试获取锁定。 SubFetchx设置为-1,然后线程2被暂停。
  3. 线程1释放锁定。 AddFetchx设置为0,因此代码会明确将x设置为1并调用Wake
  4. 线程2醒来并将x设置为-1,然后调用CompareWait
  5. 线程2现在处于等待状态,x设置为-1,但没有人将其唤醒,因为线程1已经释放了锁。

答案 1 :(得分:3)

在Ulrich Drepper的论文“Futexes are tricky”中描述了基于futex的Mutex的正确实现

http://people.redhat.com/drepper/futex.pdf

它不仅包括代码,还包含对其正确性的详细解释。论文的代码:

class mutex
{
 public:
 mutex () : val (0) { }
 void lock () {
   int c;
   if ((c = cmpxchg (val, 0, 1)) != 0)
     do {
       if (c == 2 || cmpxchg (val, 1, 2) != 0)
         futex_wait (&val, 2);
     } while ((c = cmpxchg (val, 0, 2)) != 0);
 }
 void unlock () {
//NOTE: atomic_dec returns the value BEFORE the operation, unlike your SubFetch !
   if (atomic_dec (val) != 1) {
     val = 0;
     futex_wake (&val, 1);
   }
 }
 private:
   int val;
};

将论文中的代码与您的代码进行比较,我发现了差异

你有

if(x == 2 || CompareAssign(x,1,2))

直接使用futex的值,而Drepper使用前一个CompareAssign()的返回值。这种差异可能只会影响绩效。

您的解锁代码也不同,但似乎在语义上等同。

无论如何,我强烈建议您遵循Drepper的代码。那篇论文经受住了时间的考验,并得到了很多同行评审。你自己滚动没什么收获。

答案 2 :(得分:0)

这个场景有三个线程,A,B和C.

此方案的初始状态为:

  • 持有锁的线程A
  • 线程B尚未争夺锁定
  • CompareWait()
  • 中的主题C.
  • x == -1从C无法获得锁定时
        A                  B                C
    ==============   ================  ===============
     AddFetch()
     (so x == 0)
                       SubFetch()
                       (so x == -1)

     x = 1

                       x = -1

     Wake()

此时B或C是否已取消阻止,当他们0时,他们将无法获得SubFetch()的结果。