“固定异步共享内存模型:进程可能崩溃停止,正确进程持续取得步骤;实现可使用任意多个原子读写寄存器和某种线性一致的对象类型 $T$。$T$ 的共识数 $c(T)$ 定义为:能够用这些对象为 $n…”
形式陈述
并发对象是共享内存系统中一个实现了顺序抽象数据类型的共享实体:多个线程可以调用其操作,且不同线程的操作区间(从调用事件到返回事件)允许在时间上重叠。对象的行为由并发对象历史记录——调用与返回事件的序列,每个线程内部保持程序顺序。正确性条件规定哪些并发历史是可接受的,做法是将其与顺序规格关联:例如线性一致性要求每个已完成操作都能指定一个落在其调用与返回之间的线性化点,使按这些点排序的顺序执行符合顺序规格;顺序一致性只要求存在保持各线程程序顺序的合法顺序重排。
除安全性外,对象规格还须声明进展条件。阻塞、obstruction-free、lock-free 与wait-free分别量化不同的执行和完成对象;它们不改变对象的顺序规范,安全与进展是相互独立的两个维度。
直觉
并发对象把内部的缓存访问、原子指令和重试步骤封装成方法调用。客户端依赖抽象规格与一致性条件,不必逐条分析实现指令;但客户端自身仍是并发程序,两次方法调用之间可以插入别人的操作。“每个方法都像瞬间完成”不意味着一串方法自动成为一个原子事务。
例如 if (!q.empty()) q.dequeue() 中,检查返回非空后,另一个线程可以先取走唯一元素;队列的两个方法即使各自线性一致,也没有违反规格。客户端要依赖一次 try_dequeue 同时返回成功标志与元素,或用覆盖整个组合操作的共同同步协议。
“线程安全”至少需要说明一致性与进展两件事。用锁包住每个方法与 wait-free 实现可能满足同一顺序规格,持锁线程暂停时前者却可能使其他调用一起停住;封装并不会消除这种区别。
例子与边界
正例:一个初始为空的线性一致队列上,P 调用 enqueue(1)、Q 调用 enqueue(2),两个区间重叠。在没有其他操作约束时,两种入队顺序都符合规格;这表示可选的抽象历史不唯一,不表示具体实现的线性化点能任意移动。若两次入队都返回后才有第一次 dequeue,且它返回 2,那么在没有其他插入或删除的条件下,下次 dequeue 必须返回 1。已观察结果会约束同一个历史中的后续行为。
若所有对象访问都由同一把互斥锁保护,每个临界区正确实现顺序操作且内存可见性满足要求,就可按临界区顺序建立线性一致性证明。它仍是阻塞的:持锁线程暂停,其他线程可能无法完成调用。线性一致性还具有对象层的局部性:各对象历史分别线性一致,可组合成共同的合法历史;这不把跨对象的多次调用合成一个操作。顺序一致性没有同样的局部性,分别找到的顺序可能无法合并。
并发栈的两个重叠 push 可按任一顺序线性化,但随后 pop 必须与所选顺序一致。若一个 pop 在某个 push 调用前已经返回,就不能把它排在该 push 之后。线程安全只说没有数据竞态通常不够,还需判断行为是否满足栈、队列等抽象规格。
推论与应用
并发对象是多核数据结构设计的基本单元:并发哈希表、无锁队列、读写锁与事务内存都以“顺序规格 + 一致性条件 + 进展条件”的三元组陈述目标。非阻塞层级说明一次调用在何种调度下完成;端到端证明还要把内存分配、回收和回调的进展性质算入。
理论上,对象类型是共享内存可计算性研究的载体。Herlihy 共识层级按“能为多少线程 Wait-free 地解决共识”分类对象,并说明低层对象不能 Wait-free 实现高层对象;这是一项对象类型的表达力结论,不是具体实现的性能排名。
参考资料
- Maurice Herlihy and Nir Shavit, The Art of Multiprocessor Programming, rev. 1st ed., Morgan Kaufmann, 2012,Chs. 1–18。
- Hagit Attiya and Jennifer Welch, Distributed Computing: Fundamentals, Simulations, and Advanced Topics, 2nd ed., Wiley, 2004,Chs. 1–18。