L17:事务内存(Transactional Memory)
MIT 6.5900 Fall 2024 · Daniel Sanchez(基于 Christos Kozyrakis 的幻灯片) 主题:并行编程之难、并发控制、事务内存(TM)语义、数据版本管理与冲突检测、硬件事务内存(HTM)
一、动机:并行编程很难
多核的理由是性能/成本曲线:与其一个高性能高成本的核,不如用多个适中而高效的核。但并行编程很难:
- 把算法划分为任务;
- 把任务映射到线程;
- 加同步(锁、屏障……)以避免数据竞争并保证任务顺序;
- 陷阱:可扩展性、局部性、死锁、活锁、公平性、竞争、可组合性、可移植性……
例:哈希表
顺序实现的 lookup 非线程安全(并发 insert 与 lookup 会竞争),需要同步。
- 粗粒度锁:给所有方法加
lock/unlock。问题:把对互不相关的桶的操作也串行化了; - 细粒度锁:每桶一把锁。问题:加锁开销大,且仍过度串行化(如对同一桶的并发只读也被串行)。
性能:在平衡树与哈希表两种负载下,细粒度锁通常优于粗粒度锁,但都难以随处理器数线性扩展。
二、并发控制(Concurrency Control)
为避免共享数据上的竞争,两类思路:
- 停顿(Stall)/ 互斥:保证临界区内至多一个进程,其余等待;
- 推测(Speculate):
- 猜:假设临界区内不会冲突;
- 查:检测是否发生冲突的数据访问;
- 恢复:若冲突则回滚,否则提交。
三、事务内存(TM)
内存事务(memory transaction,[Lomet'77, Knight'86, Herlihy & Moss'93]):一段原子且隔离的内存访问序列,灵感来自数据库事务。
- 原子性(Atomicity,要么全做要么不做):提交时所有写一次性生效;中止时所有写如同未发生;
- 隔离性(Isolation):提交前其他代码看不到这些写;
- 可串行化(Serializability):事务看起来以某个单一串行顺序提交(但具体顺序不保证)。
用 TM 编程:声明式同步
程序员说做什么而非怎么做,无需声明或管理锁;系统通常通过推测实现同步,只在冲突(R-W 或 W-W)时才有性能损失。
// 用锁 // 用 TM
void deposit(account, amount) { void deposit(account, amount) {
lock(account.mutex); atomic {
int t = bank.get(account); int t = bank.get(account);
t = t + amount; t = t + amount;
bank.put(account, t); bank.put(account, t);
unlock(account.mutex); }
} }TM 的优势
- 易用:像粗粒度锁一样易用,程序员声明、系统实现;
- 高性能:至少不逊于细粒度锁,自动获得读-读并发与细粒度并发,性能与正确性之间无需权衡;
- 可组合性(Composability):软件模块可安全可扩展地组合(嵌套事务)。
性能:硬件 TM 系统 TCC [Hammond et al., ISCA'04] 在两种负载上随处理器数扩展明显优于锁。
四、TM 实现基础
用推测在不牺牲并发的前提下提供原子性与隔离性。基本要求:
- 数据版本管理(Data versioning);
- 冲突检测与解决(Conflict detection & resolution)。
实现选项:硬件 TM(HTM)、软件 TM(STM)、混合 TM(硬件加速的 STM 与双模系统)。
为什么要硬件 TM
- 单线程 STM 性能有 2–8× 减速(短期使并行编程失去动力,长期不节能);
- 业界已采用 HTM:Intel(自 Haswell)、IBM(POWER8+、Blue Gene、zSeries)、ARM(v9)。
五、数据版本管理策略
为并发事务管理未提交(新)与已提交(旧)两个版本:
| 策略 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| 急切版本(Eager,基于撤销日志 undo-log) | 直接更新内存,把旧值记入 undo log | 提交快 | 中止慢 |
| 惰性版本(Lazy,基于写缓冲 write-buffer) | 数据缓冲到写缓冲,提交时才写回内存 | 中止快 | 提交慢 |
急切版本:写 X←15 时直接改内存并把 X:10 记入 undo log;提交丢弃日志,中止时用日志恢复。 惰性版本:写 X←15 进写缓冲,内存仍是 10;提交时写回内存,中止时丢弃写缓冲。
六、冲突检测(Conflict Detection)
须跟踪事务的读集(read-set,事务内读过的地址)与写集(write-set,事务内写过的地址),处理 R-W 与(常见的)W-W 冲突。
1. 悲观检测(Pessimistic)
在 load/store 当时就检查冲突:
- SW:用锁和/或版本号设软件屏障;
- HW:通过一致性动作检查;
- 用竞争管理器(contention manager)决定停顿还是中止,按优先级策略让常见情况快。
四种情形:成功;提前检测后停顿;检测到后中止重启;多事务相互中止导致无进展(无前进保证)。
2. 乐观检测(Optimistic)
在事务尝试提交时才检测冲突:
- SW:用锁/版本号校验读写集;
- HW:用一致性动作校验写集(为写集的 Cache 行取得独占访问),冲突时优先提交中的事务,其他事务稍后可能中止;
- 也可二者结合:不少 STM 对读乐观、写悲观。
冲突检测的权衡
| 悲观 | 乐观 | |
|---|---|---|
| 优点 | 早检测,少做无用功,部分中止转停顿 | 有前进保证,潜在更少冲突、更短加锁(SW)、批量通信(HW) |
| 缺点 | 无前进保证,某些情况下更多中止;加锁问题(SW)、细粒度通信(HW) | 晚检测,仍有公平性问题 |
七、硬件事务内存(HTM)实现
- 数据版本管理:用 Cache —— 把写缓冲或 undo log 缓存进 Cache,用 Cache 元数据跟踪读集与写集(可在私有/共享/多级 Cache 上做);
- 冲突检测:用缓存一致性协议 —— 一致性查找检测事务间冲突(支持侦听式与目录式一致性);
- 中止时还须恢复寄存器状态 → 取寄存器检查点(乱序核可借助重命名表快照以极小改动支持)。
HTM 设计:每行的 R/W 位
每个 Cache 行跟踪读集与写集:
- R 位:事务读过该数据(load 时置位);
- W 位:事务写过该数据(store 时置位);
- R/W 位可在字或行粒度;提交或中止时整组清除(gang-clear)。
一致性请求检查 R/W 位以检测冲突:
- 对 W 字的 Shared 请求 = 读-写冲突;
- 对 R 字的 Exclusive 请求 = 写-读冲突;
- 对 W 字的 Exclusive 请求 = 写-写冲突。
行格式示意:V D E Tag | R W Word1 | ... | R W WordN。
示例:惰性 + 乐观 HTM(基于总线)
CPU 改动:寄存器检查点、TM 状态寄存器(状态、处理器指针等);Cache 改动:每行 R/W 位。
执行 Xbegin / Load A / Store B←5 / Load C / Xcommit:
- 事务开始:初始化 CPU 与 Cache 状态,取寄存器检查点;
- Load A:必要时处理缺失,置该行 R 位;
- Store B←5:必要时处理缺失(即使其他核有该行也先取 shared),置该行 W 位;
- 快速两阶段提交:① 校验(对写集行请求独占访问)② 提交(整组清 R/W 位,把写集数据转为有效脏数据)。
冲突检测与中止:对读集/写集查找独占请求 → 命中则中止(作废写集、整组清 R/W 位、恢复检查点)。
HTM 的优势与挑战
优势:
- 常见情况快:零开销跟踪读写集、零开销版本管理、无数据移动的快速提交/中止、对读集持续校验;
- 强隔离:对非事务 load/store 也检测冲突;
- 简化多核一致性与一致性模型(如可用 HTM 实现难做的顺序一致性 SC)。
挑战:
- 性能病态:频繁竞争如何处理?HTM 是否应保证公平/优先级?
- 容量限制:读集+写集超过 Cache 容量怎么办?
- 虚拟化、I/O、系统调用……;
- 混合 TM 可能两全:硬件处理常见情况但无保证(Cache 溢出、中断、syscall 时中止),中止后回退到 STM(Intel RTM 的当前思路),但 HTM 与 STM 如何良好整合仍不清楚;
- 目前程序员采用缓慢/受限,仍须支持非 HTM 系统。
小结
- 锁在易用性与并发度之间总有取舍;事务内存用声明式
atomic把"做什么/怎么做"分离,提供原子性、隔离性、可串行化与可组合性; - 实现核心是数据版本管理(急切 undo-log / 惰性 write-buffer)与冲突检测(悲观早检 / 乐观晚检);
- HTM 复用 Cache 做版本管理、复用一致性协议做冲突检测,以每行 R/W 位跟踪读写集,常见情况近零开销;难点在容量、虚拟化与公平性,混合 TM 是折中方向。
下一讲:VLIW