无锁边界、性能与诊断 / Lock-Free Boundaries, Performance, and Diagnostics
1. 无锁的进展保证
| 保证 | 含义 |
|---|---|
| blocking | 线程可能因锁持有者停顿而无法前进 |
| lock-free | 系统整体持续有操作完成,单线程可能饥饿 |
| wait-free | 每个操作在有界自身步骤内完成 |
| obstruction-free | 单独运行足够久可完成 |
无锁不等于无等待、无竞争或更快。CAS 失败重试、缓存线争夺和回收扫描可能造成很差尾延迟。
2. 真正难点是内存回收
T1 读取 node* p
T2 从结构删除 p
T2 delete p
T1 解引用 p --------> use-after-free原子化 head 指针只保护指针值更新,不保证节点活着。常见方案:
- hazard pointer:线程发布正在访问的节点,回收前扫描危险指针;
- epoch/RCU 风格:延迟到所有旧读者离开 epoch 后回收;
- 引用计数:需解决获取引用与对象归零之间的竞争,成本也可能高;
- GC/成熟库:把复杂协议交给经过验证实现。
3. ABA 与版本标签
版本化指针把地址和计数一起 CAS,可降低 A→B→A 误判,但要处理计数回绕、对齐位、原子宽度和平台支持。它也不自动解决节点回收。
4. False sharing
一条 cache line(例如常见 64B,但不是标准常量)
[counter A][counter B][........]
Thread A 写 A Thread B 写 B
逻辑不同变量,却因同一缓存线反复失效C++17 提供 hardware_destructive_interference_size / hardware_constructive_interference_size 提示,但实现可用性和数值需检查。padding 会增加内存,只有 profile 证明热点时使用。
5. 争用、吞吐与尾延迟
总吞吐高可能掩盖某些线程长期饥饿。并发基准至少报告:
- 线程数扫描;
- ops/s 与 p50/p95/p99;
- 读写比例、键分布和临界区长度;
- CPU 使用、上下文切换、缓存未命中;
- NUMA 和绑定配置;
- 调试/发布、优化和 Sanitizer 是否开启。
不要用单线程微基准推断高争用性能,也不要在 Sanitizer 构建上比较绝对性能。
6. ThreadSanitizer
典型 Clang/GCC 命令:
clang++ -std=c++20 -O1 -g -fsanitize=thread -fno-omit-frame-pointer app.cppTSan 动态检测实际执行路径中的数据竞争和部分同步误用。它不能证明无竞态,可能不理解自定义汇编同步,且通常不能与 ASan 同时使用。Windows/MSVC 原生支持情况与 Linux/Clang 不同,应记录实际工具链边界。
7. 第一份报告优先
一次 data race 会破坏后续状态,产生大量次生报告。调试流程:
- 固定最小可复现和输入种子;
- 从第一份报告的两条冲突栈开始;
- 找到对象所有权和预期同步协议;
- 修复协议而非加入随机 sleep;
- 重跑压力测试和 Sanitizer;
- 添加能覆盖该交错的测试/断言。
8. 常见伪修复
- 把普通变量改为
volatile; - 在两次访问间加 sleep;
- 把所有 atomic 改 relaxed;
- 只在写端加锁,读端不加;
- 泄漏节点以“解决”UAF,却没有明确资源上限;
- 把大锁拆成多锁,却没重新定义不变量和锁顺序。
9. 何时不要手写无锁结构
若团队无法给出线性化点、progress guarantee、内存序证明、回收协议和压力/工具测试,就优先使用 mutex、标准同步设施或成熟库。mutex 在低争用时往往有非常优化的快路径,并能在长等待时让线程休眠。
10. 面试速答
问:lock-free 是否保证每个线程前进?
不保证,只保证系统整体持续完成操作;某线程可能不断 CAS 失败而饥饿。
问:false sharing 是数据竞争吗?
不是,变量可被正确同步且逻辑独立,但共享缓存线导致性能抖动。
问:TSan 没报告能否证明线程安全?
不能,它是动态检测,只覆盖本次运行路径和其理解的同步机制。
11. 自测题与答案
- 为什么 atomic head 不能独自保证节点安全?
- hazard pointer 的核心思想是什么?
- 为什么并发基准必须看尾延迟?
- sleep 为什么不能修复 data race?
<details> <summary>参考答案</summary>
- 另一个线程可删除并释放已读出的节点,指针值原子不延长对象生命周期。
- 读者公开当前可能解引用的节点,回收者仅释放未被任何 hazard 保护的退休节点。
- 高吞吐可能伴随个别请求/线程长期等待,用户体验和关闭时延由尾部决定。
- 它不建立标准同步关系,只改变某次调度概率,data race 仍是 UB。
</details>
12. Lock-free 的三层问题
1. 原子更新:CAS 是否正确?
2. 线性化:操作在哪一点对外生效?
3. 内存回收:其他线程何时不再持有旧节点?只写出无锁 push/pop 的 CAS 循环远未完成;节点一旦过早 delete,另一个线程可能仍在读其 next,形成 UAF。
13. 进展保证不是性能排序
blocking: 线程可因锁持有者暂停而无法推进
lock-free: 系统整体总有某操作推进,单线程可饥饿
wait-free: 每个操作在有界自身步骤内完成更强保证通常更复杂,不代表墙钟更快。低争用 mutex 可能在缓存、功耗和尾延迟上优于重试 CAS。
14. Hazard pointer 的基本握手
reader load candidate
reader publish hazard=candidate
reader recheck candidate still reachable
reader safely dereference
writer removes node -> retire list
writer scans hazards; unprotected retired nodes can reclaim发布与重检顺序必须严格,hazard slot 管理也有成本。Epoch/RCU 类方案让线程声明读侧时期,批量延迟释放,但停滞线程会阻碍回收。
15. ABA 与带版本指针
head = (A, version 7)
A removed/reused
head = (A, version 9)
CAS expecting (A,7) fails版本位宽会回绕,打包还受指针对齐、平台位数和原子宽度约束。版本标签解决身份变化检测,仍需安全回收保证读取节点本身合法。
16. False sharing 的具象图
one cache line
[counter T0][counter T1][counter T2][counter T3]
core0 writes core1 writes
cache line ownership repeatedly bounces although logical variables differ可用 alignas/hardware_destructive_interference_size 思路分隔热点,但具体硬件值和标准库支持需验证。填充会增加内存占用,先用性能计数器证明。
17. CAS 失败率是争用信号
记录尝试数/成功数、队列深度、退避次数和 p99。忙等在超额订阅或虚拟机暂停下会浪费核心;指数退避、yield 或阻塞后备可能改善,但会改变公平性与尾延迟。
18. TSan 报告与理论证明
TSan 观察一次执行中的同步和冲突访问,能发现许多 data race;自定义原子协议可能需要工具注解,未走路径不会被检查。即使 TSan clean,ABA、错误线性化、逻辑丢更新和内存序在弱架构问题仍可能存在。
formal happens-before/linearization reasoning
+ stress/fuzz/scheduler perturbation
+ TSan
+ production metrics
= 更可信证据19. 何时退回锁
- 无成熟回收方案;
- 争用低且临界区短;
- 需要复合不变量与公平性;
- 团队无法长期维护内存序证明;
- benchmark 未证明收益;
- 平台原子能力不统一。
标准/成熟库的并发队列通常优于自写;最好的优化常是减少共享、分片或批处理,而非更复杂 CAS。
20. 连续追问与自测
问:lock-free 是否没有锁? 答:算法进展不依赖互斥阻塞,但实现/特定 atomic 可能受平台支持;术语重点是进展保证。
问:atomic<shared_ptr> 能解决所有回收吗? 答:可简化部分所有权,但引用计数、控制块争用和结构线性化仍需分析。
- 线性化点是什么? 答:并发操作可视为瞬间生效的那个原子事件。
- hazard pointer 为何要重检? 答:候选可能在读取与发布 hazard 之间已被移除。
- false sharing 有 data race 吗? 答:可以没有;它是不同变量共享缓存行导致的性能争用。
9. 真实问题:无锁队列吞吐高但尾延迟失控
基准测试只看平均吞吐。 高竞争时 CAS 重试让少数线程长时间饥饿。 缓存行在核心之间来回转移。 回收扫描又占用工作线程时间。 结果是 p99 延迟远高于互斥队列。 无锁只描述进展保证,不承诺公平或低延迟。 应同时测量失败重试、调度等待和缓存未命中。 基准必须固定线程数、任务大小和 CPU 亲和性。 单线程结果不能代表并发行为。
10. 最小实验:false sharing
struct Counters { std::atomic<long> a{0}; std::atomic<long> b{0}; };
struct Padded { alignas(64) std::atomic<long> a{0}; alignas(64) std::atomic<long> b{0}; };两个线程分别递增 a 和 b。 Counters 可能让两个原子共享一条缓存线。 Padded 用对齐把它们分开。 如果吞吐显著变化,说明瓶颈来自缓存一致性流量。 这两个变量没有数据竞争。 不要把性能问题误报成内存模型错误。 缓存线大小应按目标架构验证。 过度填充会增加内存占用和 NUMA 访问成本。
11. ABA 与安全回收
线程 A 读取指针 P 并暂停。 线程 B 移除 P、释放对象,再分配新对象复用同一地址。 线程 A 的 CAS 看到地址仍为 P,就错误地认为状态未变。 这就是 ABA。 增加版本计数可以识别部分复用。 但宽度不足会发生计数回绕。 hazard pointer 让读者发布正在访问的地址。 epoch reclamation 等待所有可能读者离开旧纪元。 引用计数把对象寿命交给控制块,但会产生原子计数争用。 回收协议必须和线性化点一起证明。
12. 诊断工具边界
ThreadSanitizer 擅长发现普通内存数据竞争。 它不能证明无锁算法的线性化正确。 也不能覆盖未执行的调度交错。 AddressSanitizer 可发现部分越界和 UAF。 性能分析器可观察锁等待、上下文切换和缓存事件。 Linux perf、Windows ETW/WPA、Intel VTune 的采样维度不同。 工具开销会改变调度,需要对照基准。 先构造最小复现,再启用高开销检测。 保留编译器、架构和运行参数。
13. 工程实践:何时不用无锁
共享不变量超过一个原子字段时优先 mutex。 需要阻塞等待时,条件变量通常更清晰。 需要公平性时不要依赖 lock-free 保证。 需要安全回收且团队无经验时选择共享所有权。 只有瓶颈经测量确认后才引入 CAS 循环。 记录 CAS 失败率和重试上限。 为饥饿线程提供退避或分片策略。 按生产者和消费者分离缓存线。 发布 ABI 时确认 atomic 类型的对齐和锁实现。 跨 DLL 传递原子对象要统一编译器和运行库。
14. 失败场景复盘
失败一:CAS 循环无退避,核心占用 100%。 修复:指数退避、分片或回退到锁。 失败二:节点删除后立即 free,压力下崩溃。 修复:hazard 或 epoch 回收。 失败三:只看平均吞吐,忽略 p99。 修复:报告分位数和饥饿时间。 失败四:填充常量固定为 64,迁移到其他架构失效。 修复:依据目标平台缓存线和硬件计数验证。 失败五:TSan clean 就宣布算法正确。 修复:补充模型检查、线性化证明和调度压力。
15. 自测题
问:lock-free 是否代表每个线程有界完成?答:不是,那是 wait-free 的目标。 问:ABA 只是指针问题吗?答:任何可复用的状态值都可能出现类似问题。 问:false sharing 是否未定义行为?答:不是,它通常是合法但低效的访问模式。 问:TSan 能证明没有死锁吗?答:不能,它主要检测数据竞争。 问:为什么先测量再无锁?答:无锁增加证明、回收和诊断成本,未必改善真实瓶颈。
16. 基准的最小协议
固定编译器版本、优化级别和 CPU 频率策略。 预热后再采样,避免把初始化算入结果。 报告中位数、p95、p99 和最大值。 记录线程数与核心数的比例。 分别测试低竞争和高竞争输入。 统计 CAS 失败次数和退避次数。 区分吞吐下降与调度抖动。 检查是否有日志、分配和系统调用污染结果。 把互斥版本作为基线。 没有基线就无法判断无锁优化是否值得。
17. 线性化审查
为每个公开操作指出唯一线性化点。 入队可能在线程链接新节点的 CAS 成功处线性化。 出队可能在推进 head 的 CAS 成功处线性化。 失败路径不能泄漏已保留的资源。 帮助者线程修改状态时也要保留调用语义。 状态读取和对象销毁的顺序要独立证明。 测试只能增加信心,不能替代不变量推理。 无法说明线性化点时,优先回到互斥实现。
18. 这一节先记住
无锁并不自动更快,也不自动公平。 ABA 与回收是算法的一部分。 false sharing 是缓存一致性成本,不是数据竞争。 工具提供证据,不替代正确性证明。 以 p99、失败重试和可维护性决定是否采用。
工程深化:把本篇知识落到项目里
这一节不是为了凑篇幅,而是把《无锁边界、性能与诊断 / Lock-Free Boundaries, Performance, and Diagnostics》从“知道概念”推进到“能在项目里稳定使用”。 阅读时可以把每个知识点都追问成四件事:它保护什么边界,失败时有什么现象,怎样最小复现,怎样写进团队流程。
concept -> boundary -> failure signal -> minimal proof -> project rule工程切片 1:最小可复现样例
**场景。**围绕 互斥量 建一个很小的案例,不要一开始就放进完整业务系统。把问题压缩到一个可以提交给同事的目录,保留源码、构建命令、版本输出和预期现象。
**边界。**先判断这里讨论的是 互斥量、条件变量 还是 临界区 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 互斥量 -> 条件变量 -> 临界区 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 2:接口边界
**场景。**围绕 条件变量 建一个很小的案例,不要一开始就放进完整业务系统。写清调用方能依赖什么、不能依赖什么,并把隐式假设转换成命名函数、配置项或测试。
**边界。**先判断这里讨论的是 条件变量、临界区 还是 等待谓词 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 条件变量 -> 临界区 -> 等待谓词 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 3:失败注入
**场景。**围绕 临界区 建一个很小的案例,不要一开始就放进完整业务系统。故意制造一个常见错误,让日志、断言或测试先失败,再用修复后的证据说明规则生效。
**边界。**先判断这里讨论的是 临界区、等待谓词 还是 死锁 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 临界区 -> 等待谓词 -> 死锁 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 4:跨平台差异
**场景。**围绕 等待谓词 建一个很小的案例,不要一开始就放进完整业务系统。至少比较 Windows、Linux 或不同编译器下的一个差异,记录差异属于标准、实现还是平台约定。
**边界。**先判断这里讨论的是 等待谓词、死锁 还是 锁粒度 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 等待谓词 -> 死锁 -> 锁粒度 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 5:性能观察
**场景。**围绕 死锁 建一个很小的案例,不要一开始就放进完整业务系统。用小规模和压力规模各跑一次,区分算法成本、同步成本、I/O 成本和工具链配置成本。
**边界。**先判断这里讨论的是 死锁、锁粒度 还是 互斥量 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 死锁 -> 锁粒度 -> 互斥量 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 6:生命周期
**场景。**围绕 锁粒度 建一个很小的案例,不要一开始就放进完整业务系统。标出资源创建、转移、共享、停止和销毁的顺序,特别关注错误返回和提前退出路径。
**边界。**先判断这里讨论的是 锁粒度、互斥量 还是 条件变量 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 锁粒度 -> 互斥量 -> 条件变量 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 7:诊断证据
**场景。**围绕 互斥量 建一个很小的案例,不要一开始就放进完整业务系统。保留命令、日志、符号、栈、测试输出或截图,让结论可以被另一个环境重新验证。
**边界。**先判断这里讨论的是 互斥量、条件变量 还是 临界区 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 互斥量 -> 条件变量 -> 临界区 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 8:维护策略
**场景。**围绕 条件变量 建一个很小的案例,不要一开始就放进完整业务系统。把一次性经验沉淀到 README、CI、脚本、示例工程或检查表里,避免只存在个人记忆中。
**边界。**先判断这里讨论的是 条件变量、临界区 还是 等待谓词 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 条件变量 -> 临界区 -> 等待谓词 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 9:版本演进
**场景。**围绕 临界区 建一个很小的案例,不要一开始就放进完整业务系统。为未来变更预留兼容策略:版本号、特性位、弃用窗口、迁移脚本和回滚入口。
**边界。**先判断这里讨论的是 临界区、等待谓词 还是 死锁 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 临界区 -> 等待谓词 -> 死锁 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 10:最小消费者
**场景。**围绕 等待谓词 建一个很小的案例,不要一开始就放进完整业务系统。从使用者角度写一个小程序或小工程,只依赖公开接口,验证安装包、头文件和运行时行为。
**边界。**先判断这里讨论的是 等待谓词、死锁 还是 锁粒度 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 等待谓词 -> 死锁 -> 锁粒度 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 11:边界复盘
**场景。**围绕 死锁 建一个很小的案例,不要一开始就放进完整业务系统。用一张表说明问题发生在源码、编译、链接、加载、运行、关闭或发布中的哪一层。
**边界。**先判断这里讨论的是 死锁、锁粒度 还是 互斥量 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 死锁 -> 锁粒度 -> 互斥量 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 12:反例训练
**场景。**围绕 锁粒度 建一个很小的案例,不要一开始就放进完整业务系统。写出一个看似能工作但不可维护的方案,再说明它在规模、团队协作或发布时会怎样失败。
**边界。**先判断这里讨论的是 锁粒度、互斥量 还是 条件变量 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 锁粒度 -> 互斥量 -> 条件变量 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
本篇收束
掌握《无锁边界、性能与诊断 / Lock-Free Boundaries, Performance, and Diagnostics》的标志,不是记住所有名词,而是能把 互斥量、条件变量、临界区、等待谓词、死锁、锁粒度 放进一条可验证的工程链路。 先用最小样例建立判断,再用测试和脚本固定判断,最后把失败证据留给未来的自己和团队。