C++ 并发编程面试题总结

✍️ Fer·📅 2026年8月18日·👁 1 次阅读
c++开发
📚 系列:现代 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
            // 自动重试
        }
    }
};

七、asyncfuturepromise

⭐ 异步编程模型图解

┌──────────────────────────────────────────────────────────────┐
│                    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)复杂,需仔细设计

十、总结知识图谱

💬 评论

加载评论中...
📑 文章目录