Appearance
C++ 并发与多线程专题
C++ 后端/客户端面试的第二部分经常考多线程:
std::thread、mutex、condition_variable、atomic、数据竞争、死锁。C++ 本身不考操作系统底层那么深,重点在「使用正确 + 原理」。
一、核心概念
- 线程与进程:进程是资源分配单位、线程是调度单位;同进程线程共享地址空间(堆/全局),各自独立栈。
- 数据竞争(data race):两个及以上线程同时访问同一内存,至少一个在写且未同步 → UB。
- 竞态条件(race condition):执行结果依赖交错顺序,即使无数据竞争也不一定正确。
二、常用同步工具
| 工具 | 用途 |
|---|---|
std::mutex / lock_guard / unique_lock | 互斥,RAII 自动解锁 |
std::atomic<T> | 原子变量(lock-free for 常用整数)、内存序 |
std::condition_variable | 条件等待 / 通知 |
std::future / std::promise | 一次性返回值通道 |
std::async | 异步任务 |
std::thread / jthread(C++20) | 线程,join 管理 |
shared_mutex(C++17) | 读写锁 |
call_once | 只执行一次 |
三、高频自测(含答案)
1. 多线程加锁怎么写?lock_guard vs unique_lock?
答案
lock_guard 构造即锁、析构即解锁、不可手动解锁,轻量;unique_lock 可延迟加锁/手动解锁/配合条件变量,更灵活但略重。详见 threads-basics.md。
2. 什么是死锁?四个必要条件 + 如何避免?
答案
互斥、占有且等待、不可剥夺、循环等待四条件同时成立才死锁。破坏其一即可:按固定顺序加锁、std::lock 一次锁多个、try_lock、RAII、设超时。详见 deadlock-singleton.md。
3. 条件变量怎么用?为什么必须配合锁?虚假唤醒?
答案
wait 会释放锁并阻塞、被 notify 唤醒后重新加锁;必须配锁因为判断条件要原子(条件与共享数据相关)。虚假唤醒可能发生,所以要用 while (条件不满足) wait(...) 重查。
4. 手写线程安全单例?
答案
C++11 起局部 static 初始化天然线程安全(Magic Static)最简单;或 call_once;双重检查锁定需配合 atomic + 正确的内存序。详见 deadlock-singleton.md。
5. std::atomic 的内存序?
答案
seq_cst 最强、全局一致顺序;acquire/release 成对建立 happens-before;relaxed 只保证原子性、无顺序。按需选更弱序换取性能。详见 atomic-memoryorder.md。
6. volatile 与 atomic 的关系?
答案
无关。volatile 只阻止编译器优化该变量的读写(用于硬件/信号),不保证原子性、可见性、顺序;atomic 保证原子操作和内存序。并发必须用 atomic 而非 volatile。
7. 生产者-消费者模型实现?
答案
互斥锁 + 条件变量(not_empty/not_full)+ 有界缓冲;或无锁环形队列。详见 threads-basics.md。
8. 数据竞争如何用工具检测(TSan)?
答案
ThreadSanitizer,编译加 -fsanitize=thread,运行期检测数据竞争并报告位置。详见 threads-basics.md。
子文档
| 文件 | 内容 |
|---|---|
hpc-intro.md | HPC 并发总览(线程数/Amdahl/避免争用) |
threads-basics.md | 线程、mutex、条件变量、生产者消费者 |
atomic-memoryorder.md | atomic 与内存序、volatile 对比 |
deadlock-singleton.md | 死锁、单例、锁的选择 |
thread-pool.md | 线程池(含工作窃取、手写) |
lockfree.md | 无锁编程(CAS/ABA/hazard) |
lock-optimize.md | 锁优化、假共享、自旋 vs 互斥、读写锁 |
parallel-algo.md | 并行算法、分治、任务图/流水线 |
coroutine.md | C++20 协程 co_await/机制 |
| 手写并发件 | 手写 SPSC 无锁环 / 有界阻塞队列 / 单写多读快照发布(跨到 C++ 手写专题) |