并发与多线程
内容介绍
并发编程解决的是“多个任务在同一段时间内推进”的问题。
- 并发:多个任务交替推进,不一定同时执行。
- 并行:多个任务在多个 CPU 核心上真正同时执行。
- 进程:操作系统分配资源的基本单位。
- 线程:进程内的执行流,共享进程资源。
C++ 多线程的核心难点不是创建线程,而是共享数据、生命周期、同步和退出。
先用生活模型理解
可以把一个程序想成一家小店:
- 任务:要完成的事情,例如接单、做菜、收银、清理。
- 线程:员工,真正执行任务的人。
- 共享数据:订单本、库存表、收银台余额。
- 锁:同一时间只允许一个人改订单本。
- 条件变量:没有新订单时厨师休息,有订单时前台叫醒厨师。
- 退出:打烊时通知所有员工停止接新活,做完手头工作后下班。
多线程代码也是这个结构。不要一开始就想“我要开几个线程”,先问:
我有哪些任务?
哪些任务可以同时推进?
哪些数据会被多个任务访问?
谁负责通知谁?
程序结束时这些线程怎么停?从单线程到多线程
单线程写法通常是按顺序执行:
读输入 -> 处理数据 -> 写结果多线程适合把等待或可并行的部分拆出来:
主线程:响应用户 / 接收请求
后台线程:写日志 / 存文件 / 清理缓存
工作线程:处理计算任务但拆线程会带来新成本:
- 线程创建和切换有成本。
- 共享数据要同步。
- 执行顺序不再固定。
- 错误可能偶发,难复现。
- 程序退出时必须等待或通知线程。
所以并发的第一原则是:先证明值得并发,再引入线程。
什么时候需要多线程
- 任务之间相对独立,可以并行计算。
- 主线程不能被阻塞,例如 GUI、游戏主循环、服务主循环。
- 有明显 IO 等待,例如网络、磁盘、数据库、日志写入。
- 需要后台任务,例如定时清理、监控、异步保存。
可以按下面方式判断:
| 场景 | 是否适合多线程 | 原因 |
|---|---|---|
| 大量图片批量压缩 | 适合 | 每张图片相对独立,CPU 可以并行工作 |
| GUI 点击按钮后保存大文件 | 适合 | 保存文件不能卡住界面 |
| 日志写入磁盘 | 适合 | 主逻辑不必等待慢 IO |
| 一个 vector 里做很少量计算 | 通常不适合 | 线程开销可能比计算更大 |
| 多个线程频繁修改同一个复杂对象 | 通常不适合 | 锁竞争高,正确性难保证 |
什么时候不该用多线程
- 数据高度共享,线程之间总是互相等待。
- 任务很短,但频繁创建销毁线程。
- 单线程已经足够快,瓶颈不在 CPU 或 IO。
- 只是为了“看起来高级”。
一个实用判断:如果你不能清楚说出“哪些数据是共享的,哪个锁保护它,线程如何退出”,先不要写多线程。
第一个分岔:共享内存还是消息传递
多线程协作大致有两种风格。
共享内存
多个线程访问同一份对象:
线程 A -> shared_data
线程 B -> shared_data这种方式需要锁或原子操作。适合共享状态少、访问规则清楚的场景。
继续看:cpp互斥锁与RAII、cpp原子操作
消息传递
线程之间通过队列传任务或数据:
生产者 -> 队列 -> 消费者这种方式更容易维护,因为共享状态集中在队列里。
继续看:cpp条件变量、cppChannel模型
第二个分岔:一次性异步还是长期工作线程
如果只是“开一个任务,之后拿结果”,优先考虑 future:
auto result = std::async(std::launch::async, compute);
auto value = result.get();如果是长期后台工作,例如日志线程、任务队列消费者、线程池 worker,就要设计:
- 工作循环什么时候等待。
- 有任务时怎么唤醒。
- 没任务时是否睡眠。
- 程序退出时怎么通知停止。
- 谁负责
join。
最佳代码实践
- 先写清楚任务边界,再决定线程边界。
- 优先让线程之间传消息,而不是共享大量可变对象。
- 一个线程负责一类职责,例如消费队列、写日志、处理连接。
- 线程退出路径要和启动路径一样明确。
- 并发设计先保证正确,再讨论性能。
- 性能目标是让有限线程持续做有效工作,而不是把线程数量堆上去。
常见错误用法
// 错误倾向:每个小任务都创建一个线程
for (int i = 0; i < 10000; ++i) {
std::thread([i] {
// very small task
}).detach();
}问题:线程创建成本高,数量失控会拖垮系统。大量小任务应该交给 cpp线程池。
注意事项
- 线程共享同一个进程地址空间,所以修改同一份数据必须同步。
- 多线程程序的输出顺序通常不稳定,这是正常现象。
sleep只能模拟等待,不能作为可靠同步手段。- C++ 标准库多线程从 C++11 开始,建议至少按 C++17 学,能用 C++20 更好。
- 看任何多线程示例时,先标出:线程、共享数据、锁、等待条件、退出条件。
学习路径
- 上一节:cpp多线程入门导读
- 当前阶段:cpp多线程
- 下一节:cpp并发核心思维