📋 摘要
- C++ 并发编程面试高频问答,每题只保留回答重点与扩展知识
- 详细原理解析与代码示例见 03-Resources/cpp/C++ 并发编程
- 持续补充中
1. 什么是进程?什么是线程?有什么区别?
📋 回答重点
- 进程是资源分配的基本单位,拥有独立地址空间、文件描述符、堆栈
- 线程是 CPU 调度的基本单位,是进程内的执行流
- 同进程线程共享地址空间/堆/全局变量/文件描述符,独享栈与寄存器
- 线程创建/切换开销小(不切地址空间、不刷 TLB),进程隔离性强、切换重
- 线程崩溃可能拖垮整个进程;进程崩溃不影响其它进程
扩展知识
- Linux:进程
fork(写时复制),线程clone(共享地址空间) - 进程间通信靠 IPC;线程共享内存但需加锁/原子同步
- 详细讲解:03-Resources/cpp/C++ 并发编程
2. C++ 中 lock_guard 和 unique_lock 的区别?
📋 回答重点
两者都是 RAII 锁(构造加锁、析构解锁),区别在灵活性:
- lock_guard:简单,只加锁/解锁,开销更低,不能手动解锁、不能延迟加锁、不可移动。
- unique_lock:灵活,可延迟加锁(
defer_lock)、手动解锁(unlock())、可移动、配合条件变量(condition_variable必须用它)。
简单加锁用 lock_guard;需延迟/提前/条件变量/转移所有权用 unique_lock。
扩展知识
- 条件变量必须用 unique_lock 的原因
- 详细讲解:03-Resources/cpp/C++ 并发编程
3. C++ 中 thread 的 join 和 detach 的区别?
📋 回答重点
- join:主线程等待子线程执行完(阻塞),保证线程完成后再继续。
- detach:分离,主线程不等待,子线程独立后台运行,线程结束后系统回收。
注意:① detach 后子线程不能访问主线程已销毁的局部变量(悬挂引用);② 线程对象析构前必须 join 或 detach,否则 std::terminate(可用 joinable() 检查)。
扩展知识
- detach 访问已销毁变量的风险
- 详细讲解:03-Resources/cpp/C++ 并发编程
4. C++ 中 jthread 和 thread 的区别?
📋 回答重点
std::jthread(C++20)是对 std::thread 的改进,两点:
- 自动 join:thread 析构未 join/detach 会
std::terminate(崩溃),jthread 析构自动 join,更安全。 - 协作式取消:支持
stop_token/request_stop(),优雅地请求线程停止。
结论:现代 C++(C++20+)优先用 jthread。
扩展知识
- stop_token 协作取消示例
- 详细讲解:03-Resources/cpp/C++ 并发编程
5. C++ 中如何设计一个线程安全的类?
📋 回答重点
- 线程安全 = 多线程并发访问无数据竞争、结果正确
- 手段:互斥锁(复杂临界区)、原子变量(单标量)、读写锁(读多写少)、不可变对象
- 原则一:不返回内部指针/引用,防止调用方绕过锁
- 原则二:临界区最小化,锁内不做重活、不回调未知代码
- 简单计数用 atomic,多变量复合操作用 mutex
扩展知识
- const 方法加锁时锁要声明为 mutable
- 单例懒加载用 std::call_once / 局部 static
- 详细讲解:03-Resources/cpp/C++ 并发编程
6. C++ 有哪些进程间通信的方式?
📋 回答重点
- 管道/FIFO:字节流,管道限亲缘进程、FIFO 有文件名可跨进程
- 消息队列:结构化消息、有边界、可优先级
- 共享内存:最快(绕过内核拷贝),但要配信号量同步
- 信号:只传事件不传数据;socket:唯一可跨主机的通用方式
- C++ 标准库不提供 IPC,需用 OS API(POSIX/System V/Windows)
扩展知识
- 信号量 semaphore 是同步原语,不是数据传递手段
- 详细讲解:03-Resources/cpp/C++ 并发编程
7. 请介绍 C++ 中 future、promise、packaged_task、async 的区别?
📋 回答重点
- promise 是结果「写端」,future 是结果「读端」,get_future() 配对
- future:get() 阻塞取结果、wait() 等待,只读句柄
- packaged_task:包装可调用对象,可取 future、可指定执行线程
- async:最上层封装,自动异步执行并直接返回 future
- 抽象层次:async > packaged_task > promise+future
扩展知识
- promise 可手动 set_value / set_exception
- 详细讲解:03-Resources/cpp/C++ 并发编程
8. C++ 的 async 使用时有哪些注意事项?
📋 回答重点
- 必须接收返回值:丢弃 future 会退化为同步执行(临时 future 析构阻塞)
- launch 策略:async 必异步、deferred 延迟到 get() 执行、默认实现决定
- 异常延迟到 get() 才抛出
- get() 只能调用一次,多消费者用 shared_future
- 大量任务用线程池而非无脑 async
扩展知识
- 要确定性就显式指定 std::launch::async
- 详细讲解:03-Resources/cpp/C++ 并发编程
9. 如何理解 C++ 中的 atomic?
📋 回答重点
- 把「读-改-写」变成不可分割的原子操作,消除数据竞争
- 底层靠 CPU 单条原子指令(lock 前缀 / CAS)实现
- 小类型通常 is_lock_free 无锁,大类型可能退化用内部锁
- CAS(compare_exchange)是无锁编程基石
- 原子性 ≠ 顺序性,顺序靠内存序保证
扩展知识
- std::atomic_flag 最轻量,常用于自旋锁
- 详细讲解:03-Resources/cpp/C++ 并发编程
10. 什么场景下使用锁?什么场景下使用原子变量?
📋 回答重点
- 原子变量:单个标量(计数器/标志位),无锁、开销小
- 锁:多指令/多变量的复合临界区(先判断再修改、容器增删)
- 判断标准:需要「多条操作整体原子」就用锁
- 复杂结构也可用 CAS 做无锁,但实现难、易错
扩展知识
- 无锁结构(无锁栈/队列)用 CAS 循环实现,仅性能关键路径用
- 详细讲解:03-Resources/cpp/C++ 并发编程
11. C++ 中锁的底层原理是什么?
📋 回答重点
- 加锁本质 = 把一个状态变量原子地从 0 改成 1,靠 CPU 的 CAS 指令
- 自旋锁:忙等循环,适合锁持有时间极短
- 挂起:拿不到锁让线程睡眠,适合临界区较长
- Linux 的 mutex 基于 futex:无竞争纯用户态,有竞争才进内核
- 多级降级:先自旋、再挂起
扩展知识
- x86 用 lock 前缀锁缓存行保证原子性
- 详细讲解:03-Resources/cpp/C++ 并发编程
12. 请介绍 C++ 的 6 种内存序?
📋 回答重点
- relaxed:只保证原子性、不保证顺序,纯计数用
- acquire:本线程后续读写不重排到它之前(读侧)
- release:本线程之前读写不重排到它之后(写侧)
- acq_rel:兼具二者,用于 RMW(CAS/fetch_add)
- seq_cst:全局单一顺序,最强、默认
- consume:依赖弱同步,基本不用,建议用 acquire
扩展知识
- acquire/release 配对建立同步关系,是无锁发布-订阅的关键
- 详细讲解:03-Resources/cpp/C++ 并发编程
13. C++ 的条件变量为什么要配合锁使用?
📋 回答重点
- 锁保护条件所依赖的共享状态(谓词)
- 根本原因:避免丢失唤醒,检查条件 + 进入等待必须原子完成
- wait 语义:原子地「解锁并挂起」,唤醒后重新加锁
- 必须用 unique_lock(支持临时解锁/重新加锁),lock_guard 不行
- 生产方修改条件也必须在锁内
扩展知识
- notify 可在锁外调用
- 详细讲解:03-Resources/cpp/C++ 并发编程
14. C++ 中如何使用线程局部存储?它的原理是什么?
📋 回答重点
- thread_local 变量每线程一份独立副本,线程私有、无需加锁
- 首次访问时构造、线程结束时析构
- 与 static 区别:static 全局一份、thread_local 每线程一份
- 原理:存在线程控制块(TCB)的 TLS 区,靠 FS/GS 段寄存器定位当前线程
扩展知识
- 典型用途:errno、随机数引擎、每线程连接/缓冲
- 详细讲解:03-Resources/cpp/C++ 并发编程
15. 如何解决 C++ 中条件变量的信号丢失和虚假唤醒问题?
📋 回答重点
- 信号丢失:检查条件与等待之间被插入写操作,错过通知
- 虚假唤醒:wait 无通知也可能返回
- 统一解法:带谓词的 wait(本质是 while 循环重查条件)
- 用 while 不用 if,唤醒后重查条件
- 条件必须在锁内读写
扩展知识
- cv.wait(lock, pred) 等价于 while(!pred()) cv.wait(lock)
- 详细讲解:03-Resources/cpp/C++ 并发编程
16. 什么情况下会出现死锁?如何避免死锁?
📋 回答重点
- 四个必要条件:互斥、占有且等待、不可剥夺、循环等待
- 破坏任一条件即可避免,最常见破坏「循环等待」
- 统一加锁顺序(所有线程相同顺序取锁)
- std::scoped_lock 一次性锁多把
- try_lock 超时回退、避免嵌套锁、RAII 保证异常解锁
扩展知识
- 锁内不调用可能再拿锁的未知代码
- 详细讲解:03-Resources/cpp/C++ 并发编程
17. C++ 如何实现线程池?给出大体思路?
📋 回答重点
- 四要素:任务队列 + 工作线程 + 条件变量 + 停止标志
- submit 用 packaged_task 包装任务、返回 future 拿结果
- 空队列时 cv.wait 挂起,新任务 notify 唤醒
- 析构时先置停止标志并 notify_all,再 join 所有线程
扩展知识
- 可扩展:任务优先级、动态扩缩容、饱和策略
- 详细讲解:03-Resources/cpp/C++ 并发编程
18. C++ 的有栈协程和无栈协程有什么区别?
📋 回答重点
- 有栈协程:独立运行时栈、任意点可挂起、切换/内存开销大
- 无栈协程:复用调用者栈、只能在 co_await 处挂起、开销小
- 无栈协程由编译器转成状态机(coroutine frame)
- C++20 采用无栈协程
- 代表:有栈 = boost.coroutine/Lua;无栈 = C++20 coroutine/Python asyncio
扩展知识
- 无栈协程不能在内层普通函数里任意挂起
- 详细讲解:03-Resources/cpp/C++ 并发编程
19. C++ 什么场景用线程?什么场景用协程?
📋 回答重点
- CPU 密集(大量计算)用线程,真正并行利用多核
- IO 密集(网络/文件等待)用协程,海量并发、切换开销低
- 线程抢占式、OS 调度;协程协作式、显式让出
- 线程解决并行,协程解决并发(吞吐)
扩展知识
- 百万连接场景协程明显优于每连接一线程
- 详细讲解:03-Resources/cpp/C++ 并发编程
20. C++ 多线程开发需要注意些什么?线程同步有哪些手段?
📋 回答重点
- 避免数据竞争:共享可变数据必须加锁或用原子变量
- 防死锁:统一加锁顺序、scoped_lock、避免嵌套锁
- 注意生命周期:线程不能访问已销毁对象(尤其 detach)
- join/detach:析构前必须二选一,否则 terminate
- 同步手段:锁、原子变量、条件变量、future/promise/async、信号量/屏障(C++20)
扩展知识
- 能用 async/线程池就不手写裸 thread
- 详细讲解:03-Resources/cpp/C++ 并发编程
21. 无锁编程是什么?CAS 的原理?ABA 问题如何解决?
📋 回答重点
- 无锁编程:用原子操作(CAS)+ 重试循环代替互斥锁,不阻塞线程、无死锁、实现难度高
- CAS(比较并交换):
compare_exchange_weak/strong,底层是 x86 的lock cmpxchg,一条原子指令完成「比较 + 交换」 - CAS 循环是无锁算法通用骨架:读旧值 → CAS 尝试 → 失败重试
- ABA 问题:值被别的线程「改走又改回」,CAS 误以为”没变”导致结构破坏
- 解决:tagged pointer 加版本号,每次修改版本 +1,CAS 同时比较指针和版本
扩展知识
compare_exchange_weak允许伪失败但性能好,适合放循环里;strong值相等必成功- 无锁不等于无同步,内存序(acquire/release)依然要配
- 详细讲解:03-Resources/cpp/C++ 并发编程
22. std::shared_mutex(读写锁)的原理与适用场景?
📋 回答重点
- 读写锁:读共享、写独占——多线程可同时读,写必须互斥
- 读用
std::shared_lock,写用std::unique_lock - 适用读多写少场景(缓存、配置、字典查找),吞吐高于普通 mutex
- 写多读少时收益不大,且要防写者饥饿
扩展知识
- C++17 才进标准,头文件
<shared_mutex> - 原理是内部状态计数区分读锁数量与写锁标志,底层仍是 CAS + futex
- 详细讲解:03-Resources/cpp/C++ 并发编程
23. 如何实现线程安全的单例?双检锁(DCLP)为什么需要内存序配合?
📋 回答重点
- 单例四写法:饿汉(天然安全)/ 懒汉(要加锁)/ DCLP(需内存序)/ Meyers(推荐)
- DCLP:两次判空减少加锁,但普通指针的
new会被重排,别的线程可能读到未构造完的对象 - 必须
atomic+ release 发布 + acquire 获取,保证「构造完成先于指针发布、读到指针后看到完整构造」 - C++11 后直接推荐 Meyers 单例(局部 static 的 magic static 自动线程安全)
扩展知识
- release/acquire 配对建立同步关系,是 DCLP 正确的关键
- 普通指针版 DCLP 即使加锁也是未定义行为
- 详细讲解:03-Resources/cpp/C++ 并发编程
24. C++ 内存模型的 happens-before 关系?memory_order 如何保证可见性?
📋 回答重点
- happens-before = 跨线程操作的可见性顺序;无此关系且共享变量至少一写 = 数据竞争
- 建立途径:单线程程序顺序、mutex 加解锁、release/acquire 同步、线程 create/join
- release 写与 acquire 读配对形成 synchronizes-with,再经传递性推出 happens-before
- memory_order 约束编译器/CPU 重排,保证该看到的值不会被提前/滞后读
扩展知识
- acquire 是读侧屏障、release 是写侧屏障、acq_rel 两者兼具
- 能现场画「普通写 → release → acquire → 读」的 happens-before 推导是高分点
- 详细讲解:03-Resources/cpp/C++ 并发编程
25. 什么是伪共享(false sharing)?如何用 cache line 对齐避免?
📋 回答重点
- 缓存行通常 64 字节,CPU 按行加载内存
- 两个线程各改同一缓存行里的不同变量 → 互相无效化缓存 → 性能暴跌,即伪共享
- 解决:
alignas(64)或 padding 把变量隔到不同缓存行 - 进阶:
std::hardware_destructive_interference_size(C++17)跨平台更稳
扩展知识
- 典型场景:无锁计数、线程池统计、环形队列中相邻存放的计数器
- 性能差异可达数倍,普通业务不必过度对齐(浪费内存)
- 详细讲解:03-Resources/cpp/C++ 并发编程
26. 自旋锁和互斥锁的取舍?什么场景用自旋锁?
📋 回答重点
- 自旋锁拿不到锁时忙等(不释放 CPU),互斥锁拿不到锁时睡眠(释放 CPU)
- 自旋锁适合极短临界区 + 多核 + 竞争时间短,免上下文切换
- 互斥锁适合长临界区或含阻塞操作,睡眠让出 CPU 更划算
- 现代 mutex 先自旋后睡眠(自适应);单核上自旋无意义
扩展知识
- 自旋锁用
atomic_flag+test_and_set实现,互斥锁是 CAS + futex 多级降级 - 持有自旋锁时不能睡眠/阻塞
- 详细讲解:03-Resources/cpp/C++ 并发编程
27. 线程安全的队列(加锁+条件变量)如何实现?
📋 回答重点
- 用
std::mutex保护std::queue,用std::condition_variable让空队列的pop阻塞等待 pop用unique_lock+cv.wait(lk, 谓词),谓词判断「非空」天然防虚假唤醒push加锁入队后notify_one(),且通知放在解锁之后更高效- 非阻塞版本用
std::optional<T>做try_pop,避免哨兵值 / 引用传出 - 有界队列(限容量)用
not_full/not_empty两个条件变量
扩展知识
- 线程安全队列是条件变量最经典的应用,也是线程池任务队列的底层组件
- 加锁队列实现简单、够用;更高吞吐才上无锁队列(见下一题)
- 详细讲解:03-Resources/cpp/C++ 并发编程
28. 无锁队列(MPMC)如何实现?典型难点?
📋 回答重点
- 经典思路 Michael-Scott 队列:单向链表 + head/tail 指针 + 空哨兵节点,入队改 tail、出队改 head,都用 CAS
- 难点一(最难):内存回收——弹出的节点不能立刻 delete,用 hazard pointer / epoch / 引用计数
- 难点二:ABA 问题,用版本号 tagged pointer 解决
- 难点三:tail 指针一致性与帮助机制(帮助落后线程移动 tail)
扩展知识
- MPMC = 多生产者多消费者,吞吐高于加锁队列
- CAS 要配正确的 acquire/release 内存序
- 详细讲解:03-Resources/cpp/C++ 并发编程
29. C++20 的 std::latch、std::barrier、std::counting_semaphore 的作用?
📋 回答重点
std::latch:一次性倒计数闸门,等 N 个线程到齐(count_down + wait)std::barrier:可复用分阶段同步点,每轮到齐放行并进入下一阶段std::counting_semaphore:计数信号量,控制最多 N 个线程访问资源(acquire/release)- 三者都是 C++20 引入,替代手写 mutex + condition_variable
扩展知识
- 头文件分别
<latch>、<barrier>、<semaphore> std::binary_semaphore(上限 1)即信号量版互斥锁- 详细讲解:03-Resources/cpp/C++ 并发编程
30. C++20 协程的底层机制(对称/非对称转移)?
📋 回答重点
- C++20 协程是无栈的,编译器把函数体转成状态机 + 堆上 coroutine frame
- 三关键字:co_await 挂起、co_yield 产值、co_return 结束
- 挂起只发生在显式 co_await/co_yield 点,恢复靠
coroutine_handle::resume() - 非对称转移:挂起后控制权交还调用者/调度器;对称转移是协程间直接互切
扩展知识
- coroutine frame 存 promise 对象、协程参数、局部变量与状态位(resume 点)
- 对称转移更多见于有栈协程(boost.coroutine)
- 详细讲解:03-Resources/cpp/C++ 并发编程