glibc 实现代码的注释之翻译

condvar 的实现保证了对于 signal, broadcast, wait 的每次调用是以某种全序关系发生 -- 即在调用流程中保持 happens-before 关系。然而,全序关系并不一定会导致会建立附加的 happens-before 关系。对 wait 的调用实际上包括了三个独立的原子操作:(1)释放锁并进入阻塞等待; (2)等待结束; (3)加锁.


所有的 waiters 都在一个等待序列(WSEQ)中获得一个确定的位置,等待序列的长度为 64 位长的 __wseq。该序列决定哪些 waiters 可以消耗 signals。 调用一个 broadcast 等价于发送和 waiters 一
样多的 signals。当一个 signal 到达,它使用 releax-MO(松散内存顺序 releax memory order) load 的方式去读取 __wseq 值(即下一个 waiter 获得的位置)。使用 releax-MO 是足够的因为 signal 的调用已经是获得锁之后发生的。只有当 waiters 的 position 小于 signal 观察到的 __wseq 值时,waiters 才有机会消耗这个 signal [意为在 signal 到达之前已经在等待的 waiters 可以消耗这个 signal].


如果所有的 waiters 仅自旋等待那么是很容易实现的,但是我们需要用到 futex 使 waiters 进入阻塞等待。Futexes 并没有作出 FIFO 唤醒顺序的保证,所以我们仅使用 futex 是无法实现可靠唤醒的[有可能有机会消耗 signal 的 waiters 没有被唤醒]。而且,futex word 是 32 位长的,但我们需要区分多于 1<<32 个状态,因为我们需要表达出 wake-up 的顺序(即那些有机会消耗 signal 的 waiters); 一个 waiter 在 futex 上进入等待和决定它在 __wseq 上的位置,这两个操作并不是原子的,所以我们需要 futex word 可靠地通知 waiters 不应该再尝试进入等待因为它们在此时已经被唤醒了。因为 ABA 问题在 32 位长的值上是很小概率事件,当意识到它可能带来副作用时我们可以忽略它。
所以我们使用一个 64 位长的计数器去表示 __wseq(在仅支持32位长值原子操作的架构上,我们使用少于 64 位长的值). 我们使用两个组去处理在 futexes 上的等待:
1.G1 组由那些有机会消耗 signals 的 waiters 组成。新到来的 signals 将一直唤醒该组的 waiters 直到 G1 所有的 waiters 都被唤醒。
2.G2 组由后到达的 waiters 组成(此时 G1 中还存在未被唤醒的 waiters)。当 G1 中所有的 waiters 都被唤醒且有一个新的 signal 到达,则这个 signal 将 G2 转化为 G1 且“新建”
一个 G2 为将来的 waiters 使用.
由于 condvar 可能在多个进程之间共享,所以我们不能分配新的内存. 上面的“新建”其实是 G1 和 G2 之间的角色互换。它们各有独立的: futex word, 可供消耗的 signals 数量, 没有被唤醒的 waiters 数量,引用计数。


组引用计数维护了使用组的 futex 的 waiters 数量。当组角色互换之前必须的条件是没有 waiters 正使用组的 futex。这正好避免了 futex word 上的 ABA 问题。


为了表示 __wseq 中各个组覆盖的范围以及区分 G1/G2,我们使用一个 64位的计数器来指示 G1 的开始位置,以及它中间的一位来表示哪一个组是 G2。这允许了我们原子地做这两件事:
转换 G1/G2 角色,waiters 获得 WSEQ 中的位置. G1 开始的位置作用在于 waiters 可据此计算出它们是否处于一个已经被全部唤醒的组中(即,如果 G1 开始位置在当前 waiter 之后则说明它已经被唤醒了). waiters 不能实时知道它们处于 G1 或 G2 中,但它们不需如此,因为 waiters 关心的仅仅是当前有没有可消耗的 signals,并且 waiters 经常一开始位于 G2 中(waiters 知道 G2 的 slot 因为 __wseq 有一个位指示了). signals 将简单地填充那个完全唤醒且可以 close 的组(组的角色将被转换,直到组有必要去减少等待 waiters 仍持有即将关闭的 G1 的引用的可能性)。


signals 维护了 G1 初始化大小,这样它可以计算出 G2 的起始位置(G2 是右开区间的直到它变为 G1)。signals 可以跟踪任何一个 group 的剩余大小; 当 waiters 取消等待(调用 pthread 系列函数或超时),signals 将减少剩余大小.


为实现 convar 必要的销毁操作(如 ptrhead_cond_destroy 可以在所有 waiters 都被唤醒后被调用),waiters在等待之前会增加引用计数,结束等待之后且获得用户锁之前会减少引用计数。


所以 pthread_cond_t 数据结构包含以下成员(用于标志的一些位变量虽不是流程需要的一部分,但为了实现原子化,抑或是为了没有别的空间可以放置,所以某些成员的位就单独拿出来作为此用):
__wseq(waiter 序列计数)
1.LSB(least significant bit 最低有效位) 表示 G2 的索引
2.waiters 在获得用户锁之后对此变量执行 fetch-add(增加并返回旧值) 操作,signalers 加载并对它进行 fetch-xor. (以上两个操作者是并行的)


__g1_start(G1 开始位置, inclusive)
1.LSB 表示 G2 的索引
2.signalers 获得 condvar-internal 锁之后会修改此值,且该值会被并行地被 waiters 观察。

__g1_orig_size(G1 的初始化大小)
1.最低两位表示 condvar-internal 锁(00 表示没有获得锁;01 表示获得锁; 02 表示使用 futex_wake 的方式获得了锁)
2.仅获得了 condvar-internal 锁才能访问此值

__wrefs(waiter reference count)
1.第 2 位为 true 表示 waiters 应该执行 futex_wake 当最后一个引用被移除。pthread_cond_destory 使用该位作为 futex word
2.第 1 位表示 clock ID(0 表示实时时间,1 表示系统运行的流逝时间)
3.第 0 位为 true 表示当前的 condvar 是进程间共享的
4.如果 _wrefs 的格式变化了,应该....


对于每个 group 有下面这些成员:
__g_refs(futex 的 waiter 的引用计数)
1.LSB 为 true 表示最后一个引用被移除后应该运行 futex_wake 
2.该引用计数同时被 waiters 和 signalers 访问,前提是获得 condvar-internal 锁


__g_signals(可被消耗的 signals 的个数)
1.waiters 用此变量作为 futex word. 且它是同时被 signalers 和 waiters 访问的
2.LSB 为 true 表示此 group 已经被完全唤醒了(即此组被关闭了)


__g_size(当前组中还没有被唤醒的 waiters 的个数)
1.被 signalers 和取消等待的 waiters 访问,前提是获得 condvar-internal 锁
2.G2 的此值一直为 0 ,直到该组转变为 G1 才去计算
3.虽然此变量是 unsigned 的,但是我们可以依赖于 unsigned 的溢出规则来保存一个负数(特别地,G2 中的 waiters 取消等待)。


PTHREAD_COND_INITIALIZER 会将 condvar 的各个成员置为 0,且 G2 开始位置是 0 且 G1 是关闭的。


因为 waiters 正当从 __wseq 中获得一个位置时,并没有要求明确归属的 group,而只是在使用 futex 进入等待前明确了 group 的引用计数,于是便可能发生
一个问题:waiter 增加一个组的引用计数前组已经关闭了。所以 waiters 需要使用 __g1_start 检查组是否已经关闭(从 __wseq 中获得一个位置时),且在为了
从 __g_signals 中获得一个可用的 signal 进行的自旋等待时也需要作此检查. 需要注意的是, 检查中使用 releax-MO 加载 __g1_start 便已足够,  因为如果一个 waiter 可以看到一个足够大的值则它也可以在 waiter 组中消耗一个signal。


waiters 尝试从 __g_signals 中获得一个 signal 时,没有使用引用计数,有可能导致:它自从更近一个组中取得 signal,因为自己的组已被关闭。waiters 并不能时时监测到
这一行为,因为他们不知道什么时候会有这种行为,但是它们可以保守地往那个组加回一个 signal,如果这样-导致的结果将是虚假唤醒。为了尽可能避免这发生, __g1_start 
也包含了当前 G2 的索引,它将允许 waiters 进行是否存在组混淆的检查,如果没有,则它们并没有从 G1 错误地取得 signal,这意味着,G1 已经关闭了所以不需要任何修复。


pthread_cond_t 最后一个成员是 __g_signals[1],这是必要要。在老的版本的实现中,它指针大小的,所以使用 PTHREAD_COND_INITIALIZER 初始化后将得到一个0填充的四字节
空间,然而我们需要的是8字节的,__g_signals[1] 不会被访问直到第一次的组转换发生(G2 的 index 是0):经过一次无害的 fetch-or 操作(返回值被忽略)后设置 __g_signals[1]
为 0。








We need 3 least-significant bits on __wrefs for something else


以上翻译基于:

https://code.woboq.org/userspace/glibc/nptl/pthread_cond_wait.c.html#__condvar_cancel_waiting

https://code.woboq.org/userspace/glibc/nptl/pthread_cond_signal.c.html#__pthread_cond_signal




































































  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值