第一季讲完语言特性,第二季进入并发与工程。第一课是 C++11 起就有的四件套:std::threadmutexcondition_variableatomic——创建并行任务、保护共享数据、线程间等待通知、无锁计数。

这一篇对应 demo:ref/cpp_demo/concurrency/thread_demos/(含 thread 基础、mutex、条件变量、atomic、线程安全队列)。

这是「现代 C++ 实战」系列的第 12 篇,第二季开篇。建议先读 第 11 篇:Concepts

一、为什么需要多线程?

  • 利用多核:CPU 密集型任务拆到多个核并行
  • 重叠 I/O 与计算:一个线程等磁盘/网络,另一个继续算
  • 响应性:UI 线程不阻塞于后台任务

C++ 标准库从 C++11 起提供跨平台线程抽象,不必直接写 pthread(Linux)或 Win32 API。

二、std::thread:创建、join、detach

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <thread>
#include <iostream>

void worker(int id) {
std::cout << "thread " << id << '\n';
}

int main() {
std::thread t1(worker, 1); // 函数 + 参数
std::thread t2([]{ std::cout << "lambda\n"; });

t1.join(); // 等待 t1 结束
t2.join();

// std::thread t3(worker, 3);
// t3.detach(); // 后台运行,不再 join;主线程退出前必须保证 detach 线程已结束或仍存活
}
操作 含义
join() 阻塞直到线程函数返回
detach() 分离,线程独立运行;不可再 join
析构 若既未 join 也未 detach → std::terminate

传参注意:默认按值拷贝;传引用用 std::ref(x);移动大对象用 std::move

1
std::thread t(worker, std::ref(shared_data));

三、数据竞争与 std::mutex

多线程同时读写同一变量且至少一个是写 → 数据竞争(data race) → 未定义行为。

1
2
3
4
5
6
7
8
9
10
11
#include <mutex>

int counter = 0;
std::mutex mtx;

void increment() {
for (int i = 0; i < 100000; ++i) {
std::lock_guard lock(mtx); // C++17 类模板参数推导
++counter;
}
}

互斥锁 std::mutex:同一时刻只有一个线程持有锁。

四、RAII 锁:lock_guardunique_lock

类型 特点
lock_guard 构造加锁、析构解锁;简单场景
unique_lock 可 defer_lock、try_lock、配合 condition_variable
1
2
3
4
5
6
7
{
std::lock_guard<std::mutex> lock(mtx);
// 临界区
} // 自动 unlock,异常安全

std::unique_lock<std::mutex> lock(mtx);
// 可 lock.unlock() 提前释放,再 lock.lock()

永远用 RAII 管理锁——不要手写 lock() / unlock() 配对,异常路径容易漏解锁。

五、condition_variable:生产者-消费者

线程需要等待某个条件(队列非空、任务完成),而不是忙等(burn CPU):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <condition_variable>
#include <queue>

std::mutex mtx;
std::condition_variable cv;
std::queue<int> q;
bool done = false;

void consumer() {
std::unique_lock lock(mtx);
cv.wait(lock, []{ return !q.empty() || done; });
// 被 notify 唤醒后,已重新持有 lock
while (!q.empty()) {
int v = q.front(); q.pop();
lock.unlock();
process(v);
lock.lock();
}
}

void producer() {
{
std::lock_guard lock(mtx);
q.push(42);
}
cv.notify_one(); // 唤醒一个等待线程
}

标准模式wait(lock, predicate) 防止虚假唤醒;修改共享状态时在锁内,通知在锁外亦可。

demo:thread_condition_variable.cppthread_safe_queue.cpp

六、std::atomic:无锁计数器

简单整型增减,可用原子操作避免锁:

1
2
3
4
5
6
7
8
#include <atomic>

std::atomic<int> counter{0};

void increment_atomic() {
for (int i = 0; i < 100000; ++i)
++counter; // 原子读-改-写
}
mutex atomic
适用 保护复杂临界区、多变量不变式 单个整型/指针的简单操作
开销 可能系统调用 通常 CPU 原子指令
误用 死锁 只保护一个变量,复合操作仍需 care

memory_order(relaxed / acquire / release)进阶见 C++ 内存模型;默认 seq_cst 最安全。

七、常见并发 Bug

问题 表现 缓解
数据竞争 结果随机、崩溃 mutex / atomic
死锁 两线程互相等锁 固定加锁顺序、try_lock、超时
忘记 join terminate join 或 detach;C++20 jthread 自动 join(第 13 篇)
伪共享 多核缓存行抖动 对齐、padding(高性能场景)

八、demo 导览

ref/cpp_demo/concurrency/thread_demos/ 通常包含:

文件 主题
thread_demo.cpp thread 创建与 join
thread_sync.cpp mutex 与数据竞争对比
thread_condition_variable.cpp 条件变量
thread_atomic.cpp atomic vs mutex
thread_safe_queue.cpp 线程安全队列
1
2
cd ref/cpp_demo/concurrency/thread_demos
./build.sh --run

链接时需 -pthread(Linux)或 CMake find_package(Threads)

九、小结

组件 用途
std::thread 启动并行任务,join/detach
mutex + RAII 锁 互斥访问共享数据
condition_variable 等待条件、生产者-消费者
atomic 无锁简单计数/标志

现代 C++ 实战系列第 12 篇完。下一篇 C++20 同步原语——jthread、semaphore、latch、barrier。

系列导航

篇号 标题 状态
11 Concepts 与模板进阶
12 多线程基础(本篇)
13 C++20 同步原语 下一篇

完整大纲见工作区 docs/CPP_SERIES_OUTLINE.md