并发性能模型
内容介绍
想用 C++ 写出高性能并发,关键不是“线程开得多”,而是让有限的系统线程持续做有效工作。
Go 的并发体验来自运行时调度器、轻量任务、网络轮询器和阻塞管理。C++ 标准库没有这些运行时能力,所以工程上要靠设计补齐:
- 用线程池复用系统线程。
- 用任务队列把任务和线程解耦。
- 用有界队列做背压。
- 用合理任务粒度减少调度开销。
- 用局部数据、批处理、分片锁减少竞争。
- 用协作取消和关闭协议避免线程卡死。
性能指标
吞吐量
单位时间完成多少任务。线程池、批处理、减少锁竞争主要提升吞吐。
延迟
单个任务从提交到完成的时间。队列过长、锁等待、线程饥饿都会增加延迟。
资源占用
包括线程数、栈内存、队列内存、CPU 使用率、上下文切换次数。
可预测性
工程并发不只看平均值,还要看长尾延迟。偶发锁竞争、队列堆积、阻塞扩散会让尾延迟很差。
线程数量
CPU 密集任务:
线程数 ≈ CPU 核心数IO 密集任务:
线程数可以多于核心数,但必须受控不要用“任务数 = 线程数”的模型。大量任务应该进入 cpp线程池 的队列,由固定数量工作线程执行。
任务粒度
任务太大:
- 单个任务占用线程太久。
- 负载不均衡。
- 取消不及时。
任务太小:
- 入队、出队、唤醒、调度成本超过业务计算。
- 锁竞争变多。
- 吞吐下降。
最佳实践是把任务切到“足够大,值得调度;足够小,便于负载均衡”的粒度。
背压
没有背压的队列会把性能问题变成内存问题。
// 危险倾向:生产者无限 push,消费者处理不过来
while (true) {
queue.push(make_task());
}工程上应使用有界队列:
- 队列满时阻塞生产者。
- 队列满时丢弃低优先级任务。
- 队列满时返回失败,让上游降速。
背压是高性能服务稳定运行的核心。
减少锁竞争
常用方法:
- 缩小锁作用域。
- 持锁期间不做 IO。
- 每个线程先写局部结果,最后合并。
- 用分片锁代替全局锁。
- 读多写少场景考虑
std::shared_mutex。 - 简单计数器使用
std::atomic。
批处理
批处理能显著减少锁和调度成本。
单条处理:
lock -> pop 1 个任务 -> unlock -> 执行
批处理:
lock -> pop 一批任务 -> unlock -> 连续执行适合日志、消息消费、数据清洗、批量计算等场景。
最佳代码实践
- 默认从线程池开始,而不是手写大量
std::thread。 - 明确任务是 CPU 密集还是 IO 密集,再设置线程数。
- 队列要有容量上限或降级策略。
- 优先通过队列传递任务,减少多个线程直接改同一个对象。
- 用局部累积加最终合并,替代每次操作都抢全局锁。
- 性能优化要测量:吞吐、延迟、CPU、上下文切换、队列长度。
常见错误用法
for (int i = 0; i < 100000; ++i) {
std::thread([i] {
do_small_work(i);
}).detach();
}问题:创建 10 万个系统线程会导致内存、调度、上下文切换成本失控。
std::lock_guard<std::mutex> lock(m);
write_log_to_disk();问题:持锁做慢 IO,会让其他线程被慢设备拖住。
注意事项
- C++ 标准库不会替你调度海量轻量任务,必须自己控制线程数。
- 性能瓶颈常常不是计算,而是锁竞争、队列堆积、内存分配、上下文切换。
- 并发性能优化必须用数据说话,不要凭感觉加线程。
- 想进一步接近高性能服务模型,需要继续学习事件循环、异步 IO、协程和成熟线程池实现。