进程与线程深度解析

4 阅读46分钟

进程与线程深度解析

本文档采用"问题驱动"的方式讲解,每个主题都按照以下结构展开:

  1. 问题背景 - 为什么需要这个概念?解决什么问题?
  2. 设计思路 - 设计师是如何思考来解决这个问题的?
  3. 具体实现 - 从原理到代码,底层机制是什么?
  4. 常见陷阱/面试题 - 实际使用中的问题和面试常考点

目录

  1. 进程与线程基础
  2. 数据竞争与同步问题
  3. 互斥锁
  4. 条件变量
  5. 信号量
  6. 原子操作与CAS
  7. 可见性、原子性、有序性
  8. 内存序
  9. 死锁与避免
  10. 原子操作的底层原理
  11. volatile关键字
  12. 读写锁与自旋锁
  13. 线程池
  14. 进程间通信
  15. 常见面试题
  16. RCU (Read-Copy-Update)
  17. Seqlock (序列锁)
  18. 线程局部存储 (TLS)
  19. 屏障 (Barrier)
  20. 无锁数据结构
  21. 上下文切换与调度
  22. 进程间通信详解
  23. 常见并发模式
  24. 性能优化技巧
  25. 调试与诊断

一、进程与线程基础

1.1 问题背景:为什么需要进程和线程?

1.1.1 为什么要有多任务?

想象一下你在用电脑:

  • 边听音乐边写代码
  • 后台下载文件的同时浏览网页
  • 播放器在播放音乐,浏览器在渲染页面

如果没有多任务,你的电脑只能一件事一件事做——听音乐时不能写代码,下载文件时不能浏览网页。这得多痛苦?

问题:如何让计算机同时做多件事情?

1.1.2 进程 vs 线程 - 为什么要区分?
场景:开一家餐厅

进程 = 一家独立的餐厅
- 有自己的厨房、仓库、员工
- 餐厅之间互不影响
- 开一家新餐厅成本很高(需要全新资源)

线程 = 餐厅里的员工
- 共享餐厅的厨房、仓库
- 员工之间可以协作
- 增加一个新员工成本很低

问题:那什么时候用进程?什么时候用线程?

特性进程线程
资源分配独立(内存、文件等)共享(内存、文件等)
创建成本高(需要分配资源)低(共享资源)
通信成本高(需要IPC)低(直接共享内存)
隔离性高(一个崩溃不影响另一个)低(一个崩溃整个进程崩溃)
并行性多核并行多线程并行
适用场景需要隔离、独立运行需要协作、共享数据
1.1.3 线程的内存模型 - 为什么需要了解这个?
进程内存空间
┌─────────────────────────────────────────────┐
│              内核空间                        │  ← 操作系统管理
├─────────────────────────────────────────────┤
│  主线程栈  ↓                                │
├─────────────────────────────────────────────┤
│                                             │
│  ┌─────────────────────────────────────┐   │
│  │         mmap 区域                   │   │
│  │  ├─ 共享库                          │   │
│  │  ├─ 子线程栈 1                      │   │
│  │  ├─ 子线程栈 2                      │   │
│  │  └─ ...                             │   │
│  └─────────────────────────────────────┘   │
│                                             │
├─────────────────────────────────────────────┤
│  堆 ↑                                       │
├─────────────────────────────────────────────┤
│  BSS 段(未初始化全局变量)                  │
├─────────────────────────────────────────────┤
│  数据段(已初始化全局变量)                  │
├─────────────────────────────────────────────┤
│  代码段                                      │
└─────────────────────────────────────────────┘

关键问题

  • 共享的:代码段、数据段、堆、所有线程可见的全局变量
  • 独立的:每个线程有自己的栈、寄存器、程序计数器

这意味着:

  • 堆上的数据:线程共享 → 需要同步
  • 栈上的数据:线程独立 → 无需同步

1.2 设计思路:如何设计进程和线程?

1.2.1 进程的设计目标

目标:让多个程序能够"同时"运行,且互不干扰

设计思路

  1. 虚拟地址空间:每个进程有自己的"假装独占"的内存空间
  2. 隔离:一个进程崩溃不影响其他进程
  3. 资源抽象:文件、网络、内存都抽象成"资源"
1.2.2 线程的设计目标

目标:让同一个进程内的多个任务能够"同时"执行,且高效协作

设计思路

  1. 共享资源:线程共享进程的内存和资源
  2. 轻量创建:不需要重新分配资源,创建快
  3. 高效通信:直接读写共享内存

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::threadfork / CreateProcess
栈分配1-8MB(可配置)1-8MB
内核对象只需线程内核对象需要进程内核对象 + 资源
时间约 1-10μs约 100-1000μs

1.4 常见陷阱/面试题

面试题 1:进程和线程的区别?

参考答案

  1. 资源:进程拥有独立资源,线程共享进程资源
  2. 开销:线程创建/切换开销小
  3. 通信:线程间通信简单(共享内存),进程间通信复杂
  4. 隔离:进程隔离性好,线程隔离性差
  5. 崩溃:一个线程崩溃会导致整个进程崩溃
面试题 2:线程越多越好吗?

参考答案: 不是。线程过多会导致:

  1. 上下文切换开销增大
  2. 内存消耗增加
  3. 调度复杂度增加
  4. 同步开销增加

最佳线程数公式

  • 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 指令:

  1. 读取 (LOAD): 从内存读取 balance 到寄存器
  2. 计算 (MODIFY): 在寄存器中做减法
  3. 写入 (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:如何检测数据竞争?

参考答案

  1. 工具检测:ThreadSanitizer(TSan)、Helgrind、Intel Inspector
  2. 代码审查:检查所有共享变量的访问
  3. 压力测试:多线程高并发测试

三、互斥锁

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 基本要求
  1. 互斥:同一时刻只有一个线程能获得锁
  2. 公平:避免线程饿死
  3. 高效:无竞争时快速获取
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_guardunique_lock
灵活性简单,析构时解锁可随时解锁,可重入
功能只在作用域结束时解锁可延迟加锁、可尝试加锁
性能稍轻量稍重
适用简单临界区需要灵活控制的场景
面试题 2:为什么推荐使用 lock_guard 而不是手动 lock/unlock?

参考答案

  1. 异常安全:即使抛出异常,析构函数也会解锁
  2. 避免死锁:不会忘记解锁
  3. 代码简洁:减少出错概率
// ❌ 危险:可能忘记解锁
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 生产者-消费者问题

想象餐厅厨房:

  • 生产者:厨师做菜
  • 消费者:服务员端菜
  • 缓冲区:菜品暂存区

问题:

  1. 缓冲区空时,消费者怎么办?→ 等待
  2. 缓冲区满时,生产者怎么办?→ 等待
  3. 如何通知对方?→ 条件变量
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 条件变量的核心思想

等待-通知机制

  1. 线程等待某个条件成立
  2. 条件成立时,其他线程通知等待者
  3. 等待者被唤醒,继续执行
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?

参考答案

  1. 虚假唤醒:POSIX 标准允许条件变量在没有 signal 的情况下唤醒
  2. 竞争条件:被唤醒时,条件可能已经被其他线程改变
  3. 安全: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:信号量和互斥锁的区别?

参考答案

  1. :互斥锁只能是 0 或 1,信号量可以是任意整数
  2. 所有权:互斥锁有所有权(谁加锁谁解锁),信号量没有
  3. 用途:互斥锁用于互斥,信号量用于计数/同步
面试题 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 问题是指:

  1. 线程 A 读取值 A
  2. 线程 B 把值改成 B
  3. 线程 C 把值改回 A
  4. 线程 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=truedata=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:如何检测死锁?

参考答案

  1. 运行时检测:使用工具如 Helgrind、ThreadSanitizer
  2. 代码审查:检查锁的获取顺序
  3. 超时机制:使用 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:原子操作为什么比锁快?

参考答案

  1. 无系统调用:大多数情况下在用户态完成
  2. 无上下文切换:不需要进入内核
  3. 细粒度:只影响一个变量
面试题 2:什么时候应该用锁而不是原子操作?

参考答案

  1. 需要保护多个变量
  2. 操作复杂,不是简单的增减
  3. 需要更复杂的同步逻辑

十一、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?
特性volatileatomic
禁止编译器优化
原子性
可见性
有序性
多线程安全
硬件寄存器
信号处理

十二、读写锁与自旋锁

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:如何设计一个高并发的计数器?

参考答案

  1. 简单场景:使用 atomic,无锁
  2. 高并发场景:使用分段锁(分桶),减少竞争
  3. 需要持久化:考虑 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 常见陷阱/面试题

面试题:无锁数据结构的挑战?

参考答案

  1. ABA 问题:需要使用带版本号的指针
  2. 内存回收:旧节点可能还被其他线程引用
  3. 复杂度:实现正确很困难
  4. 性能:高竞争时可能不如锁

二十一、上下文切换与调度

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 调度目标
  1. 公平:每个进程都有机会运行
  2. 效率:最大化 CPU 利用率
  3. 响应:减少响应时间
  4. 吞吐:最大化吞吐量
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, &param);
}

// 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 方式最快?

参考答案

  1. 共享内存:最快(直接映射,无需复制)
  2. 管道/消息队列:中等(需要内核中转)
  3. 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年