我在操作系统类中根据需要分配Linux FUTEX(2)
手册页,以警告学生在设计同步原语时不要自满。
futex()
系统调用是Linux提供的API,允许用户级线程同步原语在必要时休眠和唤醒。手册页描述了可以使用futex()
系统调用调用的5种不同操作。两个基本操作是FUTEX_WAIT
(一个线程在尝试获取同步对象并且某人已经持有它时将其自身置于休眠状态)和FUTEX_WAKE
(线程用来唤醒它)释放同步对象时的所有等待线程。)
接下来的三个操作是乐趣开始的地方。手册页描述如下:
FUTEX_FD (present up to and including Linux 2.6.25)
[...]
Because it was inherently racy, FUTEX_FD has been removed
from Linux 2.6.26 onward.
论文"Futexes are Tricky" by Ulrich Dreper, 2004描述了竞争条件(这可能是错过的唤醒)。但还有更多:
FUTEX_REQUEUE (since Linux 2.5.70)
This operation was introduced in order to avoid a
"thundering herd" effect when FUTEX_WAKE is used and all
processes woken up need to acquire another futex. [...]
FUTEX_CMP_REQUEUE (since Linux 2.6.7)
There was a race in the intended use of FUTEX_REQUEUE, so
FUTEX_CMP_REQUEUE was introduced. [...]
FUTEX_REQUEUE
的比赛是什么? Ulrich的论文甚至没有提到它(该论文描述了使用futex_requeue()
而不是FUTEX_CMP_REQUEUE
操作实现的函数FUTEX_REQUEUE
。
答案 0 :(得分:2)
旧的重新排队操作需要两个地址 addr1 和 addr2,首先它在 addr1 上解除等待者的停放,然后将它们停放回 addr2 上。
新的重新排队操作在验证 *addr1 == user_provided_val
后执行所有这些操作。
要找出可能的竞争条件,请考虑以下两个线程:
wait(cv, mutex);
lock(&cv.lock);
cv.mutex_ref = &mutex;
unlock(&mutex);
let futexval = ++cv.futex;
unlock(&cv.lock);
FUTEX_WAIT(&cv.futex, futexval); // --- (1)
lock(&mutex);
broadcast(cv);
lock(&cv.lock);
let futexval = cv.futex;
unlock(&cv.lock);
FUTEX_CMP_REQUEUE(&cv.futex, // --- (2)
1 /*wake*/,
ALL /*queue*/,
&cv.mutex_ref.lock,
futexval);
syscall(1)和(2)都是无锁执行的,但要求它们和mutex
锁的总顺序相同,这样用户就不会丢失信号.
因此,为了检测在实际 wait
之后重新排序的 wake
操作,在锁中获取的 futexval
在 (2) 处传递给内核。
类似地,我们将 futexval
传递给 (1) 处的 FUTEX_WAIT
调用。此设计在 futex man page 中明确说明:
执行请求阻塞线程的futex操作时, 仅当 futex 字具有以下值时,内核才会阻塞 提供的调用线程(作为 futex() 调用)作为 futex 字的期望值。这 加载 futex 字的值,该值的比较 与预期值,实际阻塞将发生 原子地并且将相对于并发完全排序 其他线程对同一个 futex 字执行的操作。 因此,futex 字用于连接同步 用户空间由内核执行阻塞。 类似于原子比较和交换操作 可能会改变共享内存,通过 futex 阻塞是一个 原子比较和块操作。
恕我直言,在锁外调用(2)的原因主要是性能。持有锁时调用wake
会导致waiter醒来无法获取锁的“赶紧等”的情况。
还值得一提的是,上面的答案是基于 pthread 实现的历史版本。最新版本的 pthread_cond
删除了 REQUEUE
的用法。 (查看此 patch 了解详情)。
答案 1 :(得分:1)
看起来竞争条件是由于glibc中互斥锁的实现以及它们与futexes的差异。似乎需要FUTEX_CMP_REQUEUE
来支持更复杂的glibc互斥体:
它们要复杂得多,因为它们支持更多功能,例如测试死锁和递归锁定。因此,他们有一个内部锁定保护额外的状态。这种额外的锁定意味着由于可能的竞争,他们无法使用FUTEX_REQUEUE多重功能。