C++多线程知识地图
内容介绍
这组笔记的目标不是背 API,而是补上工程里真正需要的并发编程能力。
中国大学课程里常见的 C/C++ 教学通常停在语法、面向对象、STL,较少系统讲并发。但工程代码里,网络服务、后台任务、日志、缓存、批处理、爬虫、音视频、游戏、GUI 都会遇到并发。
学完这条线后,你应该能借鉴 Go 把并发任务组织清楚的思想,但用 C++ 的方式管理线程、内存、生命周期和性能。
这里的重点不是“写得像 Go”,也不是盲目追求某个语言的并发模型,而是形成 C++ 自己的并发判断力:
- 先拆清楚任务边界,再决定用不用线程。
- 先明确数据所有权,再决定共享内存还是消息传递。
- 先设计生命周期和退出路径,再写
std::thread。 - 先保证正确性,再用线程池、背压、批处理优化性能。
- 能用 C++ RAII 管好资源,就不要把并发代码写成手动收尾的状态机。
这条线会覆盖:
- 用
std::thread/std::jthread正确管理线程生命周期。 - 用
std::mutex/std::lock_guard保护共享数据。 - 用
std::condition_variable实现阻塞等待和通知。 - 用
std::future/std::promise表达异步返回值。 - 用阻塞队列降低共享状态复杂度,并实现背压。
- 用完成计数模型等待一组任务结束。
- 用线程池控制并发数量,避免线程爆炸。
- 用
std::atomic处理简单无锁状态。 - 用
std::jthread/stop_token做协作取消。
可借鉴的任务组织思想
| 思想 | C++ 落地方式 | 说明 |
|---|---|---|
| 任务先于线程 | cpp并发核心思维、cpp线程池 | 先识别任务,再选择执行资源 |
| 通信优先于到处共享 | cppChannel模型 | 用阻塞队列传任务/数据,减少共享可变状态 |
| 等待关系要显式 | cppWaitGroup模型、cppfuture和promise | 明确谁等谁、等什么结果 |
| 退出路径必须设计 | cpp协作取消 | 停止标志、关闭队列、stop_token |
| 性能来自资源控制 | cpp并发性能模型 | 线程数、任务粒度、背压、锁竞争、批处理 |
注意:C++ 标准线程是系统线程,不具备 Go runtime 那种用户态轻量调度器。C++ 的优势在于资源控制、对象生命周期、内存布局和性能边界更显式;这既更麻烦,也更适合系统级、高性能、低延迟场景。
学习路径
如果第一次看多线程觉得概念散,先读 cpp多线程入门导读。它用“任务、线程、数据、同步、退出”五个问题把后面的页面串起来。
cpp多线程入门导读
-> cpp并发与多线程
<-> cpp并发核心思维
<-> cpp并发性能模型
<-> cpp线程生命周期
<-> cpp线程传参与返回值
<-> cpp互斥锁与RAII
<-> cpp条件变量
<-> cpp原子操作
<-> cppfuture和promise
<-> cppChannel模型
<-> cppWaitGroup模型
<-> cpp线程池
<-> cpp协作取消章节导航
- cpp多线程入门导读:用任务、线程、数据、同步、退出建立总模型。
- cpp并发与多线程:先分清并发、并行、进程、线程,以及什么时候不该开线程。
- cpp并发核心思维:建立任务拆分、执行资源、数据所有权、同步通信、退出取消这条主线。
- cpp并发性能模型:建立高吞吐并发的工程指标:线程数、任务粒度、背压、锁竞争、批处理。
- cpp线程生命周期:掌握
std::thread、join、detach、std::jthread。 - cpp线程传参与返回值:解决参数复制、引用传递、生命周期、返回值收集。
- cpp互斥锁与RAII:用 RAII 管理锁,避免数据竞争和死锁。
- cpp条件变量:实现“没数据就睡眠,有数据再唤醒”的阻塞等待。
- cpp原子操作:处理计数器、标志位、停止信号等简单共享状态。
- cppfuture和promise:获取异步任务结果,理解
async、future、promise。 - cppChannel模型:用阻塞队列做任务通信和背压,减少共享状态。
- cppWaitGroup模型:用完成计数等待一组任务结束。
- cpp线程池:控制并发数量,把任务提交给固定数量工作线程。
- cpp协作取消:用
std::jthread和stop_token做可停止任务。
分阶段读法
第一阶段:先能写对
目标是知道线程怎么启动、怎么收尾、怎么安全访问共享数据。
完成标准:
- 能解释
join、detach、jthread的区别。 - 能判断一个 lambda 捕获引用是否可能悬空。
- 能说清楚“哪个 mutex 保护哪个数据”。
- 能写出带谓词的
cv.wait(lock, predicate)。
第二阶段:能组织任务
目标是把多个任务之间的等待、返回、通信和退出设计清楚。
完成标准:
- 能区分任务和线程。
- 能用
future表达异步结果。 - 能用阻塞队列组织生产者消费者。
- 能设计“关闭队列/通知等待者/等待线程退出”的完整收尾流程。
第三阶段:能控制性能
目标是知道线程不是越多越好,并能控制任务粒度、队列积压和锁竞争。
完成标准:
- 能解释线程数、核心数、任务粒度、上下文切换的关系。
- 知道
atomic适合简单状态,不适合维护复杂不变量。 - 能说明为什么大量短任务应该进线程池,而不是每个任务创建线程。
最佳代码实践
- 优先用
std::jthread,它析构时会自动请求停止并join。 - 必须共享数据时,用
std::mutex保护,并用std::lock_guard/std::unique_lock管理锁。 - 跨线程传对象前先回答:谁拥有它,谁能改它,它活到什么时候。
- 能用消息传递就不要共享可变状态,阻塞队列通常比到处加锁更容易做性能控制。
- 线程数量要受控。大量短任务交给线程池,不要每个任务都新建线程。
- 用有界队列做背压,防止生产速度超过消费能力后把内存打爆。
- CPU 密集任务的线程数通常接近核心数;IO 密集任务再根据阻塞比例增加。
- 小任务要合批,避免调度开销大于任务本身。
- 锁的作用域越小越好,不要在持锁期间做 IO、睡眠、复杂计算或调用未知外部函数。
- 条件变量必须配合谓词使用:
cv.wait(lock, predicate)。 - 原子只适合简单状态,不要误以为用了
atomic就自动线程安全。
常见错误用法
std::thread t([] {
// do work
});
// 忘记 join 或 detach,std::thread 析构时会 std::terminateif (!queue.empty()) {
auto value = queue.front();
queue.pop();
}
// 多线程下 check-then-act 不是原子操作,中间可能被其他线程改掉std::mutex m;
m.lock();
do_work();
m.unlock();
// do_work 抛异常时 unlock 不会执行注意事项
- C++ 线程是系统线程,创建成本和栈内存成本都远高于用户态轻量任务。
- 多线程 bug 往往不可稳定复现,必须从设计上减少共享状态。
- 看到“偶尔错一次”的并发 bug,不要靠加 sleep 修。
- 并发不是性能银弹。CPU 密集任务要考虑核心数,IO 密集任务要考虑阻塞点。
- 标准库并发能力足够学习和中小型工程使用,但大型服务常会引入成熟线程池、事件循环或协程框架。
- Windows 上不要使用过旧的 MinGW.org 工具链学习
std::thread。建议使用 MSVC、LLVM/Clang、MinGW-w64 或 Linux GCC,并按需要添加-pthread。