现代c++并发编程教程

接入与开发

给 C++ 程序员讲清现代并发编程里线程、锁与内存模型的选型标准、排查顺序和可落地清单,不堆概念只讲怎么用。

写多线程 C++ 时最容易卡在三处:线程该开几个、锁该锁多细、原子变量到底该用哪种内存序。搜索引擎给的答案往往是语言规范复述,落地时对不上自己的场景。这篇按「先定并发模型、再挑同步手段、最后写验证清单」的顺序给一套判断标准和排查顺序,并说清什么情况下这套做法不适用。

写多线程 C++ 最容易卡在三处:线程该开几个、锁该锁多细、原子变量到底该用哪种内存序。搜索引擎给的答案往往是语言规范复述,落地时对不上自己的场景。这篇按「先定并发模型、再挑同步手段、最后写验证清单」的顺序,给一套能直接照着走的判断标准和排查顺序。

先说明一点:并发编程没有万能模板,下面每条标准都配了不适用的边界。如果你手上任务之间有强顺序依赖,或者共享状态重到不好拆,那第一步不是优化线程数,而是先把串行版本跑稳,再谈并行——这条比后面所有技巧都重要。

先定并发模型,再谈开几个线程

线程数不是拍脑袋定的,先按任务性质分三类再决定。CPU 密集型任务的瓶颈是算力,理论上限就是硬件并发度,开再多只会增加切换开销;IO 密集型任务的瓶颈是等待,线程数应该按「等待占比」来估,而不是按核数。混合型任务建议拆成两个池分别处理计算和等待,用一个池硬扛通常两头都不讨好。

任务类型定线程数的依据什么时候不适用
CPU 密集型以硬件并发度为上限,超了几乎纯亏任务里有大段不可并行的串行段时,加线程基本无效
IO 密集型按单任务等待时长与总时长的比例估算下游本身就限流或排队时,加线程只会让队列更长
混合型拆成计算池与等待池,各自定容量任务边界切不干净、拆完反而更多同步时,不如先保持简单

定数量之前建议先量一次:把单任务的总耗时和其中纯等待时间记下来,这个比例比任何经验值都靠谱。另外,线程数远超硬件并发度后,上下文切换和缓存失效的代价会开始反噬,收益曲线是往下走的,不是线性往上。

锁的选择顺序:能不能不加 → 加多细 → 加哪种

选锁不要一上来就纠结用哪种互斥量,按三步走更省事。第一问是能不能消除共享:把状态收敛到一个线程持有,用队列或消息交接,往往比反复调锁粒度更彻底。第二问才是粒度:整表锁、分段锁、每对象锁,越细越要防死锁,统一加锁顺序并写进注释。第三问才是选原语。

  • 互斥锁:适合临界区较长、可能阻塞、逻辑清晰的场景,是最不容易写错的选择
  • 读写锁:适合读多写少,且单次读操作本身也不短的场景;读操作很短时收益不明显
  • 自旋类原语:只适合临界区极短且竞争不激烈的情况,临界区里一旦有系统调用或可能阻塞,就不该用它
  • 条件变量:它只解决「等某个状态成立」,使用时必须配谓词循环,不能只判断一次就往下走

什么情况下这套顺序不适用:如果你的临界区里要做网络或磁盘等待,任何锁都会把等待时间放大到所有竞争线程身上,这时正确的做法是把等待挪出临界区,或者干脆改成异步加队列交接,而不是换个更细的锁。

内存模型:什么时候必须显式指定内存序

默认的顺序一致性语义最直观也最贵,只在无数据竞争的原子计数、单一标志位这类场景下才考虑放宽。真正容易踩坑的是「我以为原子就够了」:多个原子变量之间的先后关系,需要靠内存序或额外同步来建立,否则你读到的可能是一组互相矛盾、部分过期的值。

无数据竞争的原子计数
可以考虑放宽内存序,先确认没有别的线程依赖它去读其他共享数据
标志位加数据
标志位置位与数据写入之间必须有明确的同步关系,否则读方可能看到标志但读到旧数据
多个原子变量的先后关系
靠各自默认语义不一定能建立,需要显式同步或统一到一把锁里
无锁结构
绝大多数业务代码用锁加原子标志位就够了,无锁队列只在明确量出锁是瓶颈后才值得引入

验证手段比理论更重要:把线程数调到超过核数、重复多轮跑,再配合竞态检测工具。单次跑通不代表没问题,竞态问题的特征是偶发——今天不复现,明天在线上复现,排查成本翻几倍。

把超时、取消与日志当成并发程序的一等公民

每个可能阻塞的等待都要有超时上限,无法取消的等待是线上卡死的常见来源。超时值按下游响应时间的分位数来设,不要照抄库的默认值;默认值通常偏保守或者偏激进,都不适合你的链路。

  1. 给所有阻塞点加超时逐个检查等待点是否都有上限,并确认超时后线程能真正退出,而不是卡在下一层等待里
  2. 区分可重试与不可重试超时和临时不可用可以退避重试,参数错误与逻辑错误重试只会放大问题,要直接失败并记录
  3. 日志固定记四样线程标识、任务标识、进入与退出时间、失败原因,缺一样都会让排查变成猜谜
  4. 按顺序排查先看死锁或长阻塞,再看共享状态是否无保护访问,最后才怀疑内存序写错

这套做法在什么情况下不适用:如果你的程序是短生命周期的批处理,跑完就退出,那么重试策略和取消逻辑可以大幅简化,把精力放在结果正确性上更划算。反过来,常驻服务类程序,上面每一条都建议做到。

如果你想把并发相关的工具链和排查工具放在一起看,站内工具目录里有对应的分类,可以按需挑选。

下一步把本文的排查顺序抄下来贴在工位上,再顺手对照站内资料库核对当前主流并发库与工具的分类,比零散搜答案成体系。

常见问题

现代 C++ 并发编程,线程池该自己写还是用现成库?

先看需求边界。只是固定并发度的任务分发,用成熟库或标准库设施更稳,自己写容易在异常路径、任务取消和退出时机上出错。只有当你有明确的调度策略(优先级、亲和性、动态扩缩)时,自研才划算。无论哪种,都要给任务加上超时和失败可观测性,否则出问题一样难查。

锁和原子变量,业务代码里到底优先用哪个?

优先用锁。锁的语义直观、易读易审,只要临界区不长时间阻塞就不会是瓶颈。原子变量更适合单个标志位、计数器这类无复合逻辑的场景。多个原子变量凑出一段业务逻辑,本质上是在手写锁,出错概率高很多。真怀疑锁是瓶颈,先测再改,不要凭感觉把锁换成原子。

学并发编程,怎么判断自己写的东西是对的?

靠重复和压力,不靠单次跑通。把线程数调到超过硬件并发度、反复多轮运行,再配合竞态检测类工具跑一遍;给等待路径加上超时,观察是否出现偶发卡住。能稳定复现问题的测试比一次通过的测试有价值。具体工具和验证口径可以先在 /tools 里对照本机环境再选。

本文依据各平台公开文档与官方公告整理,只做中文归纳;具体做法、参数与限制以官方文档为准。