C++ 并发与同步速查笔记(整理版)
本文总结了C++多线程编程的核心知识点: 线程管理:join()会阻塞等待线程结束,detach()使线程后台运行但需注意生命周期; 锁机制:优先使用RAII风格的std::lock_guard,需要灵活控制时用std::unique_lock; 同步工具:条件变量(std::condition_variable)用于线程间通知,future/promise/async用于异步任务; 死锁预防:通
目录
1. std::thread::join() vs std::thread::detach()
2. RAII 风格的锁:std::lock_guard 与 std::unique_lock
3. std::condition_variable(条件变量)
4. std::future / std::promise / std::async
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_guard<std::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_lock<std::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_guard<std::mutex> lk1(m1, std::adopt_lock); std::lock_guard<std::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 对象管理。
更多推荐



所有评论(0)