我正在努力调试我的Linux-futex& amp;中的竞争条件导致死锁。基于原子操作的锁定原语。这是我正在使用的代码(与真实代码完全相同的逻辑,只是拉出了与问题无关的数据结构的依赖性):
int readers, writer, waiting;
void wrlock()
{
int cur;
while (atomic_swap(&writer, 1)) spin();
while ((cur=readers)) futex_wait(&readers, cur);
}
void wrunlock()
{
atomic_store(&writer, 0);
if (waiting) futex_wake(&writer, ALL);
}
void rdlock()
{
atomic_inc(&waiting);
for (;;) {
atomic_inc(&readers);
if (!writer) return;
atomic_dec(&readers);
futex_wait(&writer, 1);
}
}
void rdunlock()
{
atomic_dec(&waiting);
atomic_dec(&readers);
if (writer) futex_wake(&readers, ALL);
}
atomic_*
和spin
功能非常明显。 Linux futex操作是futex_wait(int *mem, int val)
和futex_wake(int *mem, int how_many_to_wake)
。
我遇到的死锁情况是3个线程,readers==0
,writer==1
,waiting==2
以及等待futex_wait
的所有线程。我不明白这是怎么发生的。
对于每个想要因为不使用pthread原语而对我大喊大叫的人,请将其保存为另一个问题。这是低级代码,它不依赖于glibc / libpthread而运行。在任何情况下,我认为这个问题对于其他人来说可能对学习低级并发黑魔法很有用,或者可能会让其他人远离试图编写这样的代码......; - )
顺便说一句,硬件是x86,所以即使代码存在内存排序问题,我也不认为它们会表现为错误。我猜测只是对我错过的futex的一种微妙的误用,特别是因为当所有的等待都被作为旋转时,代码工作正常。
这是为wrlock
生成的asm(基本上与我发布的版本相同,除了它为第一个spinlock调用一个单独的函数lock
)。开头的附加条件返回是“如果我们没有运行多个线程,则纾困”。 0x338
和0x33c
对应readers
和writer
。 call 1af
实际上是一个重新定位来调用外部的futex_wait
。
00000184 <wrlock>:
184: a1 18 00 00 00 mov 0x18,%eax
189: 55 push %ebp
18a: 85 c0 test %eax,%eax
18c: 89 e5 mov %esp,%ebp
18e: 75 02 jne 192 <wrlock+0xe>
190: c9 leave
191: c3 ret
192: 68 3c 03 00 00 push $0x33c
197: e8 7e fe ff ff call 1a <lock>
19c: 58 pop %eax
19d: a1 38 03 00 00 mov 0x338,%eax
1a2: 85 c0 test %eax,%eax
1a4: 74 ea je 190 <wrlock+0xc>
1a6: 6a 01 push $0x1
1a8: 50 push %eax
1a9: 68 38 03 00 00 push $0x338
1ae: e8 fc ff ff ff call 1af <wrlock+0x2b>
1b3: a1 38 03 00 00 mov 0x338,%eax
1b8: 83 c4 0c add $0xc,%esp
1bb: 85 c0 test %eax,%eax
1bd: 75 e7 jne 1a6 <wrlock+0x22>
1bf: eb cf jmp 190 <wrlock+0xc>
答案 0 :(得分:5)
我认为这说明了你潜在的僵局。假设一个处理器按以下顺序执行3个线程:
// to start,
// readers == 0, writer == 0, waiting == 0
Reader1
===================================
// in rdlock()
atomic_inc(&waiting);
for (;;) {
atomic_inc(&readers);
// if (!writer) has not been executed yet
// readers == 1, writer == 0, waiting == 1
writer
===================================
// in wrlock()
while (atomic_swap(&writer, 1)) spin();
while ((cur=readers)) futex_wait(&readers, cur)
// writer thread is waiting
// readers == 1, writer == 1, waiting == 1
// cur == 1
Reader1
===================================
// back to where it was in rdlock()
if (!writer) return;
atomic_dec(&readers);
futex_wait(&writer, 1);
// Reader1 is waiting
// readers == 0, writer == 1, waiting == 1
Reader2
===================================
// in rdlock()
atomic_inc(&waiting);
for (;;) {
atomic_inc(&readers);
if (!writer) return;
atomic_dec(&readers);
futex_wait(&writer, 1);
// Reader2 is waiting
// readers == 0, writer == 1, waiting == 2
现在你陷入僵局。
当然,我可能不了解futex API是如何工作的(我从未使用过它们),所以如果我不在这里,请告诉我。我假设阻塞的futex_wait()
(因为预期值是正确的)将不会解除阻塞,直到对该地址进行futex_wake()
调用。
如果atomic_xxx()
操作可以取消阻止futex_wait()
,则此分析不正确。
最后,如果发生了这种情况,我还没有机会考虑可能的解决方案......
答案 1 :(得分:0)
我的猜测是这是一个内存排序问题。我不太了解x86内存模型,但我强烈怀疑你需要在futex_*
个调用周围使用内存栅栏。据我所知,x86保证一个内核将以与写入内存单元相同的顺序更新其他内核的内存内容,但似乎您依赖于更强的假设 - 一个内核上的写入将立即被其他内核看到。例如,假设核心A具有rdlock
并且刚刚执行rdunlock
。现在,它清除了waiting
和readers
,但是当核心B尝试wrlock
时,此信息尚未进入核心B.核心B成功获得writer
,但发现现有readers
。 writer
的更新尚未发送到核心A,rdunlock
检查是否需要futex_wake(&readers)
,因此不会。futex_*
这可能会显示您的症状,并且如果while ((cur=readers)) futex_wait(&readers, cur);
调用被简单的旋转替换,它也将具有恢复的属性。这对你有意义吗?
嗯, ((cur==readers))...
应该在{{1}}时?