C++ 多线程:从「一前一后」到「一起跑」
头文件:<thread>
线程(thread)是操作系统调度的最小单位——一个进程里的多个线程共享同一块内存,轮流被 CPU 执行。多线程适合"互不相关的任务一起跑"(如一边下载一边显示进度),但共享数据需要保护。
一、第一个线程程序
1 |
|
join()之后t就不能再次join/detach了。
二、线程的生命周期
join():汇合
主线程阻塞等待子线程结束,再继续往下走。
1 | thread t(print, "Hello, thread!"); |
detach():分离
主线程和子线程各跑各的,子线程变成后台线程。detach 之后 t 就跟原线程对象脱钩,再不能 join 它。
1 | thread t(print, "Hello, thread!"); |
⚠️ 坑:
detach之后不要让主线程提前退出,否则子线程访问的全局变量可能消失 → 程序崩溃。
joinable():判断能否 join/detach
1 | thread t(print, "Hello, thread!"); |
三、向线程函数传参数
默认情况下,std::thread 把参数拷贝一份传给线程函数。
1 | void foo(int x) { |
默认拷贝传参;如果线程函数形参是引用类型,必须用
std::ref(...)显式告诉 thread:"我要传引用"。
1 | void foo(int& x) { |
四、数据共享:用互斥锁保护
多个线程改同一个变量会发生数据竞争,结果不确定。解决办法:加锁——同一时刻只允许一个线程进入临界区。
std::mutex:基础互斥量
1 |
|
⚠️ 手动
lock/unlock的隐患:中间抛异常 → 永远不解锁 → 死锁。所以实际工程都用 RAII 包装:lock_guard/unique_lock。
lock_guard:最简 RAII 锁
构造时加锁,析构时自动解锁——哪怕函数中途抛异常,析构也会跑,永远不会忘解锁。
1 | void func() { |
unique_lock:更灵活的 RAII 锁
功能比 lock_guard 多:可延迟加锁、可手动加解锁、可配条件变量。代价是比 lock_guard 略慢一点。
1 | void func() { |
延迟加锁 defer_lock
构造时不加锁,自己手动决定什么时候加:
1 | unique_lock<mutex> lg(mtx, defer_lock); |
限时尝试加锁 try_lock_for
1 | unique_lock<mutex> lg(mtx, defer_lock); |
五、互斥量死锁
......
死锁的 4 个必要条件(互斥、占有并等待、不可剥夺、循环等待),打破任意一个都能避免死锁。常见做法:按固定顺序加锁、用
std::lock一次性锁多个互斥量(内部用死锁避免算法)。
六、其他常用工具
std::call_once:多线程只跑一次
1 |
|
std::this_thread::sleep_for:让当前线程睡一会儿
1 | this_thread::sleep_for(chrono::milliseconds(2000)); // 睡 2 秒 |
七、线程安全
线程安全:多线程程序每次运行的结果都和单线程运行结果一致。
简单判据:有没有「多个线程同时读写同一块共享数据」。有 → 加锁保护或换线程局部数据。
八、易错点清单
- 忘
join或detach:std::thread析构时如果还是joinable,程序会直接std::terminate崩溃。 - 没加锁改共享变量:结果随机、调试玄学。
- 手动
lock/unlock中间忘unlock或抛异常 → 永远死锁。用lock_guard/unique_lock自动管理。 - 锁的粒度太粗:把不该一起锁的代码一起锁,性能掉到不如单线程。
- 多个锁顺序不一致:A 线程先锁 m1 再锁 m2,B 线程先锁 m2 再锁 m1 → 经典死锁。统一加锁顺序或用
std::lock。 - 把局部变量的引用传给线程:线程跑起来时变量已经销毁 → 悬垂引用。
九、极简速记
| 需求 | 写法 |
|---|---|
| 等子线程结束 | t.join(); |
| 后台跑不管它 | t.detach(); |
| 简单加锁 | lock_guard<mutex> lg(mtx); |
| 灵活加锁(延迟 / 条件变量) | unique_lock<mutex> lg(mtx); |
| 多线程只跑一次 | call_once(flag, func); |
| 让线程睡一会 | this_thread::sleep_for(ms); |
十、小结
- 线程 = 操作系统调度的最小单位,同一进程内共享内存;
- 共享数据必须加锁:
lock_guard/unique_lock是 RAII 自动解锁; join不能忘:thread析构前必须是join或detach过的;- 多线程不是越快越好,加锁和同步本身有开销,先把算法和数据结构优化好。