C++ 并发路线 / C++ Concurrency Roadmap
并发代码首先要证明“哪些访问被排序”,之后才讨论吞吐。CPU 核心多、程序偶尔运行正确或变量加了 volatile,都不是同步证明。
线程生命周期
|
v
共享状态 ----互斥路径----> mutex + condition_variable
|
+------原子路径------> atomic + memory order + 回收协议
|
+------任务路径------> future/promise + pool + cancellation
所有路径最终都要回答:happens-before、所有权、关闭和异常阅读路线
版本边界
| 设施 | 标准 |
|---|---|
thread/mutex/atomic/future | C++11 |
shared_mutex | C++17 |
jthread/stop_token/semaphore/latch/barrier/atomic::wait | C++20 |
首要定义
- race condition(竞态条件):结果依赖事件交错,概念较宽,可能有锁也可能是业务竞态;
- data race(数据竞争):两个潜在并发的冲突内存访问,至少一个写,且不是原子访问也没有 happens-before;在 C++ 中导致 UB;
- thread-safe:必须说明哪些操作可由哪些线程并发调用,不能只说“内部用了锁”;
- lock-free:系统整体持续前进,不代表每个线程无饥饿,也不代表更快。
正确性检查表
- 共享对象由谁拥有,是否活到所有线程结束?
- 每个可变字段由哪个锁/原子协议保护?
- 发布数据的写和读取之间如何形成 happens-before?
- 阻塞等待如何被完成、取消和关闭唤醒?
- 线程入口异常如何传播,析构是否可能永久阻塞?
- 队列是否有界,过载时谁承担背压?
- 性能结论是否包含吞吐、尾延迟和真实负载?
面试总答法
被问“mutex 和 atomic 怎么选”时,先说共享不变量是否跨多个字段。单一独立状态且能给出内存序证明时可用 atomic;多个字段需一致更新、阻塞等待或复杂临界区时优先 mutex。不要以“atomic 更快”作为选择依据。
自测
- 数据竞争与业务竞态有什么区别?
- 为什么原子 flag 不会自动保护任意普通数据?
- 为什么线程池必须定义关闭协议?
<details> <summary>参考答案</summary>
- 数据竞争是 C++ 内存模型定义的 UB;业务竞态是更宽泛的时序逻辑错误,完全可能发生在无 data race 的加锁程序中。
- 只有通过正确 release/acquire 等关系发布时,其他普通写入才得到可见性排序。
- 否则无法确定是否继续接收、是否排空、如何唤醒阻塞线程,以及资源何时可销毁。
</details>
统一心智模型:多个执行者如何观察同一状态
并发问题不从 API 开始,而从三个要素开始:
执行者:thread / task / coroutine / process
状态:局部、线程封闭、共享可变、不可变
关系:谁先发生、谁等待谁、谁负责停止若所有状态按值传入任务且不共享,问题主要是调度与聚合;一旦多个执行者访问同一可变对象,就必须建立同步与生命周期协议。
并发、并行与异步
并发:多个工作在时间上重叠推进,是程序结构
并行:多个工作在同一时刻真正执行,是硬件执行
异步:发起者不原地等待结果,是控制流接口单核可通过切换并发但不并行;多核可并行;异步 I/O 在等待设备期间可能没有 CPU 工作。三者不能互作同义词。
从源码到硬件的层次
C++ thread/task
|
C++ memory model: data race / happens-before / atomic
|
runtime + OS scheduler: runnable / blocked / wakeup
|
CPU cores + cache coherence + memory systemC++ 内存模型给出可移植可观察语义;cache coherence 是常见硬件机制,但不能用“最终会刷缓存”替代 happens-before 证明。
正确性四张图
所有权图
Service owns ThreadPool
ThreadPool owns workers/queue
Task owns captured values
raw references borrow external objects等待图
T1 holds A -> waits B
T2 holds B -> waits A => cycle/deadlock发布订阅图
producer writes data
producer release-store ready
consumer acquire-load ready
consumer reads data => happens-before关闭状态机
Running -> StopRequested -> Draining/Cancelling -> Joined -> Destroyed同步工具选择
复合不变量/多字段事务 -> mutex
等待“状态变为真” -> condition_variable / semaphore / atomic wait
一次初始化 -> call_once / local static
单个独立计数或标志 -> atomic(仍需内存序证明)
结果一次交付 -> promise/future
批量任务执行 -> thread pool + bounded queue原子变量不是“更快 mutex”。mutex 能把多步状态转换组成临界区,atomic 只保证规定的单次/复合原子操作,并要求更细的排序推理。
性能与正确性的顺序
先消除 data race / deadlock / lifetime bug
-> 测量队列、锁等待、上下文切换、缓存未命中
-> 减少共享、缩短临界区、调整粒度
-> 必要时研究 lock-free错误程序的高吞吐没有意义;无锁也可能因 CAS 重试和缓存行争用比锁更慢。
综合面试与自测
问:互斥锁解决什么? 答:它既提供排他访问,也建立解锁到后续成功加锁的同步关系,用于保护一组共享不变量。
问:data race 与 race condition 区别? 答:data race 是语言层未同步冲突访问并导致未定义行为;race condition 是结果依赖时序的更广逻辑问题,原子程序也可能有。
- 单核有并发吗? 答:可以,通过时间片/事件交错推进。
- atomic 能保护两个字段一致性吗? 答:各自原子不自动形成跨字段事务,通常需锁或版本化协议。
- 关闭为什么必须 join? 答:确保工作不再访问即将销毁的队列、回调和服务。
工程深化:把本篇知识落到项目里
这一节不是为了凑篇幅,而是把《C++ 并发路线 / C++ Concurrency Roadmap》从“知道概念”推进到“能在项目里稳定使用”。 阅读时可以把每个知识点都追问成四件事:它保护什么边界,失败时有什么现象,怎样最小复现,怎样写进团队流程。
concept -> boundary -> failure signal -> minimal proof -> project rule工程切片 1:最小可复现样例
**场景。**围绕 线程对象 建一个很小的案例,不要一开始就放进完整业务系统。把问题压缩到一个可以提交给同事的目录,保留源码、构建命令、版本输出和预期现象。
**边界。**先判断这里讨论的是 线程对象、join/detach 还是 共享状态 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 线程对象 -> join/detach -> 共享状态 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 2:接口边界
**场景。**围绕 join/detach 建一个很小的案例,不要一开始就放进完整业务系统。写清调用方能依赖什么、不能依赖什么,并把隐式假设转换成命名函数、配置项或测试。
**边界。**先判断这里讨论的是 join/detach、共享状态 还是 生命周期 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> join/detach -> 共享状态 -> 生命周期 -> 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:生命周期
**场景。**围绕 调试日志 建一个很小的案例,不要一开始就放进完整业务系统。标出资源创建、转移、共享、停止和销毁的顺序,特别关注错误返回和提前退出路径。
**边界。**先判断这里讨论的是 调试日志、线程对象 还是 join/detach 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 调试日志 -> 线程对象 -> join/detach -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 7:诊断证据
**场景。**围绕 线程对象 建一个很小的案例,不要一开始就放进完整业务系统。保留命令、日志、符号、栈、测试输出或截图,让结论可以被另一个环境重新验证。
**边界。**先判断这里讨论的是 线程对象、join/detach 还是 共享状态 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 线程对象 -> join/detach -> 共享状态 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 8:维护策略
**场景。**围绕 join/detach 建一个很小的案例,不要一开始就放进完整业务系统。把一次性经验沉淀到 README、CI、脚本、示例工程或检查表里,避免只存在个人记忆中。
**边界。**先判断这里讨论的是 join/detach、共享状态 还是 生命周期 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> join/detach -> 共享状态 -> 生命周期 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 9:版本演进
**场景。**围绕 共享状态 建一个很小的案例,不要一开始就放进完整业务系统。为未来变更预留兼容策略:版本号、特性位、弃用窗口、迁移脚本和回滚入口。
**边界。**先判断这里讨论的是 共享状态、生命周期 还是 异常边界 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 共享状态 -> 生命周期 -> 异常边界 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 10:最小消费者
**场景。**围绕 生命周期 建一个很小的案例,不要一开始就放进完整业务系统。从使用者角度写一个小程序或小工程,只依赖公开接口,验证安装包、头文件和运行时行为。
**边界。**先判断这里讨论的是 生命周期、异常边界 还是 调试日志 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 生命周期 -> 异常边界 -> 调试日志 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 11:边界复盘
**场景。**围绕 异常边界 建一个很小的案例,不要一开始就放进完整业务系统。用一张表说明问题发生在源码、编译、链接、加载、运行、关闭或发布中的哪一层。
**边界。**先判断这里讨论的是 异常边界、调试日志 还是 线程对象 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 异常边界 -> 调试日志 -> 线程对象 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 12:反例训练
**场景。**围绕 调试日志 建一个很小的案例,不要一开始就放进完整业务系统。写出一个看似能工作但不可维护的方案,再说明它在规模、团队协作或发布时会怎样失败。
**边界。**先判断这里讨论的是 调试日志、线程对象 还是 join/detach 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 调试日志 -> 线程对象 -> join/detach -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 13:最小可复现样例
**场景。**围绕 线程对象 建一个很小的案例,不要一开始就放进完整业务系统。把问题压缩到一个可以提交给同事的目录,保留源码、构建命令、版本输出和预期现象。
**边界。**先判断这里讨论的是 线程对象、join/detach 还是 共享状态 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 线程对象 -> join/detach -> 共享状态 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 14:接口边界
**场景。**围绕 join/detach 建一个很小的案例,不要一开始就放进完整业务系统。写清调用方能依赖什么、不能依赖什么,并把隐式假设转换成命名函数、配置项或测试。
**边界。**先判断这里讨论的是 join/detach、共享状态 还是 生命周期 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> join/detach -> 共享状态 -> 生命周期 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 15:失败注入
**场景。**围绕 共享状态 建一个很小的案例,不要一开始就放进完整业务系统。故意制造一个常见错误,让日志、断言或测试先失败,再用修复后的证据说明规则生效。
**边界。**先判断这里讨论的是 共享状态、生命周期 还是 异常边界 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 共享状态 -> 生命周期 -> 异常边界 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 16:跨平台差异
**场景。**围绕 生命周期 建一个很小的案例,不要一开始就放进完整业务系统。至少比较 Windows、Linux 或不同编译器下的一个差异,记录差异属于标准、实现还是平台约定。
**边界。**先判断这里讨论的是 生命周期、异常边界 还是 调试日志 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 生命周期 -> 异常边界 -> 调试日志 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 17:性能观察
**场景。**围绕 异常边界 建一个很小的案例,不要一开始就放进完整业务系统。用小规模和压力规模各跑一次,区分算法成本、同步成本、I/O 成本和工具链配置成本。
**边界。**先判断这里讨论的是 异常边界、调试日志 还是 线程对象 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 异常边界 -> 调试日志 -> 线程对象 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 18:生命周期
**场景。**围绕 调试日志 建一个很小的案例,不要一开始就放进完整业务系统。标出资源创建、转移、共享、停止和销毁的顺序,特别关注错误返回和提前退出路径。
**边界。**先判断这里讨论的是 调试日志、线程对象 还是 join/detach 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 调试日志 -> 线程对象 -> join/detach -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 19:诊断证据
**场景。**围绕 线程对象 建一个很小的案例,不要一开始就放进完整业务系统。保留命令、日志、符号、栈、测试输出或截图,让结论可以被另一个环境重新验证。
**边界。**先判断这里讨论的是 线程对象、join/detach 还是 共享状态 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 线程对象 -> join/detach -> 共享状态 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 20:维护策略
**场景。**围绕 join/detach 建一个很小的案例,不要一开始就放进完整业务系统。把一次性经验沉淀到 README、CI、脚本、示例工程或检查表里,避免只存在个人记忆中。
**边界。**先判断这里讨论的是 join/detach、共享状态 还是 生命周期 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> join/detach -> 共享状态 -> 生命周期 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 21:版本演进
**场景。**围绕 共享状态 建一个很小的案例,不要一开始就放进完整业务系统。为未来变更预留兼容策略:版本号、特性位、弃用窗口、迁移脚本和回滚入口。
**边界。**先判断这里讨论的是 共享状态、生命周期 还是 异常边界 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 共享状态 -> 生命周期 -> 异常边界 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 22:最小消费者
**场景。**围绕 生命周期 建一个很小的案例,不要一开始就放进完整业务系统。从使用者角度写一个小程序或小工程,只依赖公开接口,验证安装包、头文件和运行时行为。
**边界。**先判断这里讨论的是 生命周期、异常边界 还是 调试日志 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 生命周期 -> 异常边界 -> 调试日志 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
本篇收束
掌握《C++ 并发路线 / C++ Concurrency Roadmap》的标志,不是记住所有名词,而是能把 线程对象、join/detach、共享状态、生命周期、异常边界、调试日志 放进一条可验证的工程链路。 先用最小样例建立判断,再用测试和脚本固定判断,最后把失败证据留给未来的自己和团队。