目录
[1. std::thread::join() vs std::thread::detach()](#1. std::thread::join() vs std::thread::detach())
[2. RAII 风格的锁:std::lock_guard 与 std::unique_lock](#2. RAII 风格的锁:std::lock_guard 与 std::unique_lock)
[3. std::condition_variable(条件变量)](#3. std::condition_variable(条件变量))
[4. std::future / std::promise / std::async](#4. std::future / std::promise / std::async)
[4.1 std::future:拿结果的"票"](#4.1 std::future:拿结果的“票”)
[4.2 std::promise:主动"写入结果"的一端](#4.2 std::promise:主动“写入结果”的一端)
[4.3 std::async:一行搞定异步任务](#4.3 std::async:一行搞定异步任务)
[5. 死锁与预防](#5. 死锁与预防)
[5.1 死锁的四个必要条件](#5.1 死锁的四个必要条件)
[5.2 打破循环等待的常用手段](#5.2 打破循环等待的常用手段)
[6. 如何检测和调试死锁](#6. 如何检测和调试死锁)
[7. 原子操作与内存模型:std::atomic](#7. 原子操作与内存模型:std::atomic)
[8. 为什么要用 RAII 锁?](#8. 为什么要用 RAII 锁?)
1. std::thread::join() vs std::thread::detach()
join():等待线程结束
-
当前线程阻塞,直到子线程运行完毕。
-
用于:
-
需要 确认任务完成(例如要拿结果、要保证逻辑顺序)。
-
需要在退出前确保所有线程都结束。
-
-
注意:每个
std::thread只能join()一次 ,且仅对joinable()的线程调用。std::thread t(worker);
if (t.joinable()) {
t.join(); // 等待执行完
}
detach():与主线程"脱离关系"
-
线程变成 后台线程,主线程不再管理它。
-
进程结束时,仍在运行的后台线程会被 强制终止。
-
常见场景:
- 日志、监控、心跳等确实无所谓结果的后台任务。
-
风险:
-
被 detach 的线程 不能再 join;
-
可能访问已经销毁的对象(比如主线程退出或局部变量结束生命周期)。
std::thread t(worker);
t.detach(); // 变成守护线程
-
一般规则:
能join()就不要detach();后台线程要特别小心生命周期。
2. RAII 风格的锁:std::lock_guard 与 std::unique_lock
std::lock_guard
-
最简单的 RAII 锁,构造时加锁、析构时自动解锁。
-
不可手动解锁/重新上锁,也不能延迟加锁。
-
适合:作用域简单、不需要锁的高级操作的场景。
std::mutex m;
void f() {
std::lock_guardstd::mutex lk(m);
// 临界区
} // lk 析构 -> 自动 unlock()
std::unique_lock
-
更灵活的 RAII 锁,支持:
-
延迟加锁
std::unique_lock<std::mutex> lk(m, std::defer_lock); -
手动
lock()/unlock() -
可移动(可从函数中返回锁)
-
必须用
std::condition_variable搭配unique_lock(因为会临时释放/重新获取锁)。std::mutex m;
void f() {
std::unique_lockstd::mutex lk(m); // 构造时加锁
// 临界区
lk.unlock();
// 不在锁内
lk.lock();
}
-
经验:
默认用
std::lock_guard(简单、安全)。需要配合
condition_variable或手动控制加解锁时用std::unique_lock。
3. std::condition_variable(条件变量)
用途: 让线程 在条件满足前睡眠,由其他线程在条件变化时唤醒。避免"死循环 + 睡眠"轮询。
标准用法模式:
std::mutex m;
std::condition_variable cv;
bool ready = false;
void producer() {
{
std::lock_guard<std::mutex> lk(m);
ready = true;
}
cv.notify_one(); // 或 notify_all()
}
void consumer() {
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, []{ return ready; }); // 带谓词,避免虚假唤醒
// ready == true,安全使用共享数据
}
关键点:
-
wait()必须传std::unique_lock,因为它要:-
暂时释放锁让其他线程修改条件;
-
被唤醒后重新加锁,再返回。
-
-
总是使用 带谓词版本
wait(lock, predicate),避免虚假唤醒。
4. std::future / std::promise / std::async
4.1 std::future:拿结果的"票"
-
表示一次异步计算的 结果(或异常)。
-
通过
future.get()获取结果(阻塞直到结果就绪,且只能调用一次)。
4.2 std::promise:主动"写入结果"的一端
-
promise和future配对使用:-
promise端:set_value()或set_exception() -
future端:get()阻塞等待结果std::promise<int> p;
std::future<int> f = p.get_future();std::thread t([&p]{
p.set_value(42);
});int x = f.get(); // 得到 42
t.join();
-
4.3 std::async:一行搞定异步任务
-
自动创建线程或在线程池中执行(由实现决定)。
-
返回一个
std::future,用get()拿结果。std::future<int> f = std::async(std::launch::async, []{
return 42;
});int x = f.get(); // 阻塞直到任务完成
小结:
要 异步执行 + 获取返回值 :首选
std::async。要自己控制任务启动/线程:用
promise + future。
5. 死锁与预防
5.1 死锁的四个必要条件
-
互斥:资源一次只能被一个线程占用。
-
持有并等待:拿着一个锁,还在等待另一个锁。
-
不可抢占:锁不能强制被别的线程抢走。
-
循环等待:多个线程按环形次序互相等待对方的锁。
实践上常用的是:打破"循环等待"。
5.2 打破循环等待的常用手段
-
固定加锁顺序
-
约定所有线程按同样的顺序加锁:
先锁 m1,再锁 m2,再锁 m3 ... -
若需要同时锁多个 mutex,可用
std::lock():std::lock(m1, m2);
std::lock_guardstd::mutex lk1(m1, std::adopt_lock);
std::lock_guardstd::mutex lk2(m2, std::adopt_lock);
-
-
std::scoped_lock(C++17 推荐)-
同时锁多个 mutex,自动处理加锁顺序,不会死锁:
std::scoped_lock lk(m1, m2); // 最安全、最省心
-
-
减少锁粒度(减小临界区)
-
缩短持锁时间:
-
不要在锁内做耗时操作(IO、网络、sleep 等)。
-
处理数据前后分离:锁里只做"拷贝/取出",再在锁外慢慢处理。
-
-
-
非阻塞锁
try_lock()-
拿不到锁就 立即返回,不进入等待队列,降低死锁概率:
if (m.try_lock()) {
// 成功获取
m.unlock();
} else {
// 拿不到锁,选择放弃/重试
}
-
6. 如何检测和调试死锁
典型症状:
-
程序"卡住"但 CPU 使用率很低。
-
调试时大量线程停在
pthread_mutex_lock/futex_wait/std::mutex::lock等位置。 -
多个线程互相等待别人释放锁,形成环。
常用方法:
-
工具检测
-
Clang:
-fsanitize=thread(ThreadSanitizer) -
Visual Studio:Concurrency Visualizer
-
部分平台有专门的死锁检测/线程分析工具。
-
-
日志追踪
-
对每次 加锁/解锁 打日志(可带线程 id 和锁地址)。
-
卡住时看最后一个成功加锁的人是谁,锁没释放在哪里。
-
-
超时机制
-
使用 带超时的锁:
-
std::timed_mutex的try_lock_for()/try_lock_until() -
或
std::unique_lock+std::condition_variable::wait_for()
-
-
超时后打印报警日志,便于排查:
std::timed_mutex m;
if (!m.try_lock_for(std::chrono::milliseconds(100))) {
// 认为可能有问题,记录日志
}
-
7. 原子操作与内存模型:std::atomic
std::atomic 的特点:
-
对单个变量的读写是 原子 的(不会被打断)。
-
常用于:计数器、标志位、无锁队列等场景。
-
常见类型:
std::atomic<int>,std::atomic<bool>,std::atomic<void*>等。std::atomic<int> counter{0};
void f() {
counter.fetch_add(1); // 原子自增
}
内存模型(简要):
-
memory_order_seq_cst:默认,最强保证,最简单也最安全。 -
memory_order_acquire/memory_order_release:- 写时
release,读时acquire,形成"先写后读"的有序关系。
- 写时
-
memory_order_relaxed:只保证原子性,不保证顺序,一般仅用于简单计数器。
经验:
不熟悉内存模型时,保持使用默认
seq_cst。真正需要极致性能再考虑更弱的顺序约束。
8. 为什么要用 RAII 锁?
手动 lock() / unlock() 的最大风险:
- 在临界区内 抛异常 / 提前
return/ 中途throw,忘记unlock(),导致锁永远不被释放 → 直接导致死锁。
RAII 风格的锁(lock_guard / unique_lock)依赖于 C++ 的 作用域与析构:
void f() {
std::lock_guard<std::mutex> lk(m);
// 中间无论是 return 还是抛异常
} // 离开作用域,lk 自动析构 -> 自动 unlock()
经验法则:
不要在业务代码里直接写m.lock()/m.unlock(),
把锁交给 RAII 对象管理。