9
并发与多线程
std::thread · Mutex · Future
多核 CPU 摆在那儿,能并行就别排队。C++11 开始标准库自带线程支持,不用再依赖平台 API。核心就三件事:开线程、抢锁、等结果。难点不在开线程,而在多个线程同时改同一份数据时怎么不打架。
开线程:std::thread
起一个线程跑任务,再 join 等它结束
#include <thread>
#include <iostream>
void worker(int id) {
std::cout << "线程 " << id << " 在干活\n";
}
int main() {
std::thread t1(worker, 1); // 开一个线程跑 worker(1)
std::thread t2(worker, 2);
t1.join(); // 等 t1 跑完再继续
t2.join();
} // 忘了 join 或 detach,程序直接终止
互斥锁:防止数据竞争
两个线程同时 count++,结果可能少加——这叫数据竞争(race condition)。解决办法是加锁:同一时刻只让一个线程进临界区。用 lock_guard 这种 RAII 锁,出作用域自动解锁,绝不会忘。
mutex + lock_guard
#include <mutex>
std::mutex m;
int count = 0;
void add() {
std::lock_guard<std::mutex> lock(m); // 构造时上锁
count++; // 临界区:同一时刻只有一个线程在这
} // 出作用域,析构自动解锁——异常安全,永不遗忘
异步拿结果:future / promise / async
std::async:后台算,回头拿结果
#include <future>
// 异步跑一个计算,立刻拿到一个 future(未来的结果)
std::future<int> fut = std::async([](){
// 这里干点耗时的活
return 42;
});
// 主线程继续干别的……
int result = fut.get(); // 需要结果时再 get,会阻塞到算完
新C++20 并发三件套
std::jthread:带自动 join 的线程,析构时自动帮你 join,再也不会因为忘 join 崩。
std::counting_semaphore:信号量,控制同时进临界区的人数(比 mutex 更灵活)。
std::barrier:屏障,等 N 个线程都到齐了再一起往下走,适合分阶段并行任务。
条件变量:生产者与消费者配合
光有锁只能保证"别同时碰",但"等另一线程把活干完"还得靠条件变量 condition_variable:消费者没货就睡(wait),生产者补货后叫醒(notify_one)。经典的生产者-消费者模型:
condition_variable:队列空了就等,有货了就醒
#include <mutex>
#include <condition_variable>
#include <queue>
#include <thread>
std::queue<int> q;
std::mutex m;
std::condition_variable cv;
// 消费者:队列空就睡,等生产者叫醒
void consumer() {
std::unique_lock<std::mutex> lock(m);
cv.wait(lock, []{ return !q.empty(); }); // 第二个参数是"醒来的条件",防止假醒
int x = q.front(); q.pop();
}
// 生产者:放进东西,然后叫醒一个等的消费者
void producer(int v) {
{
std::lock_guard<std::mutex> lock(m);
q.push(v);
} // 锁在这里就放掉,别拿着锁去 notify
cv.notify_one(); // 叫醒一个在 wait 的消费者
}
记为什么 wait 要配第二个参数
多线程下,线程可能在"不该醒的时候"被叫醒(叫假醒 / spurious wakeup)。所以 cv.wait(lock, 谓词) 内部会循环检查:只有"队列真的不空了"才真的往下走。自己手写循环判断也行,但直接把条件传给 wait 最省心、最不易错。
死锁
线程 A 拿着锁 1 等锁 2,线程 B 拿着锁 2 等锁 1——互相卡死,谁也动不了。死锁的四个必要条件:互斥、持有并等待、不可抢占、循环等待。避免口诀:所有线程按相同顺序加锁;能用 lock_guard 就别手动 lock/unlock;锁的粒度尽量小。