Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

多线程与并发

C++11 起,标准库提供了完整的并发支持:线程、互斥量、条件变量、原子操作和异步任务。这些设施定义在 <thread>、<mutex>、<condition_variable>、<atomic>、<future> 等头文件中,让并发程序不再依赖平台特定的 API。

但并发编程的困难不在于 API,而在于正确性。数据竞争、死锁、虚假唤醒、内存序错误这些问题往往在测试中难以复现,却会在生产环境造成严重后果。更麻烦的是,很多错误不会立即崩溃,而是表现为「偶尔算错一个数」——这种 bug 的排查成本极高。

Warning

并发问题的本质

只要多个线程同时访问同一块内存,且其中至少一个是写操作,且没有同步措施,就构成数据竞争(data race)——这是未定义行为,编译器可以做任何假设,结果完全不可预测。

加锁或使用原子操作是消除数据竞争的唯一途径。

Note

加锁了也不一定安全

数据竞争只是并发问题的一类。还有一类更隐蔽的问题叫 TOCTOU(检查时刻到使用时刻):即使每一处访问都加了锁,只要「检查」和「使用」被拆到两个临界区,中间的状态变化仍会导致错误。详见互斥量与锁。

本章内容

章节说明
线程基础std::thread 与 std::jthread、线程局部存储、启动与结束
互斥量与锁五种互斥量类型、RAII 包装器、死锁避免、TOCTOU、call_once
条件变量等待与通知、虚假唤醒、生产者消费者模式
原子操作std::atomic、CAS 循环、内存序、无锁编程的限度
异步任务回调 / Future / 协程三种风格、async、promise、packaged_task
并发模式与陷阱线程池、回调与事件驱动、常见模式与错误汇总

怎么选同步手段

面对一个并发问题,先想清楚「线程之间需要交换什么」,再选工具:

需求工具说明
保护一小段临界区std::mutex + lock_guard最常用,优先考虑
保护多个资源,需要一起加锁std::scoped_lock自动避免死锁
读多写少std::shared_mutex + shared_lock允许多个读者并发
等待某个条件成立std::condition_variable配合互斥量使用
单个变量的原子读写std::atomic比加锁轻量
需要「稍后取结果」std::future配合 async / promise
等待一批任务完成std::latch / std::barrierC++20
限制并发数量std::counting_semaphoreC++20
请求取消长时间任务std::stop_tokenC++20,配合 jthread

默认从互斥量开始。原子操作和无锁结构看起来更快,但正确性极难保证,除非有明确的性能需求,否则不要轻易尝试。

几条经验

  • 优先用 std::jthread。它会在析构时自动 join,不会因为忘记 join 而让程序终止,还内置了取消机制。
  • 不要手动 lock() / unlock()。始终用 RAII 包装器,异常路径才安全。
  • 共享数据越少越好。最有效的并发优化是减少共享——用消息传递代替共享内存,或者让每个线程持有自己的数据。
  • 先保证正确,再考虑性能。seq_cst 最慢但最安全,确认瓶颈后再降级内存序。
  • 并发 bug 靠工具找。用 ThreadSanitizer(-fsanitize=thread)而非肉眼审查。

与内存模型的分工

本章讲怎么用这些并发设施,C++ 内存模型 讲为什么它们能工作——happens-before 关系、内存序的语义、编译器与 CPU 的重排规则。

如果只是想写出正确的并发代码,本章足够;如果需要理解底层机制或做无锁编程,建议两章对照阅读。