楼层: 首页/ 软件技术/ 现代 C++/ 并发与多线程
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;锁的粒度尽量小。