多处理器编程的艺术 第二版 第六章 笔记

6.1 引言

lock-free的结论与wait-free一致

有一些类型的对象是universal的,给得够多就可以构建出来wait-free的linearizable实现

一个类在n线程的系统是universal  <==>  这个类有一个consensus number >=n
一个机器架构能有足够强大的计算能力来支持任意的wait-free同步  <===>  提供了一个这样的universal类作为原语

提供了compareAndSet()操作的平台,就可以构建出来wait-free的同步对象给任意多个线程

本章节提供了一个universal construction,能够使用“consensus obj”实现任意的同步对象。

所以上个章节讲的共识数终于要派上用场了,之前都是只知道这玩意儿越高越好(作者说的),也不知道高了有什么用。

6.2 Universality

若某个类 C 能通过若干 C 类对象及若干读写寄存器(read-write registers),构造出任意对象的无等待(wait-free)实现,则该类为通用类(universal)

内存回收问题留作练习, 本书很喜欢留作练习啊,很烦

先讲lock-free。

6.3 A lock-free universal construction

对于一个sequential obj,初始状态是固定的,apply()方法接受一个invocation作为参数,然后返回一个response(运行状态+运行结果)

一个顺序对象可以转变成lock-free的并发对象,将任意的顺序对象通过:有着初始状态的对象+一系列的操作(log的list)。一个线程通过给这个list的head添加东西来增加这个对象的方法调用,然后就可以从尾部到头部开始给这个对象的私有复制体进行方法调用。这个对象最后返回他自己执行自己操作的结果。只有头部可变。

输入很多个node的引用,然后做n线程的共识,胜者就会被加入到list里面。

胜者可以接着计算他的回应。它通过创造一个顺序对象的本地复制。

Node after = before.decideNext.decide(prefer);
上面这行代码会被多个线程并发执行,假如我们的共识器好使,就可以选出来一个大家都同意的胜者,假如说我是胜者,那么我的prefer.seq就不再是0,而是当前头部线程+1,就可以出循环,否则就要进行下一轮的投票。出了循环之后,我们拿出来tail的那个node,然后给一个新的obj开始顺着链表挨个执行操作,最后再拿着我的操作给obj弄一下就可以返回了。

我们设计这个构建最坚硬的部分就在于consensus obj只能用一次。

Because threads that do not participate in this consensus may need to traverse the list,
the result of this consensus is stored in the node’snextfield.
我们存储讨论的结果到新的node里面去,这样遍历的时候就可以看得到。

多线程会同步更新,但是值是一样的,所以不怕。

还记得上一章,decide会记录最早离开的的值作为共识的结果,所以进了循环比较慢的也能拿得到结果了。

怎么找到头部?使用了max函数,为什么不能够使用共识对象?比如几个线程一起讨论头部应该是多少,但是问题是头部会被多次访问,比如我们接入了5号,接下来6号有好几个线程又来讨论头部应该是多少。为什么共识对象不能被访问多次,grok: 

这是因为共识对象的内部状态在第一次达成一致后会被“锁定”(逻辑上或物理上),以保证所有线程看到相同的结果。如果允许线程多次调用,可能会破坏一致性,例如导致某些线程看到不同的值。

6.4 A wait-free universal construction

每个线程分享自己的apply调用给其他的线程,n个元素的通知数组,announce[i]是第i个线程的node,当线程i往这个里面塞入node的时候,会跟其他人announce

自己塞不进去,别人可以帮你。
这个代码看起来比较靠谱,但是文章又臭又长,直接ai翻译走起。

微妙的要点:
假设节点 a 由线程 A 创建,并由线程 A 和 B 追加。在第二次追加之前,节点 a 必须至少被添加到 head[] 一次。
为什么?注意每一次循环设置after进log的时候,head[i]=after,但是有没有可能在设置之前(23~26)之间,还没添加的时候,就第二次追加?
线程A添加进去的时候,是24 before.next = after;
那有没有可能线程B在A还没放进head的时候,帮他再放进去,此时对于B来说要捣乱成功,before必须得是A或者A后面的节点(要不然的话就是给A前面那个节点添加上A,没什么影响)。
B想要让before是A或者A后面的节点,before = head[B],head[B] = a/successor(a),所以说确实必须加进去一次,不然的话B都读不到a。
 所以要想制造混乱,a此时已经被加入head了。
任意的节点加入到head之前,它的seq num已经不是0了。B在设置a进入head之前,a的seq肯定不是0. 所以B捣不了乱。
 
为什么一定是wait-free?
|concur(A)|+ start(A) = max(head[]).
上面这个成立
因为 start(A) 是线程 A 宣布时的最大序列号,concur(A) 记录之后新增的节点数。每次追加节点,head[] 更新,序列号和 concur(A) 同步增加 1,保持等式。原子操作和帮助机制确保一致性。
posted @ 2025-04-17 05:49  映空城  阅读(4)  评论(0)    收藏  举报