第八章:并发编程基础:std::thread与线程管理、互斥量与锁、条件变量、死锁预防
并发编程,说白了就是让程序同时干多件事。我刚开始接触多线程时,觉得不就是开几个线程跑任务嘛,结果线上程序三天两头崩溃,查得我头皮发麻。后来才明白——并发编程的核心不是“怎么开线程”,而是“怎么让线程安全地协作”。
8.1 std::thread:线程的创建与管理
C++11 引入了 std::thread,终于不用再跟平台 API 打交道了。我个人习惯用 lambda 表达式创建线程,代码更紧凑,上下文也更清晰。
#include <thread>
#include <iostream>
void worker(int id) {
std::cout << "线程 " << id << " 开始工作\n";
}
int main() {
std::thread t1(worker, 1);
std::thread t2([]{
std::cout << "lambda 线程\n";
});
t1.join(); // 等待线程结束
t2.join();
return 0;
}
嗯,这里要注意:join() 和 detach() 必须二选一。如果线程对象析构时还没决定,程序会直接 terminate。我曾经在项目里漏掉一个分支的 join,结果压测时偶发崩溃,查了两天才定位到。
8.2 std::mutex:互斥量的正确用法
多个线程同时读写同一份数据,就会出问题。你想想看,两个线程同时执行 counter++,这个操作不是原子的——它分“读-改-写”三步。如果时间片切得巧,结果就少加了一次。
互斥量就是用来解决这个问题的。我习惯用 std::lock_guard,因为它 RAII 风格,出了作用域自动解锁,不容易忘。
#include <mutex>
std::mutex mtx;
int counter = 0;
void safe_increment() {
std::lock_guard<std::mutex> lock(mtx);
++counter; // 临界区
}
8.3 std::lock_guard 与 std::unique_lock
std::lock_guard 简单粗暴,构造时加锁,析构时解锁。但如果你需要更灵活的控制——比如手动解锁、延迟加锁、与条件变量配合——那就得上 std::unique_lock。
| 特性 | lock_guard | unique_lock |
|---|---|---|
| 加锁/解锁时机 | 构造/析构 | 可手动控制 |
| 支持条件变量 | 否 | 是 |
| 性能开销 | 极低 | 略高(内部维护状态) |
| 推荐场景 | 简单临界区 | 需要灵活控制 |
我的建议是:默认用 lock_guard,只有需要条件变量或手动解锁时才换 unique_lock。别为了“万一以后要用”而提前上 unique_lock,性能虽差不大,但代码意图会模糊。
8.4 std::condition_variable:线程间的信号机制
条件变量解决的是“等待某个条件成立”的问题。比如生产者-消费者模型:消费者要等队列里有数据才能取,不能空转轮询。
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
std::queue<int> data_queue;
void consumer() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return !data_queue.empty(); });
int val = data_queue.front();
data_queue.pop();
// 处理 val
}
注意 wait() 的第二个参数——谓词。这是防止“虚假唤醒”的关键。我曾经踩过这个坑:没有谓词,结果线程被唤醒了但条件其实不满足,直接读取空队列,崩溃。
8.5 死锁预防:四个必要条件与应对策略
死锁是并发编程里最让人头疼的问题之一。我记得有一次线上服务突然 hang 住,所有请求都不响应了。上去一看,两个线程互相等着对方释放锁——典型的死锁。
死锁发生的四个必要条件:
- 互斥:资源一次只能被一个线程占用
- 持有并等待:线程拿着一个锁,又去等另一个锁
- 不可剥夺:锁只能由持有者主动释放
- 循环等待:线程之间形成等待环路
预防策略其实就围绕打破这些条件:
- 固定加锁顺序:所有线程按相同顺序获取锁。比如先锁 A 再锁 B,就不会出现环路
- 使用 std::lock():一次性锁住多个互斥量,避免分步加锁
- 使用 scoped_lock:C++17 引入,等价于 lock_guard 但支持多个锁
- 避免嵌套锁:能用一个锁就别用两个。我见过有人为了“性能优化”把一把锁拆成三把,结果死锁频发
// 使用 std::lock 避免死锁
std::mutex mtx1, mtx2;
void safe_operation() {
std::lock(mtx1, mtx2); // 一次性锁住
std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
// 临界区
}
8.6 本章知识体系图
下面这张图梳理了本章的核心脉络,从线程创建到同步机制,再到死锁预防,一目了然。
并发编程没有银弹。工具就这些——thread、mutex、condition_variable——但怎么组合、怎么设计,才是真正的功力所在。我个人觉得,多线程代码写得好不好,不在于用了多高级的特性,而在于能不能让每个线程都“知道自己该干什么,什么时候该等,什么时候该走”。