futex为更好支持pthread_cond的实现(,最主要是broadcast),设计了requeue功能,并以futex系统调用提供操作接口,包括一对配对的操作 futex_wait_requeue_pi 以及 futex_requeue。
mutex互斥体,确保临界区之间互斥(mutual exclusion),但不能满足不同临界区之间的前驱后继的关系,所以可以通过在临界区A使用condition variable条件变量,等待另一临界区B的发出信号指令。
Condition Variables
Synchronization mechanisms need more than just mutual exclusion; also need a way to wait for another thread to do something.
临界区A,与临界区B虽然使用了mutex,确保彼此之间互斥(mutual exclusion)。但是同时使用了condvar,保证临界区B前驱,临界区A后继,即临界区A等待临界区B先完成某些事。
一个condvar必须从属于(belong to)一个mutex。
因此condvar的等待(wait)包含了两次等待,或两个队列等待。一个是condvar信号的等待(队列),另一个是它所属的mutex锁的等待(队列)。这使得一次condvar wait必须先释放mutex,然后依次等待condvar信号和mutex。这意味着等待的线程必须进行两次等待和两次唤醒。
对condvar发起信号signal,就是唤醒其中一个等待在condvar的线程,让它转而等待condvar所属的mutex,再次回到临界区执行。而broadcast最简单的方法就是将所在等待在condvar的线程统统唤醒。
当对condvar进行broadcast时,多个等待在condvar的线程被唤醒回到用户空间,一起去竞争condvar所属的mutex,而又每个线程又因锁竞争必须再次回到内核进行排队等待,从而造成了多次的线程切换,系统调用的浪费。
为了改善这种浪费,可以将一个等待在两个等待队列之间的转换,优化在一次调用中进行,避免多个线程被唤醒去竞争锁。这样的功能就是futex提供的requeue。
被requeue后的waiter(,调用futex_wait_requeue_pi而阻塞的线程),必须等待condvar所属的mutex去唤醒。当waiter从futex_wait_requeue_pi系统调用阻塞中被唤醒,并不表示已经获得了condvar所属的mutex锁,而是要去竞争这个锁。
requeue将这个过程的condvar和mutex看作为futex1和futex2。
futex_requeue函数原型为
static int futex_requeue(u32 __user *uaddr1, unsigned int flags, u32 __user *uaddr2, int nr_wake, int nr_requeue, u32 *cmpval, int requeue_pi);
这个函数的功能是,将uaddr1对应的futex等待队列出队最多 (nr_wake + nr_requeue) 个futex_q,先对出队的futex_q进行唤醒nr_wake个,剩下的才进行requeue到futex2等待队列。而pthread_cond_broadcast固定设定参数 nr_wake = 1,nr_requeue = INT_MAX。就是说在broadcast的时候只会唤醒一个线程去竞争futex2,其余阻塞在futex1的线程都会出队再入队到futex2的等待队列。
requeue-pi 就是condvar所属的mutex是一个使用pi协议的mutex,即futex2是pi-futex,requeue对一个non-pi futex的waiters出队再入队到pi-futex等待队列进行特殊的优化。futex_requeue函数只会将可以马上获得pi-futex锁的线程唤醒,也就是说对每一个requeue到pi-futex的waiter都先尝试锁pi-futex,尝试成功的才能被唤醒,直到有线程被唤醒,其余waiter就直接requeue到pi-futex,而不再进行锁尝试。虽然futex_proxy_trylock_atomic并不像futex_lock_pi那样会去调用rt_mutex_trylock,但是与non-pi到non-pi的requeue,增加了额外的尝试。
下面是requeue non-pi 和 requeue-pi 分支的逻辑对比: