C++ 开发手册C++

CHAPTER 05抽象层 · CONCURRENCY

并发编程

thread、mutex、atomic、future、condition_variable。用标准库写出正确、无数据竞争的并发程序。

开始阅读难度高阶先修第四、六章约3小时
05.1

线程基础

std::thread 启动一个执行流。线程函数结束时必须 join(等待)或 detach(分离),否则析构会 std::terminate。

#include <thread>
void worker(int id) { /* ... */ }
std::thread t(worker, 1);
t.join();                 // 等待线程结束
RAII 管理线程

用 std::jthread(C++20)或封装 RAII 包装,构造启动、析构自动 join,避免忘记 join 导致崩溃。

05.2

数据竞争

多个线程无同步地读写同一变量即数据竞争,结果是未定义行为。必须用互斥或原子同步。

int counter = 0;
// 两个线程同时 counter++ —— 危险!
// 实际是 读-改-写 三步,会丢失更新
用 TSAN

编译加 -fsanitize=thread 用 ThreadSanitizer 自动检测数据竞争,比肉眼排查可靠得多。

05.3

互斥与锁

std::mutex 配合 std::lock_guard(RAII 锁)保护临界区,离开作用域自动解锁,异常安全。

std::mutex m;
int counter = 0;
void inc() {
  std::lock_guard<std::mutex> lk(m);  // 构造加锁
  ++counter;                          // 临界区
}                                      // 析构自动解锁
锁粒度要小

只把必须互斥的代码放进临界区,锁内别做 I/O 或耗时操作,否则严重降低并发度。

05.4

原子操作

std::atomic 提供无锁的原子读写(如计数器),比互斥轻量。适合简单共享变量,复杂结构仍需锁。

#include <atomic>
std::atomic<int> counter{0};
counter.fetch_add(1, std::memory_order_relaxed);  // 原子自增
05.5

条件变量

std::condition_variable 用于"等待某条件成立",配合 mutex 与 wait/notify 实现线程间协作(如生产者-消费者)。

std::condition_variable cv;
std::mutex m; bool ready = false;
// 等待方
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, []{ return ready; });   // 原子释放锁并等待
// 通知方
{ std::lock_guard<std::mutex> lk(m); ready = true; }
cv.notify_one();
总是用谓词等待

用带谓词的 wait(lk, pred) 防止虚假唤醒导致误判条件。

05.6

future 与 async

std::async 启动后台任务并返回 std::future,get() 获取结果(自动等待)。比手动管线程更上层、更易组合。

auto f = std::async(std::launch::async, []{
  return heavy_compute();
});
int r = f.get();        // 阻塞直到结果就绪
05.7

线程安全设计

最好的并发是"不共享":用消息传递(线程各持数据、通过队列通信)或不可变数据,远比加锁省心。必须共享时用清晰的 ownership。

// 每线程独立结果,最后汇总,避免共享
std::vector<std::future<int>> fs;
for (int i = 0; i < 4; ++i)
  fs.push_back(std::async([i]{ return compute(i); }));
05.8

常见陷阱与 FAQ

忘记 join 会怎样?
std::thread 析构时若仍 joinable,程序会 terminate。用 jthread 或在所有路径显式 join/detach。

死锁怎么产生的?

两个线程各自持有一把锁、又等对方的锁。用 std::lock 一次性锁多把,或固定加锁顺序避免。

atomic 一定无锁吗?

不一定。is_lock_free() 可查;大多数基础类型无锁,但某些平台/大小可能回退到内部锁。

05.9

实战练习与资源

延伸资源