Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← C++ 编程 / C++ Programming

并发编程 / Concurrency

1. C++ 并发路线 / C++ Concurrency Roadmap

2. 线程、任务与生命周期 / Threads, Tasks, and Lifetimes

3. C++ 内存模型与原子操作 / The C++ Memory Model and Atomics

4. 锁、条件变量与并发不变量 / Locks, Condition Variables, and Invariants

5. Future、Promise 与 Async / Futures, Promises, and Async

6. 线程池、背压与协作取消 / Thread Pools, Backpressure, and Cancellation

7. 无锁边界、性能与诊断 / Lock-Free Boundaries, Performance, and Diagnostics

8. C++ 并发综合复习 / C++ Concurrency Review

本页目录

线程、任务与生命周期 / Threads, Tasks, and Lifetimes ​

1. 线程对象不等于线程所有工作 ​

std::thread 是系统执行线程的可移动句柄。构造成功后它处于 joinable 状态,直到 join()、detach() 或被移动。joinable 的 thread 析构会调用 std::terminate,避免后台线程悄悄访问已销毁状态。

text
default thread ----> not joinable
construct ----------> joinable ----join----> not joinable
                         |
                         +--detach--> not joinable(执行仍可能继续)
                         +--move----> 新对象 joinable,源对象 not joinable
1
2
3
4
5

2. 创建与参数传递 ​

cpp
void increment(int& value) { ++value; }

int value = 0;
std::thread worker(increment, std::ref(value));
worker.join();
1
2
3
4
5

线程构造通常先衰减复制/移动函数和参数,再在新线程中调用。要传真实引用需 std::ref,但必须保证被引用对象存活并同步访问。传裸指针和捕获引用有同样风险。

成员函数需要对象:std::thread(&Worker::run, &object, arg)。若对象先销毁,线程稍后解引用即 UAF。更稳妥的是让拥有者在析构前请求停止并 join。

3. join 与 detach ​

  • join 阻塞当前线程,直到目标线程结束,并建立完成同步;
  • detach 只切断句柄关系,不延长任何捕获对象生命周期,也没有后续 join 点;
  • 对非 joinable thread 调用 join/detach 会抛 system_error;
  • 线程不能 join 自己。

业务代码应极少使用 detach。真正的后台服务应由进程级/对象级运行时拥有,具有停止、错误上报和关闭等待,而不是“丢掉 thread 对象”。

4. 异常边界 ​

线程入口函数的异常若逃逸,会调用 std::terminate。异常不会自动跨线程传播:

cpp
std::exception_ptr error;
std::thread worker([&] {
    try { run(); }
    catch (...) { error = std::current_exception(); }
});
worker.join();
if (error) { std::rethrow_exception(error); }
1
2
3
4
5
6
7

示例中 error 的访问由 join 建立顺序。更常见的是用 promise/future 或任务系统传递结果和异常。

5. RAII 管理 ​

C++17 可写 joining-thread 包装器;析构 join 能避免 terminate,但也可能在不可预期位置长时间阻塞。接口仍需说明停止机制和析构阻塞策略。

C++20 std::jthread 析构时先请求停止再 join:

cpp
std::jthread worker([](std::stop_token token) {
    while (!token.stop_requested()) {
        do_small_piece();
    }
});
1
2
3
4
5

停止请求是协作式状态,不会中断任意系统调用,也不会强杀线程。循环和阻塞操作必须提供可取消点与唤醒路径。

6. stop_token 协议 ​

text
stop_source.request_stop()
          |
          +--> token.stop_requested() 变 true
          +--> 已注册 stop_callback 同步执行

工作线程:观察请求 -> 维护不变量 -> 清理 -> 正常返回
1
2
3
4
5
6

回调可能在发起停止的线程执行,注册/注销与请求存在竞争;回调不能假定运行在线程池工作线程,也应避免长时间阻塞和复杂锁反转。

7. 任务粒度与硬件提示 ​

hardware_concurrency() 可能返回 0,只是逻辑并发度提示,不表示可用 CPU 配额、NUMA 拓扑或最佳线程数。CPU 密集工作线程数通常接近实际可用核心;阻塞 I/O 任务需要结合等待比例和资源上限测量。

任务过细会让排队、唤醒和同步成本超过计算;任务过粗会负载不均、取消延迟变大。用真实吞吐与 p95/p99 延迟选择粒度。

8. 线程局部存储 ​

thread_local 对每个线程产生独立对象,初始化和析构发生在线程生命周期相关时点。它可减少共享,却有隐患:

  • 线程池线程长期存在,缓存也长期占内存;
  • 动态库卸载与析构顺序复杂;
  • 任务在线程间迁移时,thread-local 不是 task-local;
  • 隐式上下文降低测试性。

9. 常见错误 ​

  1. detach 后捕获栈引用;
  2. 忘记 join 导致 thread 析构 terminate;
  3. 认为线程函数异常会在 join 时自动抛出;
  4. 用 sleep 等待线程“应该已经完成”;
  5. 只设置 stop flag,却无法唤醒阻塞在条件变量/I/O 的线程;
  6. 在 GUI/事件线程析构 joining thread,造成界面长期冻结。

10. 面试速答 ​

问:join 与 detach 区别?

join 等待执行结束并建立同步;detach 仅放弃句柄,线程继续且无法再 join,所有捕获生命周期需另行保证。

问:为什么 thread 析构时 joinable 会 terminate?

标准拒绝隐式 detach 或无条件阻塞的模糊语义,迫使所有者明确决定 join/detach。

问:jthread 是否能强制终止线程?

不能,它通过 stop_token 发出协作请求,线程代码必须观察并安全退出。

11. 自测题与答案 ​

  1. 为什么 std::ref 既必要又危险?
  2. join 除等待外还带来什么内存模型意义?
  3. stop request 为什么必须配合阻塞唤醒?

<details> <summary>参考答案</summary>

  1. thread 默认复制参数;ref 能保留引用语义,但被引用对象必须存活且访问同步。
  2. 线程完成同步于成功 join 返回,使该线程此前的写对 join 后代码可见。
  3. 线程若睡在无法观察 token 的等待中,就不会执行检查和退出。

</details>

返回并发路线

12. 线程启动会复制哪些东西 ​

std::thread 构造通常衰减复制 callable 与参数到内部存储,再由新线程调用。要传引用需显式 std::ref,但这只表达引用,不延长对象生命:

text
creator stack local --------+
std::thread stores reference |
creator scope ends ----------+--> worker later accesses dangling
1
2
3

默认按值/移动传入任务状态更易证明;借用必须由 join 或更外层所有者约束。

13. join 是生命周期屏障 ​

text
main:   start worker | other work | join waits | destroy shared state
worker:              |---- uses shared state ----| exit
1
2

成功 join 后,线程完成 synchronizes-with join 返回,且线程不再执行。它同时解决执行完成和资源生命问题。detach 失去等待与错误传播句柄,进程退出还可能直接终止后台工作,应只用于拥有独立进程级生命周期协议的极少场景。

14. jthread 改进的是结构化关闭 ​

std::jthread 析构会请求停止并 join,避免普通 thread 析构仍 joinable 时 terminate:

cpp
std::jthread worker([](std::stop_token token) {
    while (!token.stop_requested()) do_one_unit();
});
1
2
3

停止是协作请求,不会强杀线程。阻塞 I/O、锁等待或长计算必须设置可取消等待点或定期检查 token。

15. 线程入口是异常边界 ​

异常若逃出线程函数会调用 terminate,不会自动传播给创建线程。入口应 catch 并通过 promise、错误队列或任务框架共享状态报告:

text
worker throws -> catch at thread entry -> store exception_ptr -> signal result
caller waits/get -> rethrow in caller context
1
2

16. 线程对象移动不移动正在运行的栈 ​

移动 std::thread 只是转移关联线程的句柄/责任;OS 线程继续执行原入口。被移动源变为 non-joinable,目标负责 join/detach。

17. TLS 的任务污染 ​

线程池复用线程,thread_local 状态不会在任务结束自动重置:

text
worker T
 task A sets request_id=A
 task B starts on same T -> sees stale A unless protocol clears
1
2
3

请求级上下文更适合显式参数或 RAII reset guard;TLS 适合真正线程级缓存/运行库状态。

18. 粒度与 oversubscription ​

线程数远大于可运行核心会增加切换与缓存扰动;但 I/O 阻塞工作可需要多于核心数的并发。hardware_concurrency() 只是提示,可能为 0,也不反映配额、NUMA 与其他进程负载。

text
CPU-bound: workers 约可用核心,任务足够大
I/O-bound : 并发由等待比例、连接数、背压决定
1
2

19. 线程生命周期清单 ​

  1. callable/参数是值还是借用?
  2. 每个线程必经 join/detach 哪条路径?
  3. 异常如何传回?
  4. 停止能打断阻塞点吗?
  5. 谁先销毁:线程还是共享状态?
  6. TLS 是否跨任务泄漏?
  7. 线程数与任务粒度有测量吗?

20. 连续追问与自测 ​

问:thread 析构会自动 join 吗? 答:普通 std::thread 不会;若仍 joinable 会 terminate。jthread 才提供请求停止并 join 的析构行为。

问:detach 为什么危险? 答:失去等待、错误传播和生命周期协调,后台线程容易访问已销毁状态。

  1. std::ref 是否保活对象? 答:不保活,只让参数以引用语义传递。
  2. stop_token 是否强制终止? 答:不是,工作必须协作观察并退出。
  3. 移动 thread 后谁 join? 答:获得关联线程的目标对象。

9. 真实问题:服务关闭时偶发崩溃 ​

假设服务收到停止信号后销毁 Worker 对象。 Worker 的线程入口捕获了 this。 如果析构只释放成员而没有等待线程,线程下一次访问成员就是悬空访问。 这种故障常在压测或机器关机时出现。 正常运行几千次并不能证明它不存在。 真正的问题不是“线程是否启动成功”。 问题是执行权和对象所有权没有绑定。 每个线程都必须有明确的拥有者。 拥有者负责启动、传递停止意图、等待完成和收集错误。 调用方不能只保存一个裸函数指针。

10. 直觉模型:把线程当作借用资源 ​

可以把 std::thread 看成文件描述符一样的资源句柄。 构造后句柄指向一个仍在运行或即将运行的执行实体。 join 类似关闭前等待资源完成。 detach 类似把资源交给系统,但应用不再拥有完成通知。 移动构造转移的是句柄所有权,不是复制线程。 因此源对象移动后不再 joinable。 对象析构只检查句柄状态,不会猜测业务意图。 普通 thread 析构时仍 joinable 会终止进程。 这是故意的强制失败,避免静默 UAF。 jthread 增加了停止请求,但停止仍然需要入口配合。 停止请求不是异步杀线程。

11. 最小实验:观察 joinable 状态 ​

cpp
#include <iostream>
#include <thread>

int main() {
    std::thread first([] { std::cout << "run\\n"; });
    std::cout << first.joinable() << "\\n";
    std::thread second = std::move(first);
    std::cout << first.joinable() << " " << second.joinable() << "\\n";
    second.join();
    std::cout << second.joinable() << "\\n";
}
1
2
3
4
5
6
7
8
9
10
11

第一次输出通常是 1。 移动后通常输出 0 1。 等待完成后输出 0。 这里的输出顺序只用于观察句柄状态。 不要用 cout 的先后推断线程内部执行顺序。 把实验改为 detach 后,主线程可能先结束。 进程结束会回收剩余线程,不能当作正常关闭协议。 把 join 放进异常路径测试可以发现资源泄漏。 工程代码应使用 RAII 守卫保证每个退出路径都处理句柄。

12. 参数和生命周期实验 ​

cpp
void use(std::string const& text) {
    std::cout << text.size() << "\\n";
}

void demo() {
    std::string value = "payload";
    std::thread safe(use, value);
    safe.join();
    std::thread unsafe([&] { use(value); });
    unsafe.join();
}
1
2
3
4
5
6
7
8
9
10
11

第一个线程获得参数副本,副本存活到调用完成。 第二个线程借用局部变量,只有在 join 先于作用域结束时才安全。 如果把 unsafe 移到异步队列,原作用域可能先返回。 捕获引用并不会延长对象寿命。 捕获 this 也不会延长拥有对象寿命。 用值捕获可以延长副本寿命,但副本可能不是最新状态。 用 shared_ptr 捕获可延长共享对象寿命,但会改变所有权和析构时机。 推荐先描述“谁拥有状态”,再选择参数形式。 不要以“编译器允许捕获”作为安全证明。

13. 正式规则:线程启动同步点 ​

线程构造完成后,新线程会执行入口。 传给构造函数的可调用对象和参数会被复制或移动到内部存储。 这些副本的初始化发生在新线程调用前。 调用方在构造前准备好的值可以通过参数副本安全交付。 共享引用仍需额外同步。 join 返回时,目标线程的执行已经结束。 目标线程在 join 前完成的副作用对 join 后线程可见。 detach 没有对应的等待点。 分离线程与主对象之间不能依赖析构顺序。 标准不保证线程调度顺序。 标准也不保证两个线程的 cout 输出不交错。

14. 异常和错误传播边界 ​

入口函数抛出的异常不能穿过线程边界。 若没有捕获,库会调用 std::terminate。 主线程的 try/catch 捕获不到工作线程异常。 需要在入口捕获 ... 并保存 exception_ptr。 保存错误本身也要有同步协议。 最小做法是入口捕获后 join,再在拥有者线程重抛。 任务接口更适合使用 promise 传播值和异常。 线程启动失败通常以 system_error 在构造点报告。 启动失败时已创建的线程仍需按既定协议清理。 不要在析构函数中抛出线程错误。 析构阶段应记录错误并让显式关闭接口负责报告。

15. jthread 与协作停止 ​

jthread 析构会请求停止,然后等待线程结束。 如果线程没有观察 token,析构仍可能永久阻塞。 循环任务应在每次小工作后检查 stop_requested()。 阻塞队列需要把停止请求接入条件变量唤醒。 系统调用阻塞时,token 不会自动打断系统调用。 网络任务通常需要关闭 socket 或设置超时来唤醒。 文件读取可能需要独立的取消句柄。 停止回调执行位置由请求线程决定。 回调不应拿与工作线程相反顺序的锁。 停止是状态协议,不是强制抢占。 关闭设计必须明确“请求停止后最长多久返回”。

16. 工程实践:结构化关闭清单 ​

先禁止新任务进入,再请求现有任务停止。 唤醒所有可能睡眠的工作线程。 等待工作线程退出并收集错误。 最后销毁共享状态和底层资源。 关闭顺序必须与启动顺序相反或由依赖图决定。 日志记录线程身份、任务标识和关闭阶段。 为 join 设置可观测的耗时指标。 不要在信号处理器里直接 join。 信号处理器只写入安全标志,再由控制线程执行关闭。 GUI 主线程关闭时,避免等待持有 UI 锁的后台线程。 线程池拥有线程,业务对象不应随意 detach。

17. 失败场景复盘 ​

失败一:析构中忘记 join,发布版本随机 terminate。 修复:封装不可复制的 joining owner,并在测试中触发异常退出。 失败二:detach 线程捕获局部引用,偶发读取乱码。 修复:改为值捕获或共享所有权,并提供显式停止。 失败三:停止后线程卡在无限阻塞读取。 修复:为 I/O 增加超时、关闭唤醒或可取消等待。 失败四:多个线程同时写日志,日志行互相穿插。 修复:使用串行日志队列,不把 cout 当同步器。 失败五:移动 thread 后仍对源对象调用 join。 修复:移动后检查 joinable,只由目标对象负责等待。 失败六:异常路径跳过线程清理。 修复:让守卫对象承担 join,并将异常延迟到 join 后重抛。

18. 自测题 ​

问:detach 后如何知道线程何时结束? 答:标准句柄不再提供完成点,必须自己建立 future、计数或服务生命周期协议。 问:值捕获一定正确吗? 答:只解决副本寿命,不解决副本与共享真实状态的一致性。 问:jthread 能否立即终止死循环? 答:不能,循环必须检查 token 或被外部唤醒。 问:线程函数是否可以返回结果? 答:返回值被忽略,使用 promise/future 或共享结果对象传递。 问:join 是否意味着所有共享数据都安全? 答:只建立线程完成后的同步,线程运行期间仍需保护共享访问。 问:多个 join 调用是否幂等? 答:不是,第二次对非 joinable 线程调用 join 会报错。 问:线程池工作线程能否保存调用者引用? 答:只有调用者保证任务完成前对象存活才行,更稳妥是值捕获或所有权句柄。 问:关闭接口为什么不能只设置一个原子 bool? 答:原子标志不能自动唤醒阻塞线程,也不能表达队列排空和资源释放阶段。

工程深化:把本篇知识落到项目里 ​

这一节不是为了凑篇幅,而是把《线程、任务与生命周期 / Threads, Tasks, and Lifetimes》从“知道概念”推进到“能在项目里稳定使用”。 阅读时可以把每个知识点都追问成四件事:它保护什么边界,失败时有什么现象,怎样最小复现,怎样写进团队流程。

text
concept -> boundary -> failure signal -> minimal proof -> project rule
1

工程切片 1:最小可复现样例 ​

**场景。**围绕 线程对象 建一个很小的案例,不要一开始就放进完整业务系统。把问题压缩到一个可以提交给同事的目录,保留源码、构建命令、版本输出和预期现象。

**边界。**先判断这里讨论的是 线程对象、join/detach 还是 共享状态 的责任;如果三者混在一起,先把输入、输出和所有权拆开。

text
input -> 线程对象 -> join/detach -> 共享状态 -> observable result
1

**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。

**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。

**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?

工程切片 2:接口边界 ​

**场景。**围绕 join/detach 建一个很小的案例,不要一开始就放进完整业务系统。写清调用方能依赖什么、不能依赖什么,并把隐式假设转换成命名函数、配置项或测试。

**边界。**先判断这里讨论的是 join/detach、共享状态 还是 生命周期 的责任;如果三者混在一起,先把输入、输出和所有权拆开。

text
input -> join/detach -> 共享状态 -> 生命周期 -> observable result
1

**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。

**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。

**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?

工程切片 3:失败注入 ​

**场景。**围绕 共享状态 建一个很小的案例,不要一开始就放进完整业务系统。故意制造一个常见错误,让日志、断言或测试先失败,再用修复后的证据说明规则生效。

**边界。**先判断这里讨论的是 共享状态、生命周期 还是 异常边界 的责任;如果三者混在一起,先把输入、输出和所有权拆开。

text
input -> 共享状态 -> 生命周期 -> 异常边界 -> observable result
1

**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。

**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。

**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?

工程切片 4:跨平台差异 ​

**场景。**围绕 生命周期 建一个很小的案例,不要一开始就放进完整业务系统。至少比较 Windows、Linux 或不同编译器下的一个差异,记录差异属于标准、实现还是平台约定。

**边界。**先判断这里讨论的是 生命周期、异常边界 还是 调试日志 的责任;如果三者混在一起,先把输入、输出和所有权拆开。

text
input -> 生命周期 -> 异常边界 -> 调试日志 -> observable result
1

**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。

**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。

**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?

工程切片 5:性能观察 ​

**场景。**围绕 异常边界 建一个很小的案例,不要一开始就放进完整业务系统。用小规模和压力规模各跑一次,区分算法成本、同步成本、I/O 成本和工具链配置成本。

**边界。**先判断这里讨论的是 异常边界、调试日志 还是 线程对象 的责任;如果三者混在一起,先把输入、输出和所有权拆开。

text
input -> 异常边界 -> 调试日志 -> 线程对象 -> observable result
1

**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。

**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。

**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?

工程切片 6:生命周期 ​

**场景。**围绕 调试日志 建一个很小的案例,不要一开始就放进完整业务系统。标出资源创建、转移、共享、停止和销毁的顺序,特别关注错误返回和提前退出路径。

**边界。**先判断这里讨论的是 调试日志、线程对象 还是 join/detach 的责任;如果三者混在一起,先把输入、输出和所有权拆开。

text
input -> 调试日志 -> 线程对象 -> join/detach -> observable result
1

**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。

**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。

**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?

工程切片 7:诊断证据 ​

**场景。**围绕 线程对象 建一个很小的案例,不要一开始就放进完整业务系统。保留命令、日志、符号、栈、测试输出或截图,让结论可以被另一个环境重新验证。

**边界。**先判断这里讨论的是 线程对象、join/detach 还是 共享状态 的责任;如果三者混在一起,先把输入、输出和所有权拆开。

text
input -> 线程对象 -> join/detach -> 共享状态 -> observable result
1

**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。

**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。

**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?

工程切片 8:维护策略 ​

**场景。**围绕 join/detach 建一个很小的案例,不要一开始就放进完整业务系统。把一次性经验沉淀到 README、CI、脚本、示例工程或检查表里,避免只存在个人记忆中。

**边界。**先判断这里讨论的是 join/detach、共享状态 还是 生命周期 的责任;如果三者混在一起,先把输入、输出和所有权拆开。

text
input -> join/detach -> 共享状态 -> 生命周期 -> observable result
1

**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。

**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。

**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?

工程切片 9:版本演进 ​

**场景。**围绕 共享状态 建一个很小的案例,不要一开始就放进完整业务系统。为未来变更预留兼容策略:版本号、特性位、弃用窗口、迁移脚本和回滚入口。

**边界。**先判断这里讨论的是 共享状态、生命周期 还是 异常边界 的责任;如果三者混在一起,先把输入、输出和所有权拆开。

text
input -> 共享状态 -> 生命周期 -> 异常边界 -> observable result
1

**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。

**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。

**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?

本篇收束 ​

掌握《线程、任务与生命周期 / Threads, Tasks, and Lifetimes》的标志,不是记住所有名词,而是能把 线程对象、join/detach、共享状态、生命周期、异常边界、调试日志 放进一条可验证的工程链路。 先用最小样例建立判断,再用测试和脚本固定判断,最后把失败证据留给未来的自己和团队。

最后更新于:

Pager
上一篇1. C++ 并发路线 / C++ Concurrency Roadmap
下一篇3. C++ 内存模型与原子操作 / The C++ Memory Model and Atomics

持续记录,持续成长

Copyright © Tidenflow