C++ 并发编程面试题总结
一、进程与线程基础
⭐ 什么是进程?什么是线程?它们的区别?
┌─────────────────────────────────────────────────────────────┐
│ 进程 (Process) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 线程 1 │ │ 线程 2 │ │ 线程 3 │ │
│ │ │ │ │ │ │ │
│ │ 栈 │ │ 栈 │ │ 栈 │ ← 每个线程私有 │
│ │ 寄存器 │ │ 寄存器 │ │ 寄存器 │ │
│ │ 程序计数器│ │ 程序计数器│ │ 程序计数器│ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 共享资源 │ │
│ │ • 代码段 • 堆 • 全局变量 • 文件描述符 • 信号处理 │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
| 特性 | 进程 | 线程 |
|---|---|---|
| 定义 | 资源分配的基本单位 | CPU 调度的基本单位 |
| 地址空间 | 独立的虚拟地址空间 | 共享所属进程的地址空间 |
| 开销 | 创建/切换开销大 | 创建/切换开销小 |
| 通信 | IPC(管道、消息队列、共享内存) | 直接读写共享变量 |
| 安全性 | 一个进程崩溃不影响其他进程 | 一个线程崩溃可能导致整个进程崩溃 |
| 资源 | 拥有独立资源 | 共享进程资源 |
⭐ 线程的生命周期和状态
创建
│
▼
┌──────────┐
│ 新建 │
│ (Created) │
└────┬─────┘
│ start()
▼
┌──────────┐ ┌──────────┐
┌───→│ 就绪 │◄────────│ 阻塞 │
│ │ (Ready) │ │(Blocked) │
│ └────┬─────┘ └────┬─────┘
│ │ 获得 CPU ↑
│ ▼ │ 等待锁/IO/sleep
│ ┌──────────┐ │
│ │ 运行 │───────────────┘
│ │(Running) │
│ └────┬─────┘
│ │ 时间片用完
└─────────┘
│ 执行完毕 / 异常
▼
┌──────────┐
│ 终止 │
│(Terminated)│
└──────────┘
⭐ 上下文切换是什么?有什么开销?
线程 A 运行中 线程 B 等待中
┌──────────────┐ ┌──────────────┐
│ 寄存器状态 │ │ 寄存器状态 │
│ PC 指针 │ │ PC 指针 │
│ 栈指针 │ │ 栈指针 │
└──────┬───────┘ └──────┬───────┘
│ │
│ ① 保存 A 的上下文到 TCB │
├──────────────────────────────────┤
│ ② 切换页表/TLB(进程切换时) │
├──────────────────────────────────┤
│ ③ 从 TCB 恢复 B 的上下文 │
│ │
▼ ▼
线程 A 等待中 线程 B 运行中
上下文切换的开销:
• 直接开销:保存/恢复寄存器、切换内核栈
• 间接开销:CPU 缓存失效(Cache Miss)、TLB 刷新
• 典型耗时:几微秒到几十微秒
二、C++ 多线程编程
⭐ 创建线程的几种方式
cpp
#include <thread>
#include <functional>
// 方式 1:普通函数
void Task(int id) { std::cout << "Thread " << id << "\n"; }
std::thread t1(Task, 1);
// 方式 2:Lambda(最常用)
std::thread t2([](int id) {
std::cout << "Lambda " << id << "\n";
}, 2);
// 方式 3:成员函数
class Worker {
public:
void Run(int id) { std::cout << "Worker " << id << "\n"; }
};
Worker w;
std::thread t3(&Worker::Run, &w, 3);
// 方式 4:std::function
std::function<void()> fn = []() { std::cout << "function\n"; };
std::thread t4(fn);
// 方式 5:可调用对象(仿函数)
struct Functor {
void operator()() { std::cout << "Functor\n"; }
};
std::thread t5(Functor{});
// 必须 join 或 detach
t1.join(); t2.join(); t3.join(); t4.join(); t5.join();
⭐ join() 和 detach() 的区别?joinable() 是什么?
join(): detach():
主线程 ──────┐ 主线程 ────────────────────→
│ 阻塞等待 │ 分离
│ ↓
子线程 ══════╪══════> 完成 子线程 ════════════> 完成
│ (后台运行,主线程不等待)
▼
主线程 ──────────────────→ 继续
cpp
std::thread t([]() { /* ... */ });
t.joinable(); // true:线程对象关联了活跃线程
t.join(); // 阻塞等待线程结束
// 或
t.detach(); // 分离线程
t.joinable(); // false:join/detach 后变为不可汇合
// ⚠️ 析构时如果 joinable() == true,会调用 std::terminate()!
🌈 最佳实践:使用
std::jthread(C++20),析构时自动join,并支持取消请求。
cpp
// C++20 jthread —— 自动 join
#include <thread>
{
std::jthread jt([](std::stop_token st) {
while (!st.stop_requested()) {
// 工作...
}
});
// 离开作用域时自动 join,不会忘记
}
三、同步原语
⭐ 互斥锁(Mutex)家族
┌───────────────────────────────────────────────────────────────┐
│ C++ 互斥锁家族 │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ std::mutex │ │ std::timed_mutex│ │
│ │ 基本互斥锁 │ │ 支持超时等待 │ │
│ │ │ │ try_lock_for() │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ ┌─────────────────┐ ┌─────────────────────────┐ │
│ │std::recursive │ │ std::shared_mutex │ │
│ │ _mutex │ │ (C++17) │ │
│ │ 同一线程可重复锁 │ │ 读写锁:多读者单写者 │ │
│ └─────────────────┘ └─────────────────────────┘ │
│ │
│ RAII 锁守卫: │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ lock_guard │ │ unique_lock │ │
│ │ 简单自动锁 │ │ 灵活锁(可手动 │ │
│ │ 不可手动解锁 │ │ lock/unlock) │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ ┌─────────────────┐ │
│ │ scoped_lock │ C++17,同时锁多个 mutex │
│ │ 避免死锁 │ 内部使用 std::lock() 算法 │
│ └─────────────────┘ │
└───────────────────────────────────────────────────────────────┘
⭐ 读写锁(shared_mutex)的原理
读写锁的并发控制:
读者 1 ──读──►│ │ ✅ 多个读者可以同时持有共享锁
读者 2 ──读──►│ 共享区 │ ✅
读者 3 ──读──►│ │ ✅
写者 1 ──写──►│ 共享区 │ ✅ 写者独占,其他读者/写者都阻塞
读者 4 ──读──►│ (等待中) │ ❌ 阻塞
写者 2 ──写──►│ (等待中) │ ❌ 阻塞
cpp
#include <shared_mutex>
class ThreadSafeMap {
mutable std::shared_mutex mtx_;
std::unordered_map<std::string, int> data_;
public:
// 读操作:共享锁(多个线程可同时读)
int Get(const std::string& key) const {
std::shared_lock<std::shared_mutex> lock(mtx_);
auto it = data_.find(key);
return it != data_.end() ? it->second : -1;
}
// 写操作:独占锁(只允许一个线程写)
void Set(const std::string& key, int value) {
std::unique_lock<std::shared_mutex> lock(mtx_);
data_[key] = value;
}
};
四、死锁
⭐ 死锁的四个必要条件
┌─────────────────────────────────────────────────┐
│ 死锁四个必要条件 │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ ① 互斥条件 │ │ ② 持有并等待 │ │
│ │ │ │ │ │
│ │ 资源一次只能 │ │ 持有一个资源 │ │
│ │ 被一个线程使用│ │ 等待获取另一个│ │
│ └──────────────┘ └──────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ ③ 不可剥夺 │ │ ④ 循环等待 │ │
│ │ │ │ │ │
│ │ 资源只能由 │ │ 线程之间形成 │ │
│ │ 持有者主动释放│ │ 环形等待链 │ │
│ └──────────────┘ └──────────────┘ │
│ │
│ 四个条件同时满足 → 死锁 │
│ 破坏任意一个条件 → 可以避免死锁 │
└─────────────────────────────────────────────────┘
⭐ 死锁的图解
经典死锁场景:
线程 A: 线程 B:
lock(mutex1); ✅ lock(mutex2); ✅
lock(mutex2); ❌ 等待B释放 lock(mutex1); ❌ 等待A释放
线程A ──持有──→ mutex1
↑ ↓
等待 等待
↑ ↓
mutex2 ←──持有── 线程B
形成环形等待 → 死锁!
解决方案:
cpp
std::mutex m1, m2;
// ❌ 可能死锁
void ThreadA() {
std::lock_guard<std::mutex> l1(m1);
std::lock_guard<std::mutex> l2(m2); // 可能等待
}
void ThreadB() {
std::lock_guard<std::mutex> l2(m2);
std::lock_guard<std::mutex> l1(m1); // 反向加锁 → 死锁
}
// ✅ 方案1:固定加锁顺序
void ThreadA_Fixed() {
std::lock_guard<std::mutex> l1(m1); // 总是先锁 m1
std::lock_guard<std::mutex> l2(m2); // 再锁 m2
}
void ThreadB_Fixed() {
std::lock_guard<std::mutex> l1(m1); // 同样先锁 m1
std::lock_guard<std::mutex> l2(m2);
}
// ✅ 方案2:scoped_lock(C++17,推荐)
void ThreadA_Best() {
std::scoped_lock lock(m1, m2); // 原子地同时锁定,内部避免死锁
}
// ✅ 方案3:std::lock + adopt_lock
void ThreadA_Lock() {
std::lock(m1, m2); // 原子锁定两个
std::lock_guard<std::mutex> l1(m1, std::adopt_lock);
std::lock_guard<std::mutex> l2(m2, std::adopt_lock);
}
五、条件变量与生产者-消费者模型
⭐ 条件变量工作原理
┌──────────────────────────────────────────────────────┐
│ 条件变量 (condition_variable) 工作流程 │
│ │
│ 等待方(消费者): │
│ ┌─────────────────────────────────────────────┐ │
│ │ ① unique_lock<mutex> lock(mtx) │ │
│ │ ② cv.wait(lock, []{return !queue.empty()}) │ │
│ │ ├── 检查谓词 → false │ │
│ │ ├── 自动释放锁 + 进入等待 │ │
│ │ ├── ...(休眠)... │ │
│ │ ├── 被通知唤醒 │ │
│ │ ├── 自动重新获取锁 │ │
│ │ ├── 再次检查谓词 → true │ │
│ │ └── wait() 返回 │ │
│ │ ③ 处理数据 │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ 通知方(生产者): │
│ ┌─────────────────────────────────────────────┐ │
│ │ ① lock_guard<mutex> lock(mtx) │ │
│ │ ② 修改共享数据(如 queue.push()) │ │
│ │ ③ lock 释放后 │ │
│ │ ④ cv.notify_one() 或 cv.notify_all() │ │
│ └─────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
⭐ 生产者-消费者完整实现
┌──────────┐ ┌──────────────┐ ┌──────────┐
│ 生产者 1 │────→│ │────→│ 消费者 1 │
│ 生产者 2 │────→│ 有界队列 │────→│ 消费者 2 │
│ 生产者 3 │────→│ (Bounded Q) │────→│ 消费者 3 │
└──────────┘ └──────────────┘ └──────────┘
mutex + 2个CV
cv_not_full(生产者等待)
cv_not_empty(消费者等待)
cpp
#include <queue>
#include <mutex>
#include <condition_variable>
#include <thread>
template <typename T>
class BoundedQueue {
std::queue<T> queue_;
size_t capacity_;
std::mutex mtx_;
std::condition_variable cv_not_full_;
std::condition_variable cv_not_empty_;
public:
explicit BoundedQueue(size_t cap) : capacity_(cap) {}
void Push(const T& item) {
std::unique_lock<std::mutex> lock(mtx_);
cv_not_full_.wait(lock, [this]() {
return queue_.size() < capacity_;
});
queue_.push(item);
cv_not_empty_.notify_one();
}
T Pop() {
std::unique_lock<std::mutex> lock(mtx_);
cv_not_empty_.wait(lock, [this]() {
return !queue_.empty();
});
T item = std::move(queue_.front());
queue_.pop();
cv_not_full_.notify_one();
return item;
}
};
// 使用
BoundedQueue<int> q(10);
// 生产者线程
std::thread producer([&q]() {
for (int i = 0; i < 100; ++i) {
q.Push(i);
}
});
// 消费者线程
std::thread consumer([&q]() {
for (int i = 0; i < 100; ++i) {
int val = q.Pop();
std::cout << val << " ";
}
});
producer.join();
consumer.join();
虚假唤醒(Spurious Wakeup)是什么?
虚假唤醒:线程在没有收到 notify 的情况下被唤醒
正常流程: 虚假唤醒:
wait() → 休眠 wait() → 休眠
↓ ↓
notify() 唤醒 系统莫名唤醒(无 notify)
↓ ↓
检查条件 → true 检查条件 → false → 重新 wait()
解决方案:始终在循环中检查条件(wait 的谓词版本会自动处理)
cpp
// ❌ 可能因虚假唤醒出错
cv.wait(lock);
// 被唤醒后不检查条件,直接执行 → 可能出错
// ✅ 正确:使用谓词(内部会循环检查)
cv.wait(lock, []{ return !queue.empty(); });
// 等价于:
while (queue.empty()) {
cv.wait(lock);
}
六、原子操作与无锁编程
⭐ std::atomic 的原理
普通变量 (int): 原子变量 (atomic<int>):
线程A: 读取 → 修改 → 写回 线程A: 原子 RMW 操作
线程B: 读取 → 修改 → 写回 线程B: 原子 RMW 操作
线程A: 读取 x=0 线程A: fetch_add(1)
线程B: 读取 x=0 ┌──────────────┐
线程A: x=0+1, 写回 x=1 │ 硬件保证原子性 │
线程B: x=0+1, 写回 x=1 ❌ └──────────────┘
线程B: fetch_add(1)
期望 x=2,实际 x=1(数据竞争) 最终 x=2 ✅
⭐ 内存序(Memory Order)
┌───────────────────────────────────────────────────────────────┐
│ C++ 内存序 (Memory Order) │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ memory_order_relaxed(最宽松) │ │
│ │ • 只保证原子性 │ │
│ │ • 不保证顺序 │ │
│ │ • 性能最好 │ │
│ │ • 适用:计数器 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ↓ 更强 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ memory_order_acquire / memory_order_release │ │
│ │ • acquire:之后的读写不会重排到此操作之前 │ │
│ │ • release:之前的读写不会重排到此操作之后 │ │
│ │ • 配对使用,建立 happens-before 关系 │ │
│ │ • 适用:锁、标志位、生产者-消费者 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ↓ 更强 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ memory_order_seq_cst(最强,默认) │ │
│ │ • 顺序一致性 │ │
│ │ • 所有线程看到相同的操作顺序 │ │
│ │ • 性能最差 │ │
│ │ • 适用:不确定时的安全默认选择 │ │
│ └─────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────┘
cpp
#include <atomic>
// acquire-release 示例:无锁标志位
std::atomic<bool> ready{false};
int data = 0;
// 生产者线程
void Producer() {
data = 42; // 普通写
ready.store(true, std::memory_order_release); // release
// 保证 data=42 在 ready=true 之前对其他线程可见
}
// 消费者线程
void Consumer() {
while (!ready.load(std::memory_order_acquire)) {} // acquire
// 保证看到 ready=true 后,也能看到 data=42
assert(data == 42); // ✅ 保证成功
}
⭐ CAS(Compare-And-Swap)操作
CAS 原理(硬件原子指令):
compare_exchange_strong(expected, desired):
if (当前值 == expected) {
当前值 = desired; // 交换成功
return true;
} else {
expected = 当前值; // 更新 expected 为当前值
return false; // 交换失败,重试
}
cpp
// 无锁栈的简化实现
template <typename T>
class LockFreeStack {
struct Node {
T data;
Node* next;
};
std::atomic<Node*> head_{nullptr};
public:
void Push(const T& data) {
Node* new_node = new Node{data, head_.load()};
// CAS 循环:直到成功将 new_node 设为新的头节点
while (!head_.compare_exchange_weak(
new_node->next, new_node)) {
// 失败时 new_node->next 已被更新为当前 head
// 自动重试
}
}
};
七、async、future、promise
⭐ 异步编程模型图解
┌──────────────────────────────────────────────────────────────┐
│ C++ 异步编程模型 │
│ │
│ std::async: │
│ ┌──────────┐ 异步执行 ┌──────────┐ │
│ │ 调用者 │ ──────────────────→│ 异步任务 │ │
│ │ │ │ (新线程) │ │
│ │ future ◄─┼────── 结果通道 ────┼─ 计算结果 │ │
│ └──────────┘ └──────────┘ │
│ │ │
│ │ .get() 阻塞获取结果 │
│ ▼ │
│ │
│ std::promise + std::future: │
│ ┌──────────┐ ┌──────────┐ │
│ │ 线程 A │ │ 线程 B │ │
│ │ │ promise │ │ │
│ │ future ◄─┼──────────────────→│ 设置值 │ │
│ │ .get() │ set_value() │ 或异常 │ │
│ └──────────┘ └──────────┘ │
│ │
│ std::packaged_task: │
│ ┌──────────────┐ ┌──────────┐ │
│ │ packaged_task │──── 包装 ────→│ 可调用对象│ │
│ │ .get_future() │ └──────────┘ │
│ └──────┬───────┘ │
│ │ 可以传递给线程执行 │
│ ▼ │
│ ┌──────────┐ │
│ │ future │ ← 获取结果 │
│ └──────────┘ │
└──────────────────────────────────────────────────────────────┘
cpp
#include <future>
// 1. std::async —— 最简单的异步
auto future1 = std::async(std::launch::async, []() {
return 42;
});
int result = future1.get(); // 阻塞等待
// 2. std::promise + std::future —— 线程间传值
std::promise<int> prom;
std::future<int> fut = prom.get_future();
std::thread t([&prom]() {
prom.set_value(100); // 设置值
});
int val = fut.get(); // 100
t.join();
// 3. std::packaged_task —— 包装可调用对象
std::packaged_task<int(int)> task([](int x) { return x * x; });
auto future3 = task.get_future();
std::thread t2(std::move(task), 7);
int r = future3.get(); // 49
t2.join();
八、线程池
⭐ 线程池的设计
┌─────────────────────────────────────────────────────────┐
│ 线程池架构 │
│ │
│ 提交任务 │
│ ┌──────┐ │
│ │Task 1│─┐ │
│ └──────┘ │ ┌──────────────────────┐ │
│ ┌──────┐ ├─→│ │ ┌─────────────┐ │
│ │Task 2│─┤ │ 任务队列 │→ │ Worker 线程1│ │
│ └──────┘ │ │ (Thread-safe Queue) │→ │ Worker 线程2│ │
│ ┌──────┐ │ │ │→ │ Worker 线程3│ │
│ │Task 3│─┘ │ ┌───┬───┬───┬───┐ │→ │ Worker 线程4│ │
│ └──────┘ │ │ T │ T │ T │ T │ │ └─────────────┘ │
│ │ └───┴───┴───┴───┘ │ │
│ ┌──────┐ └──────────────────────┘ │
│ │Task N│─→ 互斥锁 + 条件变量 │
│ └──────┘ │
└─────────────────────────────────────────────────────────┘
cpp
#include <thread>
#include <queue>
#include <functional>
#include <mutex>
#include <condition_variable>
#include <future>
#include <vector>
class ThreadPool {
std::vector<std::thread> workers_;
std::queue<std::function<void()>> tasks_;
std::mutex mtx_;
std::condition_variable cv_;
bool stop_ = false;
public:
explicit ThreadPool(size_t threads) {
for (size_t i = 0; i < threads; ++i) {
workers_.emplace_back([this]() {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(mtx_);
cv_.wait(lock, [this]() {
return stop_ || !tasks_.empty();
});
if (stop_ && tasks_.empty()) return;
task = std::move(tasks_.front());
tasks_.pop();
}
task();
}
});
}
}
template <typename F, typename... Args>
auto Submit(F&& f, Args&&... args)
-> std::future<decltype(f(args...))> {
using RetType = decltype(f(args...));
auto task = std::make_shared<std::packaged_task<RetType()>>(
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
);
auto future = task->get_future();
{
std::lock_guard<std::mutex> lock(mtx_);
tasks_.emplace([task]() { (*task)(); });
}
cv_.notify_one();
return future;
}
~ThreadPool() {
{
std::lock_guard<std::mutex> lock(mtx_);
stop_ = true;
}
cv_.notify_all();
for (auto& w : workers_) w.join();
}
};
// 使用
ThreadPool pool(4);
auto result = pool.Submit([](int a, int b) { return a + b; }, 3, 5);
std::cout << result.get(); // 8
九、常见并发数据结构对比
⭐ 线程安全的选择
┌────────────────────────────────────────────────────────────────┐
│ 线程安全方案选择决策图 │
│ │
│ 需要共享数据? │
│ ╱ ╲ │
│ 是 否 │
│ ╱ ╲ │
│ 需要修改? 无需同步,直接用 │
│ ╱ ╲ │
│ 是 否(只读) │
│ ╱ ╲ │
│ 访问频率高? const + 共享即可 │
│ ╱ ╲ │
│ 是 否 │
│ ╱ ╲ │
│ atomic mutex │
│(无锁) (有锁) │
│ ╱ ╲ │
│ 读多写少 读写均衡 │
│ ╱ ╲ │
│ shared_mutex mutex │
│ (读写锁) (互斥锁) │
└────────────────────────────────────────────────────────────────┘
| 场景 | 推荐方案 | 说明 |
|---|---|---|
| 简单计数器 | std::atomic<int> | 无锁,性能最好 |
| 简单标志位 | std::atomic<bool> | 无锁 |
| 读多写少的容器 | shared_mutex + 容器 | 读不阻塞,写独占 |
| 读写均衡的容器 | mutex + 容器 | 简单可靠 |
| 生产者-消费者 | mutex + condition_variable + queue | 经典模式 |
| 高性能场景 | 无锁队列(CAS) | 复杂,需仔细设计 |
十、总结知识图谱
📑 文章目录
💬 评论