Wiki
Concurrent programming
Existing English translations were reused. 4 current source blocks remain in Simplified Chinese, without triggering paid translation.
C++ Starting from 11, the standard library officially provides concurrency tools such as threads, mutexes, condition variables, atomic operations, and asynchronous tasks. By C++17, C++20, it further added std::scoped_lock、std::shared_mutex、std::jthread、std::stop_token、std::latch、std::barrier、std::semaphore and other capabilities.
The truly difficult part of concurrent programming is not "creating a thread", but the following issues:
- When does a thread start and when does it end, and who is responsible for waiting for it?
- Are the parameters received by the thread function copies, references, or pointers? Could the object be destroyed early?
- When multiple threads access the same data simultaneously, how can data races be avoided?
- When multiple locks are used at the same time, how can deadlock be avoided?
- When a thread has no work to do, how can it wait efficiently instead of looping continuously and consuming CPU?
- When a task will produce a result in the future, how can its return value or exception be obtained?
- When the program exits, how can background threads be stopped safely?
This chapter proceeds step by step according to these questions.
1.Concurrency and Parallelism
| Concept | Meaning | Typical case |
|---|---|---|
| Concurrency | Multiple tasks make progress during the same period of time | A single-core CPU switches between multiple threads; a program waits for multiple IOs at the same time |
| Parallelism | Multiple tasks truly execute at the same instant | Multiple threads compute simultaneously on a multi-core CPU |
Concurrency is a way of organizing a program, whereas parallelism is an actual execution state. A concurrent program is not necessarily parallel, but parallel execution is usually built on concurrent tasks.
2.Thread safety first requires understanding: data races
If two threads access the same memory location at the same time, and at least one thread is writing, and those accesses are not properly synchronized with each other, a data race may occur.
In the C++ memory model, a data race usually implies undefined behavior , not simply "the final result occasionally misses a few additions." The compiler can perform many seemingly counterintuitive optimizations under the premise of undefined behavior.
For example:
int counter = 0;
void work()
{
++counter;
}
++counter is not equivalent to "a naturally indivisible single operation." It usually includes reading, computing, and writing back. When multiple threads execute interleaved, updates may be lost.
Common solutions include:
- Use
std::mutexto protect a piece of shared state; - For simple independent variables, use
std::atomic; - Minimize shared mutable state and let each thread work on its own data;
- When you need to wait for a condition, use
std::condition_variableinstead of continuously polling.
3.Learning Path
This section is divided into the following subsections:
| 顺序 | 章节 | 重点 |
|---|---|---|
| 1 | std::thread |
线程创建、可调用对象、参数传递、join()、detach()、joinable()、移动语义、线程生命周期 |
| 2 | mutex 与 RAII 锁 |
mutex、lock_guard、unique_lock、scoped_lock、死锁、读写锁、一次初始化 |
| 3 | std::atomic |
load/store、原子读改写、CAS、内存序基础、C++20 wait/notify |
| 4 | condition_variable |
等待/通知、谓词、虚假唤醒、超时等待、生产者消费者 |
| 5 | future / async / promise |
异步任务返回值、异常传播、packaged_task、shared_future |
| 6 | jthread / stop_token |
C++20 自动 join、协作式停止、线程退出设计 |
| 7 | semaphore / latch / barrier |
C++20 许可计数、一次性会合、可重复阶段同步 |
It is recommended to study strictly in order. Subsequent chapters will assume that the preceding lifecycle, RAII, and synchronization concepts have already been understood.
4.Overview of Common Concurrency Utilities
| 工具 | 标准 | 作用 | 头文件 |
|---|---|---|---|
std::thread |
C++11 | 创建和管理线程 | <thread> |
std::mutex |
C++11 | 互斥访问共享状态 | <mutex> |
std::lock_guard |
C++11 | 简单 RAII 加锁 | <mutex> |
std::unique_lock |
C++11 | 可手动解锁、延迟加锁、可移动的 RAII 锁 | <mutex> |
std::scoped_lock |
C++17 | 同时管理一把或多把锁 | <mutex> |
std::shared_mutex |
C++17 | 多读单写 | <shared_mutex> |
std::condition_variable |
C++11 | 等待共享条件变化 | <condition_variable> |
std::atomic<T> |
C++11 | 对单个对象执行原子操作 | <atomic> |
std::future |
C++11 | 接收未来产生的结果 | <future> |
std::async |
C++11 | 以任务形式异步执行函数 | <future> |
std::promise |
C++11 | 主动向 future 写入结果或异常 |
<future> |
std::packaged_task |
C++11 | 把可调用对象包装成可产生 future 的任务 |
<future> |
std::jthread |
C++20 | 析构时自动请求停止并等待线程结束 | <thread> |
std::stop_token |
C++20 | 协作式停止请求 | <stop_token> |
std::counting_semaphore |
C++20 | 计数信号量 | <semaphore> |
std::latch |
C++20 | 一次性计数同步点 | <latch> |
std::barrier |
C++20 | 可重复阶段同步点 | <barrier> |
The latter three C++20 synchronization tools are not as commonly used in ordinary business code as the preceding ones, so you should first build a solid understanding of the core concurrency model, and then use them according to actual needs.
5.Compile
GCC / Clang on Linux typically need to link pthread when using the standard thread library:
g++ demo.cpp -std=c++23 -pthread -O2 -Wall -Wextra
The examples in this chapter uniformly use the C++23 <print>, std::print() and std::println() to write terminal output; the standard versions of the concurrency tools themselves are still subject to the explanations in each section.
-pthreadIt is not just a simple matter of adding a library; it may also affect thread-related macros and options during the compilation phase, so it should usually be used directly with -pthread.
6.Basic principles of concurrent code
- 先保证正确,再讨论性能。 一个存在数据竞争的程序即使“跑起来很快”也没有意义。
- 优先减少共享状态。 不共享就不需要同步。
- 锁要由 RAII 对象管理。 除非确有必要,不要手写成对的
lock()/unlock()。 - 锁保护的是不变量,不只是某一行代码。 如果几个成员必须保持一致,应由同一把锁共同保护。
- 不要持锁执行耗时操作。 网络、文件 IO、休眠、大量计算通常应放在临界区外。
- 不要轻易
detach()。 脱离线程很容易制造生命周期问题。 - 线程停止应该有明确协议。 C++20 优先了解
std::jthread+std::stop_token。 atomic不等于“永远无锁”。 它保证操作的原子语义,但具体实现是否 lock-free 与类型和平台有关。
7.Recommended Proficiency Level
After studying this chapter, you should at least be able to answer:
std::thread t(f, a, b)中第一个参数和后面的参数分别是什么?- 为什么给
T&形参传线程参数时常需要std::ref()? joinable()到底判断“线程是否还在运行”,还是判断“线程对象是否仍关联执行线程”?- 为什么一个已经执行完函数的
std::thread仍可能joinable() == true? join()和detach()对线程对象状态有什么影响?- 为什么 joinable 的
std::thread析构会std::terminate()? lock_guard、unique_lock、scoped_lock分别适合什么场景?- 两把互斥量为什么会产生死锁,怎样避免?
atomic与mutex应该怎样选择?- 为什么条件变量必须围绕“共享状态 + mutex + 谓词”使用?
future和promise分别站在结果通道的哪一端?jthread相比thread解决了哪些生命周期问题?semaphore、latch、barrier分别对应什么同步模式?
Only if these questions can be answered clearly can one be considered to have truly mastered the fundamentals of standard library concurrency.