多线程编程:从单核到多核的进化之路
说实话,我刚入行那会儿,C++ 的多线程还是个「野路子」——要么用操作系统 API,要么靠第三方库。直到 C++11 正式把 std::thread、std::mutex 这些东西纳入标准库,我才觉得:嗯,这才像个现代语言的样子。
这一章,咱们就聊聊 C++ 多线程编程的核心武器。我会结合自己踩过的坑,把 std::thread、std::mutex、条件变量、std::async 这些家伙讲明白。
1. std::thread:开启新战场
创建线程,说白了就是告诉 CPU:「嘿,这段代码你帮我另开一条流水线跑。」
#include <thread>
#include <iostream>
void worker(int id) {
std::cout << "线程 " << id << " 开始干活\n";
}
int main() {
std::thread t1(worker, 1);
std::thread t2(worker, 2);
t1.join(); // 等 t1 结束
t2.join(); // 等 t2 结束
return 0;
}
这里有个细节:join() 是阻塞等待。如果你不调用 join() 或 detach(),程序退出时会直接 terminate()。我曾经在项目里漏掉一个 join(),结果线上服务莫名其妙崩溃——排查了半天才发现是线程没回收。
join() 或 detach()。否则程序会异常终止。
2. std::mutex 与 std::lock_guard:锁住共享资源
多线程最怕什么?数据竞争。两个线程同时写一个变量,结果就乱套了。我见过一个经典 bug:两个线程同时更新一个计数器,最后结果比预期少了 30%。
解决方案?加锁。
#include <mutex>
#include <thread>
std::mutex mtx;
int counter = 0;
void increment() {
std::lock_guard<std::mutex> lock(mtx); // 自动加锁,离开作用域自动解锁
++counter;
}
std::lock_guard 是个好东西——它遵循 RAII 原则,构造时加锁,析构时解锁。你不用担心忘记 unlock,也不会因为异常导致死锁。
std::lock_guard 就别手动 lock()/unlock()。少写代码,少犯错。
3. 条件变量:让线程学会等待
有时候,线程需要等某个条件成立再继续。比如生产者-消费者模型:消费者得等生产者放好东西才能取。
条件变量就是干这个的。
#include <condition_variable>
#include <mutex>
#include <queue>
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 ...
}
注意这里用的是 std::unique_lock 而不是 lock_guard。为什么?因为 wait() 内部需要临时解锁,再重新加锁——unique_lock 支持这种操作,lock_guard 不行。
wait() 必须配合 unique_lock,并且最好用带谓词的重载版本,避免虚假唤醒。
4. std::async 与 std::future:异步任务的优雅写法
说实话,手动管理线程有时候挺烦的。你想想看,我就想异步执行一个函数,拿个返回值,还得自己搞线程、传参、处理异常……
std::async 就是来解放你的。
#include <future>
#include <iostream>
int compute(int x) {
return x * x;
}
int main() {
std::future<int> result = std::async(std::launch::async, compute, 10);
std::cout << "结果: " << result.get() << std::endl;
return 0;
}
std::async 会自动选择一个线程(或者在当前线程同步执行)来运行任务。std::future 则用来获取异步结果。get() 会阻塞直到结果就绪。
我个人习惯用 std::launch::async 强制异步执行,避免某些实现偷懒用同步方式。
5. 线程安全与数据竞争:避坑指南
数据竞争是未定义行为。编译器优化、CPU 乱序执行、缓存不一致……这些因素会让你的程序行为完全不可预测。
我曾经遇到过一个诡异的 bug:一个 bool 标志位,主线程设置它为 true,工作线程检查它。结果工作线程永远看不到 true。为什么?因为编译器把那个变量的读取优化掉了——它觉得这个变量没被修改过。
解决方案:用 std::atomic 或者加锁。
std::atomic<bool> ready{false};
void worker() {
while (!ready.load()) {
// 忙等待
}
// 干活...
}
void set_ready() {
ready.store(true);
}
std::atomic 或互斥量。
知识体系总览
下面这张图帮你理清本章的核心脉络:
总结一下
多线程编程,说白了就是「分工 + 协作」。分工靠 std::thread 和 std::async,协作靠 std::mutex、条件变量和 std::atomic。
我最后给你三个建议:
- 能用
std::async就别手动创建线程——少操心,少出错。 - 加锁范围尽量小——锁住关键代码就行,别锁整个函数。
- 多写测试——多线程 bug 很难复现,但一旦出现就是大问题。
嗯,这一章就到这里。记住:多线程不是魔法,是工程。你踩过的坑越多,写出来的代码就越稳。