📋 摘要
  • 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。

扩展知识


3. C++ 中 thread 的 join 和 detach 的区别?

📋 回答重点
  • join:主线程等待子线程执行完(阻塞),保证线程完成后再继续。
  • detach:分离,主线程不等待,子线程独立后台运行,线程结束后系统回收。

注意:① detach 后子线程不能访问主线程已销毁的局部变量(悬挂引用);② 线程对象析构前必须 join 或 detach,否则 std::terminate(可用 joinable() 检查)。

扩展知识


4. C++ 中 jthread 和 thread 的区别?

📋 回答重点

std::jthread(C++20)是对 std::thread 的改进,两点:

  1. 自动 join:thread 析构未 join/detach 会 std::terminate(崩溃),jthread 析构自动 join,更安全。
  2. 协作式取消:支持 stop_token/request_stop(),优雅地请求线程停止。

结论:现代 C++(C++20+)优先用 jthread。

扩展知识


5. C++ 中如何设计一个线程安全的类?

📋 回答重点
  • 线程安全 = 多线程并发访问无数据竞争、结果正确
  • 手段:互斥锁(复杂临界区)、原子变量(单标量)、读写锁(读多写少)、不可变对象
  • 原则一:不返回内部指针/引用,防止调用方绕过锁
  • 原则二:临界区最小化,锁内不做重活、不回调未知代码
  • 简单计数用 atomic,多变量复合操作用 mutex

扩展知识


6. C++ 有哪些进程间通信的方式?

📋 回答重点
  • 管道/FIFO:字节流,管道限亲缘进程、FIFO 有文件名可跨进程
  • 消息队列:结构化消息、有边界、可优先级
  • 共享内存:最快(绕过内核拷贝),但要配信号量同步
  • 信号:只传事件不传数据;socket:唯一可跨主机的通用方式
  • C++ 标准库不提供 IPC,需用 OS API(POSIX/System V/Windows)

扩展知识


7. 请介绍 C++ 中 future、promise、packaged_task、async 的区别?

📋 回答重点
  • promise 是结果「写端」,future 是结果「读端」,get_future() 配对
  • future:get() 阻塞取结果、wait() 等待,只读句柄
  • packaged_task:包装可调用对象,可取 future、可指定执行线程
  • async:最上层封装,自动异步执行并直接返回 future
  • 抽象层次:async > packaged_task > promise+future

扩展知识


8. C++ 的 async 使用时有哪些注意事项?

📋 回答重点
  • 必须接收返回值:丢弃 future 会退化为同步执行(临时 future 析构阻塞)
  • launch 策略:async 必异步、deferred 延迟到 get() 执行、默认实现决定
  • 异常延迟到 get() 才抛出
  • get() 只能调用一次,多消费者用 shared_future
  • 大量任务用线程池而非无脑 async

扩展知识


9. 如何理解 C++ 中的 atomic?

📋 回答重点
  • 把「读-改-写」变成不可分割的原子操作,消除数据竞争
  • 底层靠 CPU 单条原子指令(lock 前缀 / CAS)实现
  • 小类型通常 is_lock_free 无锁,大类型可能退化用内部锁
  • CAS(compare_exchange)是无锁编程基石
  • 原子性 ≠ 顺序性,顺序靠内存序保证

扩展知识


10. 什么场景下使用锁?什么场景下使用原子变量?

📋 回答重点
  • 原子变量:单个标量(计数器/标志位),无锁、开销小
  • 锁:多指令/多变量的复合临界区(先判断再修改、容器增删)
  • 判断标准:需要「多条操作整体原子」就用锁
  • 复杂结构也可用 CAS 做无锁,但实现难、易错

扩展知识


11. C++ 中锁的底层原理是什么?

📋 回答重点
  • 加锁本质 = 把一个状态变量原子地从 0 改成 1,靠 CPU 的 CAS 指令
  • 自旋锁:忙等循环,适合锁持有时间极短
  • 挂起:拿不到锁让线程睡眠,适合临界区较长
  • Linux 的 mutex 基于 futex:无竞争纯用户态,有竞争才进内核
  • 多级降级:先自旋、再挂起

扩展知识


12. 请介绍 C++ 的 6 种内存序?

📋 回答重点
  • relaxed:只保证原子性、不保证顺序,纯计数用
  • acquire:本线程后续读写不重排到它之前(读侧)
  • release:本线程之前读写不重排到它之后(写侧)
  • acq_rel:兼具二者,用于 RMW(CAS/fetch_add)
  • seq_cst:全局单一顺序,最强、默认
  • consume:依赖弱同步,基本不用,建议用 acquire

扩展知识


13. C++ 的条件变量为什么要配合锁使用?

📋 回答重点
  • 锁保护条件所依赖的共享状态(谓词)
  • 根本原因:避免丢失唤醒,检查条件 + 进入等待必须原子完成
  • wait 语义:原子地「解锁并挂起」,唤醒后重新加锁
  • 必须用 unique_lock(支持临时解锁/重新加锁),lock_guard 不行
  • 生产方修改条件也必须在锁内

扩展知识


14. C++ 中如何使用线程局部存储?它的原理是什么?

📋 回答重点
  • thread_local 变量每线程一份独立副本,线程私有、无需加锁
  • 首次访问时构造、线程结束时析构
  • 与 static 区别:static 全局一份、thread_local 每线程一份
  • 原理:存在线程控制块(TCB)的 TLS 区,靠 FS/GS 段寄存器定位当前线程

扩展知识


15. 如何解决 C++ 中条件变量的信号丢失和虚假唤醒问题?

📋 回答重点
  • 信号丢失:检查条件与等待之间被插入写操作,错过通知
  • 虚假唤醒:wait 无通知也可能返回
  • 统一解法:带谓词的 wait(本质是 while 循环重查条件)
  • 用 while 不用 if,唤醒后重查条件
  • 条件必须在锁内读写

扩展知识


16. 什么情况下会出现死锁?如何避免死锁?

📋 回答重点
  • 四个必要条件:互斥、占有且等待、不可剥夺、循环等待
  • 破坏任一条件即可避免,最常见破坏「循环等待」
  • 统一加锁顺序(所有线程相同顺序取锁)
  • std::scoped_lock 一次性锁多把
  • try_lock 超时回退、避免嵌套锁、RAII 保证异常解锁

扩展知识


17. C++ 如何实现线程池?给出大体思路?

📋 回答重点
  • 四要素:任务队列 + 工作线程 + 条件变量 + 停止标志
  • submit 用 packaged_task 包装任务、返回 future 拿结果
  • 空队列时 cv.wait 挂起,新任务 notify 唤醒
  • 析构时先置停止标志并 notify_all,再 join 所有线程

扩展知识


18. C++ 的有栈协程和无栈协程有什么区别?

📋 回答重点
  • 有栈协程:独立运行时栈、任意点可挂起、切换/内存开销大
  • 无栈协程:复用调用者栈、只能在 co_await 处挂起、开销小
  • 无栈协程由编译器转成状态机(coroutine frame)
  • C++20 采用无栈协程
  • 代表:有栈 = boost.coroutine/Lua;无栈 = C++20 coroutine/Python asyncio

扩展知识


19. C++ 什么场景用线程?什么场景用协程?

📋 回答重点
  • CPU 密集(大量计算)用线程,真正并行利用多核
  • IO 密集(网络/文件等待)用协程,海量并发、切换开销低
  • 线程抢占式、OS 调度;协程协作式、显式让出
  • 线程解决并行,协程解决并发(吞吐)

扩展知识


20. C++ 多线程开发需要注意些什么?线程同步有哪些手段?

📋 回答重点
  • 避免数据竞争:共享可变数据必须加锁或用原子变量
  • 防死锁:统一加锁顺序、scoped_lock、避免嵌套锁
  • 注意生命周期:线程不能访问已销毁对象(尤其 detach)
  • join/detach:析构前必须二选一,否则 terminate
  • 同步手段:锁、原子变量、条件变量、future/promise/async、信号量/屏障(C++20)

扩展知识



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)

扩展知识


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

扩展知识


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++ 并发编程