进程与线程深度解析
本文档采用"问题驱动"的方式讲解,每个主题都按照以下结构展开:
- 问题背景 - 为什么需要这个概念?解决什么问题?
- 设计思路 - 设计师是如何思考来解决这个问题的?
- 具体实现 - 从原理到代码,底层机制是什么?
- 常见陷阱/面试题 - 实际使用中的问题和面试常考点
目录
- 进程与线程基础
- 数据竞争与同步问题
- 互斥锁
- 条件变量
- 信号量
- 原子操作与CAS
- 可见性、原子性、有序性
- 内存序
- 死锁与避免
- 原子操作的底层原理
- volatile关键字
- 读写锁与自旋锁
- 线程池
- 进程间通信
- 常见面试题
- RCU (Read-Copy-Update)
- Seqlock (序列锁)
- 线程局部存储 (TLS)
- 屏障 (Barrier)
- 无锁数据结构
- 上下文切换与调度
- 进程间通信详解
- 常见并发模式
- 性能优化技巧
- 调试与诊断
一、进程与线程基础
1.1 问题背景:为什么需要进程和线程?
1.1.1 为什么要有多任务?
想象一下你在用电脑:
- 边听音乐边写代码
- 后台下载文件的同时浏览网页
- 播放器在播放音乐,浏览器在渲染页面
如果没有多任务,你的电脑只能一件事一件事做——听音乐时不能写代码,下载文件时不能浏览网页。这得多痛苦?
问题:如何让计算机同时做多件事情?
1.1.2 进程 vs 线程 - 为什么要区分?
场景:开一家餐厅
进程 = 一家独立的餐厅
- 有自己的厨房、仓库、员工
- 餐厅之间互不影响
- 开一家新餐厅成本很高(需要全新资源)
线程 = 餐厅里的员工
- 共享餐厅的厨房、仓库
- 员工之间可以协作
- 增加一个新员工成本很低
问题:那什么时候用进程?什么时候用线程?
| 特性 | 进程 | 线程 |
|---|---|---|
| 资源分配 | 独立(内存、文件等) | 共享(内存、文件等) |
| 创建成本 | 高(需要分配资源) | 低(共享资源) |
| 通信成本 | 高(需要IPC) | 低(直接共享内存) |
| 隔离性 | 高(一个崩溃不影响另一个) | 低(一个崩溃整个进程崩溃) |
| 并行性 | 多核并行 | 多线程并行 |
| 适用场景 | 需要隔离、独立运行 | 需要协作、共享数据 |
1.1.3 线程的内存模型 - 为什么需要了解这个?
进程内存空间
┌─────────────────────────────────────────────┐
│ 内核空间 │ ← 操作系统管理
├─────────────────────────────────────────────┤
│ 主线程栈 ↓ │
├─────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────┐ │
│ │ mmap 区域 │ │
│ │ ├─ 共享库 │ │
│ │ ├─ 子线程栈 1 │ │
│ │ ├─ 子线程栈 2 │ │
│ │ └─ ... │ │
│ └─────────────────────────────────────┘ │
│ │
├─────────────────────────────────────────────┤
│ 堆 ↑ │
├─────────────────────────────────────────────┤
│ BSS 段(未初始化全局变量) │
├─────────────────────────────────────────────┤
│ 数据段(已初始化全局变量) │
├─────────────────────────────────────────────┤
│ 代码段 │
└─────────────────────────────────────────────┘
关键问题:
- 共享的:代码段、数据段、堆、所有线程可见的全局变量
- 独立的:每个线程有自己的栈、寄存器、程序计数器
这意味着:
- 堆上的数据:线程共享 → 需要同步
- 栈上的数据:线程独立 → 无需同步
1.2 设计思路:如何设计进程和线程?
1.2.1 进程的设计目标
目标:让多个程序能够"同时"运行,且互不干扰
设计思路:
- 虚拟地址空间:每个进程有自己的"假装独占"的内存空间
- 隔离:一个进程崩溃不影响其他进程
- 资源抽象:文件、网络、内存都抽象成"资源"
1.2.2 线程的设计目标
目标:让同一个进程内的多个任务能够"同时"执行,且高效协作
设计思路:
- 共享资源:线程共享进程的内存和资源
- 轻量创建:不需要重新分配资源,创建快
- 高效通信:直接读写共享内存
1.3 具体实现:进程和线程是如何实现的?
1.3.1 进程的实现
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
printf("主进程 PID: %d\n", getpid());
pid_t pid = fork(); // 创建子进程
if (pid == 0) {
// 子进程
printf("子进程 PID: %d, 父进程 PID: %d\n",
getpid(), getppid());
sleep(2);
return 0;
} else if (pid > 0) {
// 父进程
printf("父进程等待子进程 %d\n", pid);
wait(NULL); // 等待子进程
printf("子进程已结束\n");
}
return 0;
}
fork 的原理:
fork 前:
┌─────────────────────┐
│ 父进程 │
│ 代码、数据、堆、栈 │
└─────────────────────┘
fork 时(写时复制):
┌─────────────────────┐ ┌─────────────────────┐
│ 父进程 │ │ 子进程 │
│ 代码(共享) │ │ 代码(共享) │
│ 数据(副本) │ │ 数据(副本) │
│ 堆(副本) │ │ 堆(副本) │
│ 栈(副本) │ │ 栈(副本) │
└─────────────────────┘ └─────────────────────┘
1.3.2 线程的实现
#include <iostream>
#include <thread>
#include <vector>
void worker(int id) {
std::cout << "线程 " << id << " 开始执行\n";
// 模拟工作
for (int i = 0; i < 3; i++) {
std::cout << "线程 " << id << " 工作中...\n";
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
std::cout << "线程 " << id << " 结束\n";
}
int main() {
std::vector<std::thread> threads;
// 创建 4 个线程
for (int i = 0; i < 4; i++) {
threads.emplace_back(worker, i);
}
// 等待所有线程结束
for (auto& t : threads) {
t.join();
}
std::cout << "所有线程执行完毕\n";
return 0;
}
线程创建 vs 进程创建:
| 操作 | 线程 | 进程 |
|---|---|---|
| 创建调用 | pthread_create / std::thread | fork / CreateProcess |
| 栈分配 | 1-8MB(可配置) | 1-8MB |
| 内核对象 | 只需线程内核对象 | 需要进程内核对象 + 资源 |
| 时间 | 约 1-10μs | 约 100-1000μs |
1.4 常见陷阱/面试题
面试题 1:进程和线程的区别?
参考答案:
- 资源:进程拥有独立资源,线程共享进程资源
- 开销:线程创建/切换开销小
- 通信:线程间通信简单(共享内存),进程间通信复杂
- 隔离:进程隔离性好,线程隔离性差
- 崩溃:一个线程崩溃会导致整个进程崩溃
面试题 2:线程越多越好吗?
参考答案: 不是。线程过多会导致:
- 上下文切换开销增大
- 内存消耗增加
- 调度复杂度增加
- 同步开销增加
最佳线程数公式:
- CPU 密集型:CPU 核心数
- IO 密集型:CPU 核心数 × 2
- 混合型:CPU 核心数 × (1 + IO时间/计算时间)
二、数据竞争与同步问题
2.1 问题背景:为什么需要同步?
2.1.1 什么是数据竞争?
想象银行取款的场景:
账户余额:1000 元
线程 A (ATM 取款 100) 线程 B (柜台取款 200)
↓ ↓
1. 读取余额:1000 1. 读取余额:1000
↓ ↓
2. 计算:1000-100=900 2. 计算:1000-200=800
↓ ↓
3. 写入余额:900 3. 写入余额:800
↓ ↓
最终余额:800 元 ← 错了!应该是 700 元!
问题根源:balance = balance - amount 看起来是一条语句,实际上是三条 CPU 指令:
- 读取 (LOAD): 从内存读取 balance 到寄存器
- 计算 (MODIFY): 在寄存器中做减法
- 写入 (STORE): 把结果写回内存
当两个线程同时执行时,可能发生:
- 线程 A 读取了 1000
- 线程 B 也读取了 1000
- 线程 A 写入 900
- 线程 B 写入 800(覆盖了 A 的结果)
2.1.2 代码示例:数据竞争
#include <iostream>
#include <thread>
#include <vector>
int balance = 1000; // 共享数据
void withdraw(int amount) {
// 这三步操作不是原子的!
int current = balance; // 1. 读取
current -= amount; // 2. 计算
balance = current; // 3. 写入
}
int main() {
std::thread t1(withdraw, 100); // ATM 取 100
std::thread t2(withdraw, 200); // 柜台取 200
t1.join();
t2.join();
std::cout << "最终余额:" << balance << std::endl;
// 可能是 800 或 900,但绝不是 700!
return 0;
}
2.1.3 竞态条件 vs 数据竞争
| 概念 | 定义 | 例子 |
|---|---|---|
| 数据竞争 | 多个线程同时访问同一内存,至少一个写 | 两个线程同时写 balance |
| 竞态条件 | 结果依赖于线程执行顺序 | 取款后余额错误 |
注意:所有竞态条件都源于数据竞争,但数据竞争不一定导致错误结果(只是可能)。
2.2 设计思路:如何解决数据竞争?
2.2.1 核心问题
问题:多个线程同时访问共享数据,导致结果不确定
2.2.2 设计方案
方案一:互斥(Mutual Exclusion)
- 同一时刻只允许一个线程访问
- 就像洗手间一次只能一个人用
方案二:原子操作(Atomic Operation)
- 把"读取-计算-写入"变成不可分割的操作
- 就像按电梯按钮,要么按下,要么没按下
方案三:线程局部存储(Thread-Local Storage)
- 每个线程有自己的数据副本
- 就像每人都有自己的私人碗筷
2.3 具体实现:如何实现同步?
2.3.1 互斥锁实现
#include <iostream>
#include <thread>
#include <mutex>
int balance = 1000;
std::mutex mtx; // 互斥锁
void withdraw_safe(int amount) {
mtx.lock(); // 加锁
// 临界区:同一时刻只有一个线程能执行这里
int current = balance;
current -= amount;
balance = current;
mtx.unlock(); // 解锁
}
// 或者使用 RAII(推荐)
void withdraw_safer(int amount) {
std::lock_guard<std::mutex> lock(mtx); // 构造时加锁,析构时解锁
balance -= amount;
}
int main() {
std::thread t1(withdraw_safer, 100);
std::thread t2(withdraw_safer, 200);
t1.join();
t2.join();
std::cout << "最终余额:" << balance << std::endl;
// 现在一定是 700!
return 0;
}
2.3.2 原子操作实现
#include <atomic>
#include <thread>
#include <iostream>
std::atomic<int> balance(1000); // 原子变量
void withdraw_atomic(int amount) {
// 原子操作:不可分割
balance.fetch_sub(amount); // 原子减法
}
int main() {
std::thread t1(withdraw_atomic, 100);
std::thread t2(withdraw_atomic, 200);
t1.join();
t2.join();
std::cout << "最终余额:" << balance.load() << std::endl;
// 现在一定是 700!
return 0;
}
2.4 常见陷阱/面试题
面试题 1:数据竞争会导致什么问题?
参考答案:
- 结果不确定(依赖于线程执行顺序)
- 数据损坏(读到脏数据)
- 内存泄漏(锁没释放)
- 死锁(多个锁相互等待)
面试题 2:如何检测数据竞争?
参考答案:
- 工具检测:ThreadSanitizer(TSan)、Helgrind、Intel Inspector
- 代码审查:检查所有共享变量的访问
- 压力测试:多线程高并发测试
三、互斥锁
3.1 问题背景:为什么需要互斥锁?
3.1.1 锁的本质
想象洗手间:
- 门上有一把锁
- 一个人进去后锁门
- 其他人只能在门外等待
- 出来时解锁
问题:如何让多个线程"排队"访问共享资源?
3.1.2 不用锁的后果
// 危险!没有同步
void unsafe_increment() {
// 假设 balance = 0
int temp = balance; // 线程A读=0,线程B也读=0
temp = temp + 1; // 线程A计算=1,线程B计算=1
balance = temp; // 线程A写=1,线程B写=1(覆盖了A的结果)
// 结果:balance = 1,应该是 2!
}
3.2 设计思路:如何设计互斥锁?
3.2.1 基本要求
- 互斥:同一时刻只有一个线程能获得锁
- 公平:避免线程饿死
- 高效:无竞争时快速获取
3.2.2 锁的演进
┌─────────────────────────────────────────────────────┐
│ 锁的实现演进 │
├─────────────────────────────────────────────────────┤
│ │
│ 自旋锁(Spinlock) │
│ ↓ │
│ 简单,但浪费CPU(忙等待) │
│ │
│ 乐观锁(Try Lock + 自旋一段时间) │
│ ↓ │
│ 短时间等待用自旋,减少开销 │
│ │
│ 悲观锁(Mutex,竞争时睡眠) │
│ ↓ │
│ 长时间等待让出CPU,避免浪费 │
│ │
│ 混合锁(自适应锁,先自旋再睡眠) │
│ ↓ │
│ Linux pthread_mutex 的实现方式 │
│ │
└─────────────────────────────────────────────────────┘
3.3 具体实现:互斥锁是如何工作的?
3.3.1 简单的自旋锁实现
// 基于 CAS 的自旋锁
class SpinLock {
std::atomic<bool> locked{false};
public:
void lock() {
// 如果 locked 是 false,设置为 true,返回 true
// 如果 locked 是 true,返回 false,继续循环
while (locked.exchange(true)) {
// 自旋等待
}
}
void unlock() {
locked.store(false);
}
};
3.3.2 Linux pthread_mutex 的实现
// Linux glibc 的 pthread_mutex 结构(简化)
struct pthread_mutex {
int __lock; // 0=未锁定, 1=锁定(无等待者), 2=锁定(有等待者)
unsigned int __count; // 重入计数
int __owner; // 持有者线程ID
// ...
};
// 加锁流程
int pthread_mutex_lock(pthread_mutex_t* mutex) {
// 1. 尝试原子获取锁(用户态)
if (atomic_compare_exchange_weak(&mutex->__lock, 0, 1)) {
return 0; // 成功获取,无需进入内核
}
// 2. 竞争激烈,进入内核等待
sys_futex(&mutex->__lock, FUTEX_WAIT, ...);
}
3.3.3 Futex 的核心思想
Futex = Fast Userspace Mutex
无竞争时(大多数情况):
┌─────────────────────────────────────┐
│ 用户态原子操作获取锁 │
│ 耗时:~10-50ns │
│ 不需要进入内核! │
└─────────────────────────────────────┘
有竞争时:
┌─────────────────────────────────────┐
│ 用户态尝试失败 │ ~50ns
│ ↓ │
│ 系统调用进入内核 │ ~100-500ns
│ ↓ │
│ 线程加入等待队列 │
│ ↓ │
│ 线程被挂起(上下文切换) │ ~1-10μs
│ ↓ │
│ 等待锁释放... │
│ ↓ │
│ 被唤醒,重新调度 │ ~1-10μs
│ ↓ │
│ 返回用户态 │ ~100-500ns
└─────────────────────────────────────┘
3.4 常见陷阱/面试题
面试题 1:lock_guard 和 unique_lock 的区别?
参考答案:
| 特性 | lock_guard | unique_lock |
|---|---|---|
| 灵活性 | 简单,析构时解锁 | 可随时解锁,可重入 |
| 功能 | 只在作用域结束时解锁 | 可延迟加锁、可尝试加锁 |
| 性能 | 稍轻量 | 稍重 |
| 适用 | 简单临界区 | 需要灵活控制的场景 |
面试题 2:为什么推荐使用 lock_guard 而不是手动 lock/unlock?
参考答案:
- 异常安全:即使抛出异常,析构函数也会解锁
- 避免死锁:不会忘记解锁
- 代码简洁:减少出错概率
// ❌ 危险:可能忘记解锁
void dangerous() {
mtx.lock();
if (something_wrong) {
return; // 忘记解锁!
}
mtx.unlock();
}
// ✅ 安全:RAII 自动解锁
void safe() {
std::lock_guard<std::mutex> lock(mtx);
if (something_wrong) {
return; // 自动解锁
}
}
四、条件变量
4.1 问题背景:为什么需要条件变量?
4.1.1 生产者-消费者问题
想象餐厅厨房:
- 生产者:厨师做菜
- 消费者:服务员端菜
- 缓冲区:菜品暂存区
问题:
- 缓冲区空时,消费者怎么办?→ 等待
- 缓冲区满时,生产者怎么办?→ 等待
- 如何通知对方?→ 条件变量
4.1.2 不用条件变量的后果
// ❌ 错误方式:忙等待
void consumer_bad() {
while (buffer.empty()) {
// 一直循环检查,浪费 CPU!
}
// 取数据
}
// ❌ 错误方式:睡眠等待
void consumer_worse() {
while (buffer.empty()) {
mtx.unlock();
sleep(1); // 可能错过通知,白等!
mtx.lock();
}
// 取数据
}
4.2 设计思路:如何实现等待通知?
4.2.1 条件变量的核心思想
等待-通知机制:
- 线程等待某个条件成立
- 条件成立时,其他线程通知等待者
- 等待者被唤醒,继续执行
4.2.2 为什么 wait 要在循环里?
虚假唤醒(Spurious Wakeup):
- 操作系统可能没有发送通知就唤醒线程
- 多个等待者被唤醒,但只有一个能满足条件
- 所以要用 while 循环检查条件!
4.3 具体实现:条件变量是如何工作的?
4.3.1 生产者-消费者实现
#include <queue>
#include <mutex>
#include <condition_variable>
#include <thread>
#include <iostream>
std::queue<int> buffer; // 共享缓冲区
std::mutex mtx; // 互斥锁
std::condition_variable cv; // 条件变量
const int MAX_SIZE = 10; // 缓冲区最大容量
// 生产者
void producer(int id) {
for (int i = 0; i < 20; i++) {
std::unique_lock<std::mutex> lock(mtx);
// 等待:缓冲区不满
cv.wait(lock, [] { return buffer.size() < MAX_SIZE; });
// 生产数据
buffer.push(i);
std::cout << "生产者" << id << "生产:" << i << std::endl;
// 通知消费者
cv.notify_one();
}
}
// 消费者
void consumer() {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
// 等待:缓冲区不空
cv.wait(lock, [] { return !buffer.empty(); });
// 消费数据
int data = buffer.front();
buffer.pop();
std::cout << "消费者消费:" << data << std::endl;
// 通知生产者
cv.notify_one();
// 退出条件
if (data == 19) break;
}
}
int main() {
std::thread p1(producer, 1);
std::thread p2(producer, 2);
std::thread c(consumer);
p1.join();
p2.join();
c.join();
return 0;
}
4.3.2 条件变量的工作原理
线程 A(等待者) 线程 B(通知者)
↓ ↓
lock(mutex) lock(mutex)
↓ ↓
while (!condition) // 修改条件
↓ ↓
cond_wait(cond, mutex) ←─────── condition = true
│ ↓
├──→ 原子释放 mutex cond_signal(cond)
│ ↓
├──→ 加入等待队列 unlock(mutex)
│ (睡眠等待)
│
├──← 被唤醒
│
├──→ 重新获取 mutex
↓
检查条件(循环)
4.4 常见陷阱/面试题
面试题 1:为什么 wait 要用 while 而不是 if?
参考答案:
- 虚假唤醒:POSIX 标准允许条件变量在没有 signal 的情况下唤醒
- 竞争条件:被唤醒时,条件可能已经被其他线程改变
- 安全:while 循环确保条件真正满足才继续
// ❌ 错误
if (buffer.empty()) {
cv.wait(lock);
}
// ✅ 正确
while (buffer.empty()) {
cv.wait(lock);
}
// 或者使用带谓词的 wait(推荐)
cv.wait(lock, [] { return !buffer.empty(); });
面试题 2:notify_one vs notify_all 的区别?
参考答案:
notify_one:唤醒一个等待者(如果只有一个消费者,用这个)notify_all:唤醒所有等待者(如果多个消费者,可能都需要知道)
五、信号量
5.1 问题背景:为什么需要信号量?
5.1.1 场景:限制并发数
想象一个餐厅只有 3 张桌子:
- 来了 10 个客人
- 只能有 3 个人同时用餐
- 其他 7 个人需要等待
问题:如何限制同时访问资源的线程数量?
5.1.2 信号量 vs 互斥锁
| 特性 | 互斥锁 | 信号量 |
|---|---|---|
| 值 | 0 或 1 | 任意非负整数 |
| 用途 | 互斥访问 | 资源计数 + 互斥 |
| 所有权 | 有(谁加锁谁解锁) | 无 |
| 场景 | 保护临界区 | 生产者-消费者、限制并发数 |
5.2 设计思路:如何实现资源计数?
5.2.1 信号量的本质
// 信号量 = 计数器 + 等待队列
struct semaphore {
int count; // 可用资源数
wait_queue_t queue; // 等待的线程队列
};
// P 操作(等待)
void P(semaphore* s) {
s->count--;
if (s->count < 0) {
// 没有资源,加入等待队列
block(s->queue);
}
}
// V 操作(释放)
void V(semaphore* s) {
s->count++;
if (s->count <= 0) {
// 有线程在等待,唤醒一个
wakeup(s->queue);
}
}
5.3 具体实现:信号量的使用
5.3.1 限制并发连接数
#include <semaphore.h>
#include <pthread.h>
#include <unistd.h>
#define MAX_CONNECTIONS 3
sem_t connection_sem;
void* handle_connection(void* arg) {
// P 操作:获取一个连接槽位
sem_wait(&connection_sem);
printf("线程 %ld 获取连接\n", pthread_self());
// 模拟处理连接
sleep(2);
printf("线程 %ld 释放连接\n", pthread_self());
// V 操作:释放连接槽位
sem_post(&connection_sem);
return NULL;
}
int main() {
sem_init(&connection_sem, 0, MAX_CONNECTIONS);
pthread_t threads[10];
for (int i = 0; i < 10; i++) {
pthread_create(&threads[i], NULL, handle_connection, NULL);
}
for (int i = 0; i < 10; i++) {
pthread_join(threads[i], NULL);
}
sem_destroy(&connection_sem);
return 0;
}
5.3.2 生产者-消费者
sem_t empty; // 空槽位数
sem_t full; // 已填充槽位数
sem_t mutex; // 互斥访问缓冲区
void producer() {
while (1) {
item = produce();
sem_wait(&empty); // 等待空槽位
sem_wait(&mutex); // 互斥访问
put_item(item);
sem_post(&mutex);
sem_post(&full); // 增加已填充数
}
}
void consumer() {
while (1) {
sem_wait(&full); // 等待有数据
sem_wait(&mutex);
item = get_item();
sem_post(&mutex);
sem_post(&empty); // 增加空槽位
consume(item);
}
}
5.4 常见陷阱/面试题
面试题 1:信号量和互斥锁的区别?
参考答案:
- 值:互斥锁只能是 0 或 1,信号量可以是任意整数
- 所有权:互斥锁有所有权(谁加锁谁解锁),信号量没有
- 用途:互斥锁用于互斥,信号量用于计数/同步
面试题 2:信号量可以实现互斥锁吗?
参考答案: 可以。把信号量初始化为 1,就变成了二元信号量,相当于互斥锁。
六、原子操作与CAS
6.1 问题背景:为什么需要原子操作?
6.1.1 为什么普通操作不是原子的?
int counter = 0;
// 看起来是一条语句
counter++;
// 实际是三条 CPU 指令!
// 1. LOAD counter -> eax (读取)
// 2. ADD eax, 1 (计算)
// 3. STORE eax -> counter (写入)
问题:多线程同时执行 counter++,结果可能不对!
时间线:
T1: 线程A读取 counter=0
T2: 线程B读取 counter=0
T3: 线程A计算 0+1=1
T4: 线程B计算 0+1=1
T5: 线程A写入 counter=1
T6: 线程B写入 counter=1
结果:counter=1,应该是 2!
6.1.2 原子操作 vs 锁
| 特性 | 原子操作 | 互斥锁 |
|---|---|---|
| 粒度 | 单个变量 | 代码块 |
| 性能 | 高(无锁) | 中(有开销) |
| 复杂度 | 低 | 中 |
| 适用场景 | 计数器、标志位 | 复杂数据结构 |
| 死锁风险 | 无 | 有 |
6.2 设计思路:如何实现原子操作?
6.2.1 硬件支持
现代 CPU 提供原子指令:
- x86:
LOCK前缀(如lock xadd) - ARM:
LDXR/STXR指令对
原子操作原理:
┌─────────────────────────────────────────────────────┐
│ CPU 缓存结构 │
│ │
│ CPU0 CPU1 CPU2 CPU3 │
│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │
│ │ L1 │ │ L1 │ │ L1 │ │ L1 │ │
│ │缓存│ │缓存│ │缓存│ │缓存│ │
│ └──┬─┘ └──┬─┘ └──┬─┘ └──┬─┘ │
│ │ │ │ │ │
│ └────────────┴─────┬──────┴────────────┘ │
│ │ │
│ ┌────┴────┐ │
│ │ 系统总线 │ │
│ └─────────┘ │
└─────────────────────────────────────────────────────┘
原子操作时:
1. CPU 发出 LOCK# 信号(锁定总线)
或
锁定缓存行(现代CPU优化)
2. 其他 CPU 无法访问该内存地址
3. 操作完成后释放
6.2.2 CAS 的核心思想
CAS = Compare And Swap(比较并交换)
传统锁: CAS(无锁):
┌─────────────────┐ ┌─────────────────┐
│ 加锁 │ │ 读取当前值 │
│ 读取 │ │ 比较 │
│ 修改 │ │ 如果相等则交换 │
│ 写入 │ │ 否则重试 │
│ 解锁 │ │ │
└─────────────────┘ └─────────────────┘
6.3 具体实现:原子操作的使用
6.3.1 基本原子操作
#include <atomic>
#include <iostream>
std::atomic<int> counter(0);
// 原子加载
int val = counter.load();
// 原子存储
counter.store(100);
// 原子交换
int old = counter.exchange(200); // 返回旧值
// 原子算术操作
counter.fetch_add(1); // 原子加,返回旧值
counter.fetch_sub(1); // 原子减
counter++; // 等价于 fetch_add(1)
// CAS 操作
int expected = counter.load();
int desired = expected + 1;
while (!counter.compare_exchange_weak(expected, desired)) {
// 如果失败,expected 会被更新为当前值
desired = expected + 1;
}
6.3.2 用 CAS 实现无锁栈
#include <atomic>
template<typename T>
class LockFreeStack {
private:
struct Node {
T data;
Node* next;
};
std::atomic<Node*> head;
public:
void push(T value) {
Node* new_node = new Node();
new_node->data = value;
do {
new_node->next = head.load();
} while (!head.compare_exchange_weak(new_node->next, new_node));
}
bool pop(T& result) {
Node* old_head;
do {
old_head = head.load();
if (old_head == nullptr) return false;
} while (!head.compare_exchange_weak(old_head, old_head->next));
result = old_head->data;
delete old_head;
return true;
}
};
6.4 常见陷阱/面试题
面试题 1:CAS 的 ABA 问题是什么?
参考答案: ABA 问题是指:
- 线程 A 读取值 A
- 线程 B 把值改成 B
- 线程 C 把值改回 A
- 线程 A 执行 CAS,发现值还是 A,认为没人改过
解决方案:使用带版本号的 CAS
struct stamped_ptr {
void* ptr;
uint64_t version;
};
std::atomic<stamped_ptr> ptr;
// 每次修改都增加版本号
// A(v1) → B(v2) → A(v3),CAS 会失败
面试题 2:compare_exchange_weak vs compare_exchange_strong 的区别?
参考答案:
- weak:可能在某些平台上更快,但可能假失败( spuriously fail)
- strong:不会假失败,但可能更慢
使用建议:
- 循环中使用 weak(性能更好)
- 非循环中使用 strong(确保正确性)
七、可见性、原子性、有序性
7.1 问题背景:为什么需要理解这三个概念?
7.1.1 三大问题的本质
| 问题 | 根本原因 | 示例 |
|---|---|---|
| 原子性 | 操作被拆分成多个CPU指令,可能被中断 | counter++ 不是原子的 |
| 可见性 | CPU缓存、寄存器、写缓冲区导致数据不一致 | 线程A写入,线程B看不到 |
| 有序性 | 编译器优化重排 + CPU乱序执行 | 代码顺序 ≠ 执行顺序 |
7.1.2 可见性问题示例
// 线程 1
data = 100; // 写入数据
ready = true; // 设置标志
// 线程 2
while (!ready); // 等待标志
use(data); // 使用数据 ← 可能看到 data=0!
问题:编译器/CPU 可能重排,导致 ready=true 在 data=100 之前执行!
7.2 设计思路:如何解决这三个问题?
7.2.1 解决方案对比
| 方案 | 原子性 | 可见性 | 有序性 | 性能 |
|---|---|---|---|---|
| 互斥锁 | ✅ | ✅ | ✅ | 较低 |
| 原子操作 + 内存序 | ✅ | ✅ | ✅ | 高 |
| volatile | ❌ | 部分 | ❌ | 最高 |
| 显式屏障 | ❌ | ✅ | ✅ | 中等 |
7.2.2 为什么锁是终极解决方案?
锁隐含了所有必要的保证:
- 原子性:临界区整体不可分割
- 可见性:锁释放时刷新缓存
- 有序性:锁获取/释放隐含内存屏障
7.3 具体实现:如何解决三大问题?
7.3.1 原子性问题
// ❌ 非原子操作
int counter = 0;
counter++; // 读-改-写,三步操作
// ✅ 方案一:使用锁
pthread_mutex_lock(&mutex);
counter++;
pthread_mutex_unlock(&mutex);
// ✅ 方案二:原子操作
std::atomic<int> counter{0};
counter.fetch_add(1); // 单条CPU指令
// ✅ 方案三:CAS循环(无锁)
int expected = counter.load();
while (!counter.compare_exchange_weak(expected, expected + 1));
7.3.2 可见性问题
int ready = 0;
int data = 0;
// ❌ 可能有可见性问题
// 线程1
data = 100;
ready = 1;
// 线程2
if (ready) {
printf("%d", data); // 可能读到 0!
}
// ✅ 方案一:内存屏障
data = 100;
__sync_synchronize(); // 全屏障
ready = 1;
// ✅ 方案二:atomic + 内存序
std::atomic<int> ready{0};
data = 100;
ready.store(1, std::memory_order_release);
// 线程2
if (ready.load(std::memory_order_acquire)) {
printf("%d", data); // 保证看到 100
}
7.3.3 有序性问题
// ✅ 方案一:锁(隐含内存屏障)
pthread_mutex_lock(&mutex);
// 临界区内的操作不会被重排到临界区外
pthread_mutex_unlock(&mutex);
// ✅ 方案二:原子操作 + 内存序
std::atomic<int> flag{0};
// 写端
data = 100;
flag.store(1, std::memory_order_release);
// 读端
while (flag.load(std::memory_order_acquire) != 1);
use(data);
7.4 常见陷阱/面试题
面试题 1:volatile 能用于多线程吗?
参考答案: 不能!volatile 只保证:
- 不被编译器优化掉
- 每次从内存读取
但不保证:
- 原子性(
counter++仍然是三步) - 可见性(CPU 缓存可能没刷新)
- 有序性(编译器/CPU 仍可能重排)
volatile 的正确用途:
- 硬件寄存器访问
- 信号处理函数中的标志
面试题 2:为什么需要内存序?
参考答案: 因为编译器和 CPU 都会优化(重排指令),这在单线程中没问题,但在多线程中会导致问题。内存序提供了控制重排的手段,让程序员可以指定"这个操作必须在那之前"。
八、内存序
8.1 问题背景:为什么需要内存序?
8.1.1 三级重排
┌─────────────────────────────────────────────────────┐
│ 三级重排 │
├─────────────────────────────────────────────────────┤
│ │
│ 1. 编译器重排 │
│ 优化代码执行顺序 │
│ │
│ 2. CPU 乱序执行 │
│ 提高指令级并行 │
│ │
│ 3. 缓存可见性问题 │
│ 写入顺序与可见顺序不同 │
│ │
└─────────────────────────────────────────────────────┘
8.1.2 指令重排导致的问题
// 线程 1: 初始化
data = 42; // 步骤 1: 写入数据
ready = true; // 步骤 2: 设置标志
// 线程 2: 检查
while (!ready); // 步骤 3: 等待标志
use(data); // 步骤 4: 使用数据
// 问题:编译器/CPU 可能重排成:
// ready = true;
// data = 42;
// 线程 2 可能看到 ready=true,但 data 还没写入!
8.2 设计思路:如何控制指令重排?
8.2.1 内存序的层次
从弱到强:
memory_order_relaxed (宽松序)
├─ 只保证原子性
├─ 不保证顺序
└─ 适用:计数器
memory_order_acquire (获取序)
├─ 读操作
├─ 保证之后的读写不会被重排到前面
└─ 适用:获取锁、读取标志
memory_order_release (释放序)
├─ 写操作
├─ 保证之前的读写不会被重排到后面
└─ 适用:释放锁、设置标志
memory_order_acq_rel (获取-释放序)
├─ 读 + 写
├─ acquire + release
└─ 适用:原子交换
memory_order_seq_cst (顺序一致序)
├─ 最严格
├─ 所有线程看到相同顺序
└─ 默认选项
8.3 具体实现:如何使用内存序?
8.3.1 典型使用场景
#include <atomic>
#include <iostream>
std::atomic<int> data(0);
std::atomic<bool> ready(false);
// 线程 1: 生产者
void producer() {
data.store(42, std::memory_order_relaxed); // 宽松序
ready.store(true, std::memory_order_release); // 释放序
}
// 线程 2: 消费者
void consumer() {
while (!ready.load(std::memory_order_acquire)); // 获取序
// 此时保证能看到 data = 42
std::cout << "data = " << data.load(std::memory_order_relaxed) << std::endl;
}
8.3.2 内存屏障的硬件实现
; x86 内存屏障指令
mfence ; 全屏障(StoreLoad)
lfence ; 读屏障(LoadLoad)
sfence ; 写屏障(StoreStore)
; ARM 内存屏障指令
dmb sy ; 全屏障
dmb ld ; 读屏障
dmb st ; 写屏障
8.4 常见陷阱/面试题
面试题 1:不同内存序的区别?
参考答案:
- relaxed:只保证操作是原子的,不保证顺序
- release:保证之前的操作不会被重排到后面
- acquire:保证之后的操作不会被重排到前面
- seq_cst:最强保证,所有线程看到相同顺序
面试题 2:什么时候用 relaxed 内存序?
参考答案: 只关心原子性,不关心顺序的场景,例如:
- 简单计数器
- 统计信息
- 性能监控
std::atomic<int> counter(0);
// 不需要顺序保证,只是计数
counter.fetch_add(1, std::memory_order_relaxed);
九、死锁与避免
9.1 问题背景:为什么会出现死锁?
9.1.1 死锁的四个必要条件
死锁产生需要同时满足四个条件:
1. 互斥条件
└─> 资源一次只能被一个线程占用
2. 请求与保持
└─> 线程持有资源的同时请求其他资源
3. 不剥夺
└─> 已获得的资源不能被强制剥夺
4. 循环等待
└─> 形成等待环:A→B→C→A
打破任一条件即可避免死锁!
9.1.2 死锁示例
std::mutex mtx1, mtx2;
// 线程 1
void thread1() {
mtx1.lock(); // 1. 锁 mtx1
std::this_thread::sleep_for(std::chrono::milliseconds(10));
mtx2.lock(); // 2. 锁 mtx2 (等待)
// 临界区
mtx2.unlock();
mtx1.unlock();
}
// 线程 2
void thread2() {
mtx2.lock(); // 1. 锁 mtx2
std::this_thread::sleep_for(std::chrono::milliseconds(10));
mtx1.lock(); // 2. 锁 mtx1 (等待)
// 临界区
mtx1.unlock();
mtx2.unlock();
}
// 死锁场景:
// 线程 1: 持有 mtx1,等待 mtx2
// 线程 2: 持有 mtx2,等待 mtx1
// 互相等待,永不执行!
9.2 设计思路:如何避免死锁?
9.2.1 避免死锁的方法
| 方法 | 原理 | 适用场景 |
|---|---|---|
| 固定顺序加锁 | 打破循环等待 | 多锁场景 |
| std::lock | 同时加锁 | 两个锁 |
| try_lock | 尝试加锁 | 可回退场景 |
| 避免嵌套锁 | 打破请求与保持 | 简单场景 |
9.3 具体实现:如何避免死锁?
9.3.1 固定顺序加锁
// ✅ 安全:固定顺序
void safe_lock() {
// 总是先锁 mtx1,再锁 mtx2
mtx1.lock();
mtx2.lock();
// 临界区
mtx2.unlock();
mtx1.unlock();
}
9.3.2 使用 std::lock
// ✅ 更安全:同时加锁
void safer_lock() {
std::lock(mtx1, mtx2); // 同时锁两个,避免死锁
std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
// 临界区
}
9.3.3 使用 std::scoped_lock (C++17)
// ✅ 最简单:C++17
void safest_lock() {
std::scoped_lock lock(mtx1, mtx2); // 自动同时加锁
// 临界区
}
9.3.4 尝试加锁(带超时)
bool try_lock_both() {
// 先尝试锁 mtx1
if (mtx1.try_lock()) {
// 再尝试锁 mtx2
if (mtx2.try_lock()) {
// 成功
return true;
}
mtx1.unlock();
}
return false; // 失败,稍后重试
}
9.4 常见陷阱/面试题
面试题 1:如何检测死锁?
参考答案:
- 运行时检测:使用工具如 Helgrind、ThreadSanitizer
- 代码审查:检查锁的获取顺序
- 超时机制:使用 try_lock 带超时
面试题 2:死锁和活锁的区别?
参考答案:
- 死锁:线程无法继续执行(互相等待)
- 活锁:线程一直在执行,但无法推进(不断重试)
十、原子操作的底层原理
10.1 问题背景:为什么需要了解底层原理?
10.1.1 CPU 缓存结构
┌─────────────────────────────────────────────────────┐
│ CPU 缓存结构 │
│ │
│ CPU0 CPU1 CPU2 CPU3 │
│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │
│ │ L1 │ │ L1 │ │ L1 │ │ L1 │ │
│ │缓存│ │缓存│ │缓存│ │缓存│ │
│ └──┬─┘ └──┬─┘ └──┬─┘ └──┬─┘ │
│ │ │ │ │ │
│ └────────────┴─────┬──────┴────────────┘ │
│ │ │
│ ┌────┴────┐ │
│ │ L3 │ (共享缓存) │
│ │ 缓存 │ │
│ └────┬────┘ │
│ │ │
│ ┌────┴────┐ │
│ │ 内存 │ │
│ └─────────┘ │
└─────────────────────────────────────────────────────┘
10.1.2 MESI 缓存一致性协议
┌──────────────────────────────────────────────────────┐
│ MESI 状态机 │
├──────────────────────────────────────────────────────┤
│ │
│ M (Modified) - 已修改,独占,需写回内存 │
│ E (Exclusive) - 独占,与内存一致 │
│ S (Shared) - 共享,与内存一致 │
│ I (Invalid) - 无效 │
│ │
│ 读取操作: │
│ - 缓存命中 → 直接使用 │
│ - 缓存未命中 → 从内存/其他CPU加载 │
│ │
│ 写入操作: │
│ - 发送 Invalidate 消息 │
│ - 其他CPU缓存行变 I │
│ - 本地缓存行变 M │
│ │
└──────────────────────────────────────────────────────┘
10.2 设计思路:如何实现原子操作?
10.2.1 硬件原子指令
; x86 的 LOCK 前缀
lock cmpxchg [rdi], rax ; 原子比较并交换
; ARM 的 LDXR/STXR 指令对
ldxr r0, [r1] ; 独占加载
stxr r2, r0, [r1] ; 独占存储,r2=0表示成功
10.3 具体实现:原子操作的性能
单核无竞争:
┌─────────────────────────────────────┐
│ 原子操作 ≈ 普通操作 + LOCK 前缀开销 │
│ 耗时:~10-20ns │
└─────────────────────────────────────┘
多核无竞争:
┌─────────────────────────────────────┐
│ 可能需要缓存行所有权转移 │
│ 耗时:~20-50ns │
└─────────────────────────────────────┘
多核高竞争:
┌─────────────────────────────────────┐
│ 缓存行在多个 CPU 间反复失效 │
│ 耗时:~100-500ns │
│ CPU 占用率高(自旋等待) │
└─────────────────────────────────────┘
10.4 常见陷阱/面试题
面试题 1:原子操作为什么比锁快?
参考答案:
- 无系统调用:大多数情况下在用户态完成
- 无上下文切换:不需要进入内核
- 细粒度:只影响一个变量
面试题 2:什么时候应该用锁而不是原子操作?
参考答案:
- 需要保护多个变量
- 操作复杂,不是简单的增减
- 需要更复杂的同步逻辑
十一、volatile关键字
11.1 问题背景:volatile 是什么?
11.1.1 volatile 的作用
// volatile 告诉编译器:
// 1. 不要优化掉对这个变量的读写
// 2. 每次都要从内存读取,不要用寄存器缓存
// 3. 不要重排 volatile 变量的访问顺序
volatile int* p = (volatile int*)0xFFFF0000;
// 写入硬件寄存器
*p = 0x01; // 编译器不会优化掉
// 读取硬件状态
int status = *p; // 每次都从地址读取
11.2 设计思路:volatile 的设计目标
目标:防止编译器优化,确保每次都访问内存
适用场景:
- 硬件寄存器访问
- 信号处理函数中的标志
- setjmp/longjmp 中的变量
11.3 具体实现:volatile 的限制
volatile int counter = 0;
// ❌ volatile 不保证原子性
counter++; // 仍然是 读-改-写 三步操作
// 反汇编:
// mov eax, [counter] ; 读
// add eax, 1 ; 改
// mov [counter], eax ; 写
// 中间可能被中断!
volatile int ready = 0;
int data = 0;
// ❌ volatile 不保证可见性
// 线程1
data = 100;
ready = 1;
// 线程2
while (!ready); // 可能永远看不到 ready 变成 1
// 因为 CPU 缓存可能没有刷新
11.4 常见陷阱/面试题
面试题:volatile vs atomic?
| 特性 | volatile | atomic |
|---|---|---|
| 禁止编译器优化 | ✅ | ✅ |
| 原子性 | ❌ | ✅ |
| 可见性 | ❌ | ✅ |
| 有序性 | ❌ | ✅ |
| 多线程安全 | ❌ | ✅ |
| 硬件寄存器 | ✅ | ❌ |
| 信号处理 | ✅ | ❌ |
十二、读写锁与自旋锁
12.1 问题背景:为什么需要读写锁?
12.1.1 读多写少场景
想象一个配置服务器:
- 大量读请求(读取配置)
- 少量写请求(修改配置)
问题:如果用普通锁,所有读操作也要排队,效率低!
解决方案:读写锁
- 读操作:可以并发(读读不互斥)
- 写操作:必须独占(写写互斥、读写互斥)
12.2 设计思路:如何实现读写锁?
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
// 读线程
void reader() {
pthread_rwlock_rdlock(&rwlock);
// 读取共享数据...
pthread_rwlock_unlock(&rwlock);
}
// 写线程
void writer() {
pthread_rwlock_wrlock(&rwlock);
// 修改共享数据...
pthread_rwlock_unlock(&rwlock);
}
12.3 自旋锁
12.3.1 什么时候用自旋锁?
| 特性 | 自旋锁 | 互斥锁 |
|---|---|---|
| 等待方式 | 忙等待 | 睡眠等待 |
| CPU 占用 | 高 | 低 |
| 上下文切换 | 无 | 有 |
| 适用场景 | 短时间持有 | 长时间持有 |
| 适用上下文 | 不可睡眠的场景 | 可睡眠的场景 |
12.3.2 自旋锁实现
// 基于 CAS 的自旋锁
void spin_lock(spinlock_t* lock) {
while (atomic_exchange(&lock->locked, 1) == 1) {
// 循环等待(自旋)
}
}
12.4 常见陷阱/面试题
面试题:读写锁的饥饿问题?
参考答案:
| 类型 | 特点 | 问题 |
|---|---|---|
| 读优先锁 | 新读者可以插队 | 写者可能饥饿 |
| 写优先锁 | 有写者等待时,新读者等待 | 读者可能饥饿 |
| 公平锁 | 按请求顺序分配 | 无饥饿,但性能较低 |
十三、线程池
13.1 问题背景:为什么需要线程池?
13.1.1 线程创建的成本
创建线程的成本:
1. 系统调用开销
└─> 进入内核态
2. 内存分配
└─> 栈空间 (默认 1-8MB)
3. 内核数据结构
└─> task_struct 等
4. 上下文切换
└─> 保存/恢复寄存器
频繁创建销毁线程 = 浪费资源!
13.2 设计思路:线程池的思想
不用线程池:
任务 1 → 创建线程 → 执行 → 销毁线程
任务 2 → 创建线程 → 执行 → 销毁线程
任务 3 → 创建线程 → 执行 → 销毁线程
...
成本高!
使用线程池:
┌─────────────────────────────────────┐
│ 线程池 │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │线程 1│ │线程 2│ │线程 3│ │线程 4│ │ ← 预先创建
│ └─────┘ └─────┘ └─────┘ └─────┘ │
│ ↑ ↑ ↑ ↑ │
│ └───────┴───────┴───────┘ │
│ 任务队列 │
└─────────────────────────────────────┘
任务 1,2,3,4... → 放入任务队列 → 空闲线程取走执行
线程复用,成本低!
13.3 具体实现:简单线程池
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <vector>
class ThreadPool {
public:
ThreadPool(size_t num_threads) {
for (size_t i = 0; i < num_threads; ++i) {
workers_.emplace_back([this] {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(queue_mutex_);
condition_.wait(lock, [this] {
return stop_ || !tasks_.empty();
});
if (stop_ && tasks_.empty()) {
return;
}
task = std::move(tasks_.front());
tasks_.pop();
}
task();
}
});
}
}
template<class F>
void submit(F&& f) {
{
std::unique_lock<std::mutex> lock(queue_mutex_);
tasks_.emplace(std::forward<F>(f));
}
condition_.notify_one();
}
~ThreadPool() {
{
std::unique_lock<std::mutex> lock(queue_mutex_);
stop_ = true;
}
condition_.notify_all();
for (std::thread& worker : workers_) {
worker.join();
}
}
private:
std::vector<std::thread> workers_;
std::queue<std::function<void()>> tasks_;
std::mutex queue_mutex_;
std::condition_variable condition_;
bool stop_ = false;
};
13.4 常见陷阱/面试题
面试题:线程池的最佳线程数?
参考答案:
- CPU 密集型:CPU 核心数
- IO 密集型:CPU 核心数 × 2
- 混合型:CPU 核心数 × (1 + IO时间/计算时间)
十四、进程间通信
14.1 问题背景:为什么需要进程间通信?
14.1.1 进程隔离
进程之间是隔离的,不能直接访问彼此的内存!
进程 A:
┌─────────────────────┐
│ 内存空间 A │
│ ... │
└─────────────────────┘
进程 B:
┌─────────────────────┐
│ 内存空间 B │
│ ... │
└─────────────────────┘
进程 A 无法直接访问进程 B 的内存!
问题:进程之间如何交换数据?
14.2 设计思路:进程间通信方式
| 方式 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 管道 | 内核缓冲区 | 简单 | 单向、亲缘进程 |
| 消息队列 | 内核消息链表 | 异步、格式灵活 | 有容量限制 |
| 共享内存 | 映射同一物理内存 | 最快 | 需要同步 |
| 信号量 | 计数器 | 同步 | 功能单一 |
| Socket | 网络/本地套接字 | 跨机器、通用 | 稍慢 |
14.3 具体实现:共享内存
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/shm.h>
#include <sys/ipc.h>
// 创建共享内存
int shm_id = shmget(IPC_PRIVATE, 1024, IPC_CREAT | 0666);
// 附加共享内存
char* shm_addr = (char*)shmat(shm_id, NULL, 0);
// 写入数据
strcpy(shm_addr, "Hello from process A!");
// 分离共享内存
shmdt(shm_addr);
// 在另一个进程中附加同一共享内存
// shm_id 需要通过某种方式传递(如命令行参数)
char* shm_addr2 = (char*)shmat(shm_id, NULL, SHM_RDONLY);
// 读取数据
printf("Received: %s\n", shm_addr2);
// 分离
shmdt(shm_addr2);
// 删除共享内存
shmctl(shm_id, IPC_RMID, NULL);
14.4 常见陷阱/面试题
面试题:共享内存 vs 消息队列的区别?
参考答案:
- 共享内存:直接访问内存,最快,但需要自己同步
- 消息队列:通过内核传递,有容量限制,内核负责同步
十五、常见面试题
15.1 基础概念题
面试题 1:进程和线程的区别?
参考答案:
- 资源:进程拥有独立资源,线程共享进程资源
- 开销:线程创建/切换开销小
- 通信:线程间通信简单(共享内存),进程间通信复杂
- 隔离:进程隔离性好,线程隔离性差
面试题 2:线程同步的方式有哪些?
参考答案:
- 互斥锁(mutex)
- 读写锁(rwlock)
- 条件变量(condition_variable)
- 信号量(semaphore)
- 原子操作(atomic)
- 自旋锁(spinlock)
15.2 进阶概念题
面试题 3:什么是死锁?如何避免?
参考答案: 死锁的四个必要条件:互斥、请求与保持、不剥夺、循环等待
避免方法:
- 固定顺序加锁
- 使用 std::lock 同时加锁
- 使用 try_lock 带超时
- 避免嵌套锁
面试题 4:volatile 和 atomic 的区别?
参考答案:
- volatile:只防止编译器优化,不保证原子性、可见性、有序性
- atomic:保证原子性、可见性、有序性,适用于多线程
面试题 5:内存序是什么?为什么要使用它?
参考答案: 内存序控制指令重排,保证多线程中的操作顺序。
使用场景:
- 性能优化:使用 relaxed 序
- 正确性:使用 release/acquire 序
15.3 编程题
面试题 6:实现一个线程安全的单例模式?
#include <mutex>
class Singleton {
private:
static std::atomic<Singleton*> instance;
static std::mutex mtx;
Singleton() {}
public:
// 双检查锁定(Double-Checked Locking)
static Singleton* get_instance() {
Singleton* tmp = instance.load(std::memory_order_acquire);
if (tmp == nullptr) {
std::lock_guard<std::mutex> lock(mtx);
tmp = instance.load(std::memory_order_relaxed);
if (tmp == nullptr) {
tmp = new Singleton();
instance.store(tmp, std::memory_order_release);
}
}
return tmp;
}
};
std::atomic<Singleton*> Singleton::instance{nullptr};
std::mutex Singleton::mtx;
面试题 7:生产者-消费者问题?
// 见本文档"条件变量"章节的实现
15.4 系统设计题
面试题 8:如何设计一个高并发的计数器?
参考答案:
- 简单场景:使用 atomic,无锁
- 高并发场景:使用分段锁(分桶),减少竞争
- 需要持久化:考虑 Redis 分布式计数器
// 分段锁计数器示例
class SegmentCounter {
static const int SEGMENTS = 16;
std::atomic<long> counters[SEGMENTS];
std::mutex segment_mutex;
public:
void increment() {
int seg = std::hash<std::thread::id>{}(std::this_thread::get_id()) % SEGMENTS;
counters[seg].fetch_add(1, std::memory_order_relaxed);
}
long total() {
long sum = 0;
for (int i = 0; i < SEGMENTS; i++) {
sum += counters[i].load(std::memory_order_relaxed);
}
return sum;
}
};
十六、RCU (Read-Copy-Update)
16.1 问题背景:为什么需要 RCU?
16.1.1 读多写少场景的困境
想象一个 DNS 服务器:
- 每秒 100 万次读请求
- 每秒只有 1 次写请求(更新 DNS 记录)
问题:如果用读写锁,写操作会阻塞所有读操作!
理想的解决方案:
- 读操作:无锁,不阻塞
- 写操作:复制-修改-更新
这就是 RCU (Read-Copy-Update) 的核心思想!
16.2 设计思路:RCU 的核心思想
初始状态:
┌─────────┐
│ 数据A │ ←─── 指针
└─────────┘
↑
读者1, 读者2, 读者3...
更新过程:
步骤1:复制数据,创建副本
步骤2:修改副本
步骤3:原子替换指针(所有新读请求看到新数据)
步骤4:等待所有旧读者完成,释放旧数据
关键:读者无锁,写者复制
16.3 具体实现:RCU 的使用
#include <atomic>
#include <thread>
#include <iostream>
struct Data {
int value;
Data* next;
};
// 全局指针
std::atomic<Data*> global_ptr;
void reader() {
// 读者:无需加锁,直接读取
Data* p = global_ptr.load(std::memory_order_acquire);
if (p) {
// 使用数据...
int val = p->value;
}
}
void writer(int new_value) {
// 写者:复制-修改-更新
Data* old = global_ptr.load(std::memory_order_relaxed);
// 1. 复制
Data* new_node = new Data();
new_node->value = new_value;
new_node->next = old->next;
// 2. 修改
new_node->value = new_value;
// 3. 原子替换指针
global_ptr.store(new_node, std::memory_order_release);
// 4. 等待旧读者完成(宽限期)
// 在实际使用中,需要 synchronize_rcu()
// 这里简化处理
delete old; // 实际应该等待宽限期
}
Linux 内核 RCU 示例:
// 读者(无锁)
void read_data() {
rcu_read_lock();
struct data* p = rcu_dereference(global_ptr);
// 使用 p...
rcu_read_unlock();
}
// 写者
void update_data(int new_value) {
struct data* old = global_ptr;
struct data* new = kmalloc(sizeof(*new));
*new = *old;
new->value = new_value;
rcu_assign_pointer(global_ptr, new);
synchronize_rcu(); // 等待所有读者退出
kfree(old);
}
16.4 常见陷阱/面试题
面试题:RCU 的优缺点?
参考答案:
- 优点:读操作无锁,极高读取性能
- 缺点:写操作需要复制内存,有开销;需要等待宽限期
适用场景:
- ✅ 读多写少
- ✅ 数据量小
- ✅ 可以容忍短暂的不一致
- ❌ 写操作频繁
- ❌ 数据量大
十七、Seqlock (Sequence Lock)
17.1 问题背景:为什么需要 Seqlock?
17.1.1 场景:高频读取 + 低频写入
想象一个时间戳服务:
- 每秒 100 万次读取当前时间
- 每秒只有 1 次更新时间
问题:读写锁可以,但有没有更快的方案?
解决方案:Seqlock - 用序列号检测冲突
17.2 设计思路:序列号的思想
核心思想:
- 写者:修改数据前序列号+1,修改后序列号+1(变成偶数)
- 读者:读取序列号 → 读取数据 → 再次读取序列号
- 如果两次序列号相同,说明没有写者干扰
- 如果序列号是奇数,说明正在写,等待
17.3 具体实现:Seqlock 实现
#include <atomic>
struct Data {
int value;
char name[64];
};
class Seqlock {
private:
std::atomic<unsigned> sequence_{0};
Data data_;
std::mutex mtx_;
public:
// 写者:加锁修改
void write(const Data& new_data) {
// 序列号变成奇数(表示正在写)
sequence_.fetch_add(1, std::memory_order_relaxed);
std::lock_guard<std::mutex> lock(mtx_);
data_ = new_data;
// 序列号变成偶数(表示写完成)
sequence_.fetch_add(1, std::memory_order_relaxed);
}
// 读者:无锁读取
bool read(Data* out) const {
unsigned seq1, seq2;
do {
// 第一次读取序列号
seq1 = sequence_.load(std::memory_order_acquire);
// 如果正在写(奇数),重试
if (seq1 & 1) continue;
// 读取数据
{
std::lock_guard<std::mutex> lock(mtx_);
*out = data_;
}
// 第二次读取序列号
seq2 = sequence_.load(std::memory_order_acquire);
} while (seq1 != seq2); // 序列号变化,重读
return true;
}
};
17.4 常见陷阱/面试题
面试题:Seqlock vs 读写锁?
| 特性 | Seqlock | 读写锁 |
|---|---|---|
| 读性能 | 极高(无锁) | 中等(加锁) |
| 写性能 | 中等 | 中等 |
| 写饥饿 | 可能 | 可能(读优先) |
| 数据类型 | 简单数据 | 复杂结构 |
十八、线程局部存储 (TLS)
18.1 问题背景:为什么需要 TLS?
18.1.1 场景:避免共享
想象每个线程有自己的:
- 错误码(errno)
- 随机数种子
- 事务ID
问题:如果用全局变量,所有线程共享,会冲突!
解决方案:线程局部存储 - 每个线程有自己的副本
18.2 设计思路:每个线程独立副本
全局变量:
┌─────────┐
│ X │ ←─── 所有线程共享,需要同步
└─────────┘
线程局部变量:
┌─────────┐
│ X_T1 │ ←─── 线程1 独享
└─────────┘
┌─────────┐
│ X_T2 │ ←─── 线程2 独享
└─────────┘
┌─────────┐
│ X_T3 │ ←─── 线程3 独享
└─────────┘
18.3 具体实现:TLS 的使用
#include <iostream>
#include <thread>
#include <random>
// 方式1:__thread 关键字(C++11 之前)
__thread int thread_local_var = 0;
// 方式2:thread_local(C++11)
thread_local int tls_var = 100;
thread_local std::mt19937 rng(std::random_device{}());
void worker(int id) {
// 每个线程有自己的 tls_var
tls_var = id * 10;
std::cout << "线程 " << id << " tls_var = " << tls_var << std::endl;
// 每个线程有自己的随机数生成器
std::uniform_int_distribution<int> dist(1, 100);
int random = dist(rng);
std::cout << "线程 " << id << " 随机数 = " << random << std::endl;
}
int main() {
std::thread t1(worker, 1);
std::thread t2(worker, 2);
std::thread t3(worker, 3);
t1.join();
t2.join();
t3.join();
return 0;
}
方式3:pthread API
#include <pthread.h>
pthread_key_t key;
pthread_key_create(&key, NULL);
pthread_setspecific(key, value);
void* value = pthread_getspecific(key);
18.4 常见陷阱/面试题
面试题:TLS 的应用场景?
参考答案:
- errno(每个线程独立的错误码)
- 线程安全的随机数
- 线程安全的内存分配器
- 事务ID、追踪ID
- 线程池的任务队列
十九、屏障 (Barrier)
19.1 问题背景:为什么需要屏障?
19.1.1 场景:分阶段计算
想象并行计算矩阵:
- 阶段1:所有线程计算 A = B × C
- 阶段2:所有线程计算 D = A × E
- 阶段3:所有线程计算 F = D × G
问题:阶段2 必须等阶段1全部完成才能开始!
解决方案:屏障 - 等待所有线程到达后一起继续
19.2 设计思路:同步点
时间 →
线程1: [阶段1] ████ [屏障等待] ████ [阶段2] ████ [屏障等待] ...
线程2: [阶段1] ████████ [屏障等待] ████ [阶段2] ████ [屏障等待] ...
线程3: [阶段1] ████ [屏障等待] ████████████ [阶段2] ████ ...
所有线程必须都到达屏障,才能一起进入下一阶段
19.3 具体实现:屏障的使用
#include <pthread.h>
#include <iostream>
#include <thread>
#include <vector>
#include <chrono>
const int NUM_THREADS = 3;
pthread_barrier_t barrier;
void phase_work(int thread_id, int phase) {
// 模拟工作
std::this_thread::sleep_for(std::chrono::milliseconds(100 * (thread_id + 1)));
std::cout << "线程 " << thread_id << " 完成阶段 " << phase << "\n";
// 等待所有线程
pthread_barrier_wait(&barrier);
}
void thread_worker(int id) {
for (int phase = 1; phase <= 3; phase++) {
phase_work(id, phase);
}
}
int main() {
pthread_barrier_init(&barrier, NULL, NUM_THREADS);
std::vector<std::thread> threads;
for (int i = 0; i < NUM_THREADS; i++) {
threads.emplace_back(thread_worker, i);
}
for (auto& t : threads) {
t.join();
}
pthread_barrier_destroy(&barrier);
return 0;
}
19.4 常见陷阱/面试题
面试题:屏障 vs 条件变量?
参考答案:
- 屏障:等待所有线程,适合分阶段计算
- 条件变量:等待某个条件成立,适合生产者-消费者
二十、无锁数据结构
20.1 问题背景:为什么需要无锁数据结构?
20.1.1 传统锁的问题
锁的缺点:
1. 线程等待锁时无法做其他事
2. 锁竞争激烈时性能下降
3. 可能导致死锁
4. 优先级反转问题
无锁数据结构:
- 使用 CAS 实现
- 线程不会阻塞
- 不会死锁
20.2 设计思路:CAS 实现
核心思想:
- 失败重试:CAS 不成功,继续尝试
- 乐观并发:假设冲突少,重试成本低
20.3 具体实现:无锁队列
#include <atomic>
template<typename T>
class LockFreeQueue {
private:
struct Node {
T data;
std::atomic<Node*> next;
};
std::atomic<Node*> head_;
std::atomic<Node*> tail_;
public:
LockFreeQueue() {
Node* dummy = new Node();
dummy->next.store(nullptr, std::memory_order_relaxed);
head_.store(dummy, std::memory_order_relaxed);
tail_.store(dummy, std::memory_order_relaxed);
}
void enqueue(T value) {
Node* new_node = new Node();
new_node->data = value;
new_node->next.store(nullptr, std::memory_order_relaxed);
Node* tail;
while (true) {
tail = tail_.load(std::memory_order_acquire);
Node* next = tail->next.load(std::memory_order_acquire);
if (tail == tail_.load(std::memory_order_acquire)) {
if (next == nullptr) {
// 尝试链接到队尾
if (tail->next.compare_exchange_weak(next, new_node,
std::memory_order_release, std::memory_order_relaxed)) {
break;
}
} else {
// 队尾已经移动,帮助推进
tail_.compare_exchange_weak(tail, next,
std::memory_order_release, std::memory_order_relaxed);
}
}
}
// 更新队尾
tail_.compare_exchange_weak(tail, new_node,
std::memory_order_release, std::memory_order_relaxed);
}
bool dequeue(T& result) {
while (true) {
Node* head = head_.load(std::memory_order_acquire);
Node* tail = tail_.load(std::memory_order_acquire);
Node* next = head->next.load(std::memory_order_acquire);
if (head == tail_.load(std::memory_order_acquire)) {
// 队列空
if (next == nullptr) {
return false;
}
// 队尾落后,推进
tail_.compare_exchange_weak(tail, next,
std::memory_order_release, std::memory_order_relaxed);
} else {
// 有数据
if (head_.compare_exchange_weak(head, next,
std::memory_order_release, std::memory_order_relaxed)) {
result = next->data;
return true;
}
}
}
}
};
20.4 常见陷阱/面试题
面试题:无锁数据结构的挑战?
参考答案:
- ABA 问题:需要使用带版本号的指针
- 内存回收:旧节点可能还被其他线程引用
- 复杂度:实现正确很困难
- 性能:高竞争时可能不如锁
二十一、上下文切换与调度
21.1 问题背景:为什么需要了解上下文切换?
21.1.1 什么是上下文切换?
上下文切换 = 保存当前状态 + 恢复新状态
保存什么?
- 寄存器值
- 程序计数器 (PC)
- 栈指针
- 各种标志位
切换成本:
- 约 1000-10000 个 CPU 周期
- 约 1-10 微秒
21.1.2 上下文切换的开销
┌─────────────────────────────────────┐
│ 上下文切换成本 │
├─────────────────────────────────────┤
│ 保存寄存器 ~100 周期 │
│ 切换栈 ~200 周期 │
│ 刷新缓存 ~300 周期 │
│ TLB 刷新 ~500 周期 │
│ 总计 ~1000+ 周期 │
└─────────────────────────────────────┘
21.2 设计思路:调度算法
21.2.1 调度目标
- 公平:每个进程都有机会运行
- 效率:最大化 CPU 利用率
- 响应:减少响应时间
- 吞吐:最大化吞吐量
21.2.2 常见调度算法
| 算法 | 特点 | 适用场景 |
|---|---|---|
| FCFS | 先来先服务 | 简单 |
| SJF | 最短作业优先 | 优化吞吐量 |
| 时间片轮转 | 固定时间片 | 交互系统 |
| 优先级 | 高优先级先运行 | 实时系统 |
| 多级反馈队列 | 动态调整优先级 | 通用 |
21.3 具体实现:线程调度
// 线程优先级示例 (Linux)
#include <pthread.h>
#include <sched.h>
void set_high_priority() {
struct sched_param param;
param.sched_priority = 99;
pthread_setschedparam(pthread_self(), SCHED_FIFO, ¶m);
}
// CPU 亲和性示例
#include <sched.h>
void pin_to_cpu(int cpu_id) {
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(cpu_id, &cpuset);
sched_setaffinity(0, sizeof(cpu_set_t), &cpuset);
}
21.4 常见陷阱/面试题
面试题:进程调度 vs 线程调度?
参考答案:
- 进程调度:由操作系统内核完成
- 线程调度:在支持内核级线程的系统上,由内核调度
- 用户级线程:由用户空间调度,需要绑定到内核线程
二十二、进程间通信详解
22.1 问题背景:为什么需要多种 IPC 方式?
22.1.1 各种 IPC 方式对比
进程间通信方式:
1. 管道 (Pipe)
- 匿名管道:父子进程间
- 命名管道:任意进程间
2. 消息队列 (Message Queue)
- 内核管理
- 异步通信
3. 共享内存 (Shared Memory)
- 最快(直接映射)
- 需要同步
4. 信号量 (Semaphore)
- 同步
- 计数器
5. Socket
- 本地/网络
- 最通用
22.2 具体实现:各种 IPC 方式
22.2.1 管道
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
int pipefd[2];
pipe(pipefd); // 创建管道
pid_t pid = fork();
if (pid == 0) {
// 子进程:写
close(pipefd[0]);
char msg[] = "Hello from child!";
write(pipefd[1], msg, sizeof(msg));
close(pipefd[1]);
} else {
// 父进程:读
close(pipefd[1]);
char buf[100];
read(pipefd[0], buf, sizeof(buf));
printf("Received: %s\n", buf);
close(pipefd[0]);
wait(NULL);
}
return 0;
}
22.2.2 消息队列
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/msg.h>
struct msgbuf {
long mtype;
char mtext[100];
};
int main() {
int msgid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
pid_t pid = fork();
if (pid == 0) {
// 子进程:发送
struct msgbuf msg;
msg.mtype = 1;
strcpy(msg.mtext, "Hello from child!");
msgsnd(msgid, &msg, sizeof(msg.mtext), 0);
} else {
// 父进程:接收
struct msgbuf msg;
msgrcv(msgid, &msg, sizeof(msg.mtext), 1, 0);
printf("Received: %s\n", msg.mtext);
wait(NULL);
// 删除消息队列
msgctl(msgid, IPC_RMID, NULL);
}
return 0;
}
22.2.3 Socket (本地套接字)
// 服务端
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
int main() {
int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, "/tmp/my_socket");
bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
listen(sockfd, 5);
int client = accept(sockfd, NULL, NULL);
// 处理客户端连接...
close(client);
close(sockfd);
unlink("/tmp/my_socket");
return 0;
}
22.3 常见陷阱/面试题
面试题:哪种 IPC 方式最快?
参考答案:
- 共享内存:最快(直接映射,无需复制)
- 管道/消息队列:中等(需要内核中转)
- Socket:最慢(功能最全,但有开销)
二十三、常见并发模式
23.1 生产者-消费者模式
#include <queue>
#include <mutex>
#include <condition_variable>
#include <thread>
#include <atomic>
template<typename T>
class ProducerConsumerQueue {
private:
std::queue<T> queue_;
std::mutex mtx_;
std::condition_variable cv_;
size_t max_size_;
std::atomic<bool> running_{true};
public:
ProducerConsumerQueue(size_t max_size) : max_size_(max_size) {}
void produce(T item) {
std::unique_lock<std::mutex> lock(mtx_);
cv_.wait(lock, [this] {
return queue_.size() < max_size_ || !running_;
});
if (!running_) return;
queue_.push(item);
cv_.notify_one();
}
bool consume(T& item) {
std::unique_lock<std::mutex> lock(mtx_);
cv_.wait(lock, [this] {
return !queue_.empty() || !running_;
});
if (queue_.empty()) return false;
item = queue_.front();
queue_.pop();
cv_.notify_one();
return true;
}
void stop() {
running_ = false;
cv_.notify_all();
}
};
23.2 读者-写者模式
#include <shared_mutex>
#include <map>
class ThreadSafeMap {
private:
std::map<int, std::string> data_;
mutable std::shared_mutex mtx_;
public:
// 读者:可以并发
std::string read(int key) const {
std::shared_lock<std::shared_mutex> lock(mtx_);
auto it = data_.find(key);
return it != data_.end() ? it->second : "";
}
// 写者:独占
void write(int key, const std::string& value) {
std::unique_lock<std::shared_mutex> lock(mtx_);
data_[key] = value;
}
};
23.3 工作队列模式
#include <queue>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <vector>
#include <thread>
class WorkQueue {
private:
std::queue<std::function<void()>> tasks_;
std::mutex mtx_;
std::condition_variable cv_;
bool stop_ = false;
public:
void submit(std::function<void()> task) {
{
std::lock_guard<std::mutex> lock(mtx_);
if (stop_) return;
tasks_.push(std::move(task));
}
cv_.notify_one();
}
void run_worker() {
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();
}
}
void stop() {
{
std::lock_guard<std::mutex> lock(mtx_);
stop_ = true;
}
cv_.notify_all();
}
};
二十四、性能优化技巧
24.1 减少锁竞争
24.1.1 减少锁粒度
// ❌ 粗粒度锁
void process_all_bad(std::vector<Item>& items) {
std::lock_guard<std::mutex> lock(mtx_);
for (auto& item : items) {
process(item); // 锁住整个过程
}
}
// ✅ 细粒度锁
void process_all_good(std::vector<Item>& items) {
for (auto& item : items) {
std::lock_guard<std::mutex> lock(item.mtx_);
process(item); // 只锁单个项目
}
}
24.1.2 使用无锁结构
// ❌ 有锁计数器
std::mutex mtx;
int counter = 0;
void increment() {
std::lock_guard<std::mutex> lock(mtx_);
counter++;
}
// ✅ 无锁计数器
std::atomic<int> counter{0};
void increment() {
counter.fetch_add(1, std::memory_order_relaxed);
}
24.1.3 读写分离
// ❌ 读写都用同一把锁
std::mutex mtx;
int data;
int read() { std::lock_guard<std::mutex> lock(mtx_); return data; }
void write(int v) { std::lock_guard<std::mutex> lock(mtx_); data = v; }
// ✅ 读写分离
std::shared_mutex mtx;
int data;
int read() { std::shared_lock<std::shared_mutex> lock(mtx_); return data; }
void write(int v) { std::unique_lock<std::shared_mutex> lock(mtx_); data = v; }
24.2 减少上下文切换
// ❌ 频繁创建/销毁线程
for (int i = 0; i < 1000; i++) {
std::thread t(task);
t.join(); // 每次等待完成
}
// ✅ 使用线程池
ThreadPool pool(4);
for (int i = 0; i < 1000; i++) {
pool.submit(task); // 复用线程
}
24.3 缓存友好
// ❌ 缓存不友好(跳跃访问)
for (int i = 0; i < N; i++) {
process(data[random_index()]); // 缓存未命中
}
// ✅ 缓存友好(顺序访问)
for (int i = 0; i < N; i++) {
process(data[i]); // 缓存预取
}
二十五、调试与诊断
25.1 常见问题
25.1.1 死锁检测
# 使用 pstack 查看线程堆栈
pstack <pid>
# 使用 gdb
gdb <program>
(gdb) info threads
(gdb) thread apply all bt
25.1.2 竞态检测
# 使用 ThreadSanitizer
g++ -fsanitize=thread program.cpp -o program
./program
# 使用 Helgrind
valgrind --tool=helgrind ./program
25.1.3 性能分析
# 使用 perf
perf record -g ./program
perf report
# 使用 Intel VTune
vtune-gui -collect threading ./program
总结
为什么需要这些同步机制?
| 问题 | 解决方案 |
|---|---|
| 多线程同时访问共享数据 | 互斥锁、原子操作 |
| 线程需要等待某个条件 | 条件变量 |
| 限制并发数量 | 信号量、读写锁 |
| 指令被重排导致问题 | 内存序、内存屏障 |
| 线程创建开销大 | 线程池 |
| 进程之间需要通信 | 管道、消息队列、共享内存 |
选择指南
场景 推荐工具
─────────────────────────────────────────────
计数器/标志位 std::atomic
保护共享数据结构 std::mutex + std::lock_guard
线程间等待通知 std::condition_variable
生产者-消费者 std::queue + mutex + condition_variable
限制并发数量 sem_t (信号量)
读多写少 std::shared_mutex
短时间锁定 pthread_spinlock
大量短任务 线程池
进程间通信 共享内存、消息队列
文档版本: 2.0
最后更新: 2024年