线程同步机制详解
目录
一、线程栈的位置
1.1 进程地址空间布局
高地址
┌─────────────────┐
│ 内核空间 │
├─────────────────┤
│ 主线程栈 │ ← 向下增长
│ ↓ │
├─────────────────┤
│ 内存映射区域 │ ← mmap区域(共享库、线程栈等)
├─────────────────┤
│ ↑ │
│ 堆 │ ← 向上增长
├─────────────────┤
│ BSS/数据段 │
├─────────────────┤
│ 代码段 │
└─────────────────┘
低地址
1.2 主线程栈 vs 其他线程栈
| 特性 | 主线程栈 | 其他线程栈 |
|---|---|---|
| 位置 | 进程地址空间顶部 | mmap 区域内 |
| 分配方式 | 系统自动分配 | mmap() 动态分配 |
| 大小限制 | ulimit -s 控制 | 可通过 pthread_attr_setstacksize() 设置 |
结论:
- 主线程栈:在用户空间最顶部,向下增长
- 其他线程栈:在用户空间的 mmap 区域,介于堆和主线程栈之间
二、线程间通信方式
2.1 共享内存(最直接的方式)
// 全局变量或堆内存天然共享
int shared_data = 0;
void* thread_func(void* arg) {
shared_data = 100; // 直接访问
return NULL;
}
2.2 互斥锁
保护共享数据的访问:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int shared_counter = 0;
void* thread_func(void* arg) {
pthread_mutex_lock(&mutex);
shared_counter++;
pthread_mutex_unlock(&mutex);
return NULL;
}
2.3 条件变量
用于线程间的通知/等待机制:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int data_ready = 0;
// 生产者线程
void produce() {
pthread_mutex_lock(&mutex);
data_ready = 1;
pthread_cond_signal(&cond); // 通知等待的线程
pthread_mutex_unlock(&mutex);
}
// 消费者线程
void consume() {
pthread_mutex_lock(&mutex);
while (!data_ready) {
pthread_cond_wait(&cond, &mutex); // 等待通知
}
// 处理数据...
pthread_mutex_unlock(&mutex);
}
2.4 读写锁
适合读多写少的场景:
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
// 读线程
pthread_rwlock_rdlock(&rwlock);
// 读取共享数据...
pthread_rwlock_unlock(&rwlock);
// 写线程
pthread_rwlock_wrlock(&rwlock);
// 修改共享数据...
pthread_rwlock_unlock(&rwlock);
2.5 信号量
控制并发访问数量:
#include <semaphore.h>
sem_t sem;
sem_init(&sem, 0, 3); // 初始值3,允许3个线程同时访问
sem_wait(&sem); // P操作,计数-1
// 访问资源...
sem_post(&sem); // V操作,计数+1
2.6 自旋锁
适合短时间的锁定:
pthread_spinlock_t spinlock;
pthread_spin_init(&spinlock, 0);
pthread_spin_lock(&spinlock);
// 临界区...
pthread_spin_unlock(&spinlock);
2.7 线程局部存储 (TLS)
虽然是"反通信",但可以避免通信:
__thread int thread_local_var; // 每个线程独立副本
2.8 对比总结
| 方式 | 适用场景 | 特点 |
|---|---|---|
| 共享变量 | 简单数据共享 | 需配合锁使用 |
| 互斥锁 | 通用临界区保护 | 最常用 |
| 条件变量 | 线程同步/通知 | 需配合互斥锁 |
| 读写锁 | 读多写少 | 读读并行 |
| 信号量 | 限制并发数 | 可跨进程 |
| 自旋锁 | 短时间锁定 | 忙等待,不释放CPU |
三、原子性、可见性、有序性问题
3.1 三大问题的本质
| 问题 | 根本原因 |
|---|---|
| 原子性 | 操作被拆分成多个CPU指令,可能被中断 |
| 可见性 | CPU缓存、寄存器、写缓冲区导致数据不一致 |
| 有序性 | 编译器优化重排 + CPU乱序执行 |
3.2 为什么内存序不能完全解决?
内存序主要解决的是「有序性」,但有局限:
| 局限 | 说明 |
|---|---|
| 不保证复合原子性 | x++ 即使是atomic,也是读-改-写三步 |
| 不解决ABA问题 | 值从A变成B再变回A,无法检测 |
| 只针对atomic变量 | 普通变量需要额外处理 |
| 平台相关 | 不同CPU架构内存模型不同 |
3.3 完整解决方案
原子性问题
// ❌ 非原子操作
int counter = 0;
counter++; // 读-改-写,三步操作
// ✅ 方案一:使用锁
pthread_mutex_lock(&mutex);
counter++;
pthread_mutex_unlock(&mutex);
// ✅ 方案二:原子操作
std::atomic<int> counter{0};
counter.fetch_add(1); // 单条CPU指令(如lock xadd)
// ✅ 方案三:CAS循环(无锁)
int expected = counter.load();
while (!counter.compare_exchange_weak(expected, expected + 1));
可见性问题
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
}
有序性问题
// ✅ 方案一:锁(隐含内存屏障)
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);
3.4 解决方案对比
| 方案 | 原子性 | 可见性 | 有序性 | 性能 |
|---|---|---|---|---|
| 互斥锁 | ✅ | ✅ | ✅ | 较低 |
| 原子操作 + 内存序 | ✅ | ✅ | ✅ | 高 |
| volatile | ❌ | 部分 | ❌ | 最高 |
| 显式屏障 | ❌ | ✅ | ✅ | 中等 |
3.5 最佳实践
锁是终极解决方案,因为它隐含了所有必要的保证:
- 原子性:临界区整体不可分割
- 可见性:锁释放时刷新缓存
- 有序性:锁获取/释放隐含内存屏障
简单原则:优先用锁,性能瓶颈时再用原子操作。
四、锁的原理与实现
4.1 锁的本质
// 锁的核心结构
typedef struct {
int locked; // 0=未锁定, 1=已锁定
} lock_t;
// 最简单的锁实现(自旋锁)
void lock(lock_t* l) {
while (atomic_exchange(&l->locked, 1) == 1) {
// 自旋等待
}
}
void unlock(lock_t* l) {
atomic_store(&l->locked, 0);
}
4.2 锁的演进:从自旋到睡眠
┌─────────────────────────────────────────────────────┐
│ 锁的实现演进 │
├─────────────────────────────────────────────────────┤
│ │
│ 自旋锁(Spinlock) │
│ ↓ │
│ 简单,但浪费CPU │
│ │
│ 乐观锁(Try Lock + 自旋一段时间) │
│ ↓ │
│ 短时间等待用自旋,减少开销 │
│ │
│ 悲观锁(Mutex,竞争时睡眠) │
│ ↓ │
│ 长时间等待让出CPU,避免浪费 │
│ │
│ 混合锁(自适应锁,先自旋再睡眠) │
│ ↓ │
│ Linux pthread_mutex 的实现方式 │
│ │
└─────────────────────────────────────────────────────┘
4.3 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, ...);
}
4.4 锁的硬件基础
; x86 的 LOCK 前缀
lock cmpxchg [rdi], rax ; 原子比较并交换
; ARM 的 LDXR/STXR 指令对
ldxr r0, [r1] ; 独占加载
stxr r2, r0, [r1] ; 独占存储,r2=0表示成功
4.5 锁的开销分析
无竞争场景:
┌─────────────────────────────────────┐
│ 用户态原子操作获取锁 │
│ 耗时:~10-50ns │
└─────────────────────────────────────┘
有竞争场景:
┌─────────────────────────────────────┐
│ 用户态尝试失败 │ ~50ns
│ ↓ │
│ 系统调用进入内核 │ ~100-500ns
│ ↓ │
│ 线程加入等待队列 │
│ ↓ │
│ 线程被挂起(上下文切换) │ ~1-10μs
│ ↓ │
│ 等待锁释放... │
│ ↓ │
│ 被唤醒,重新调度 │ ~1-10μs
│ ↓ │
│ 返回用户态 │ ~100-500ns
└─────────────────────────────────────┘
总耗时:微秒级
五、原子操作
5.1 原子操作的硬件原理
┌─────────────────────────────────────────────────────┐
│ CPU 缓存结构 │
│ │
│ CPU0 CPU1 CPU2 CPU3 │
│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │
│ │ L1 │ │ L1 │ │ L1 │ │ L1 │ │
│ │缓存│ │缓存│ │缓存│ │缓存│ │
│ └──┬─┘ └──┬─┘ └──┬─┘ └──┬─┘ │
│ │ │ │ │ │
│ └────────────┴─────┬──────┴────────────┘ │
│ │ │
│ ┌────┴────┐ │
│ │ 系统总线 │ │
│ └─────────┘ │
└─────────────────────────────────────────────────────┘
原子操作时:
1. CPU 发出 LOCK# 信号(锁定总线)
或
锁定缓存行(现代CPU优化)
2. 其他 CPU 无法访问该内存地址
3. 操作完成后释放
5.2 MESI 缓存一致性协议
┌──────────────────────────────────────────────────────┐
│ MESI 状态机 │
├──────────────────────────────────────────────────────┤
│ │
│ M (Modified) - 已修改,独占,需写回内存 │
│ E (Exclusive) - 独占,与内存一致 │
│ S (Shared) - 共享,与内存一致 │
│ I (Invalid) - 无效 │
│ │
│ 读取本地缓存命中 │
│ ←─────── M/E/S │
│ │ │
│ ↓ │
│ 读取本地缓存未命中 │
│ │ │
│ ┌───────┴───────┐ │
│ ↓ ↓ │
│ 其他CPU 本地独占 │
│ 有副本 无副本 │
│ ↓ ↓ │
│ S E │
│ │
│ 写入操作 │
│ │ │
│ ↓ │
│ 发送 Invalidate 消息 │
│ │ │
│ ↓ │
│ 其他CPU缓存行变 I │
│ │ │
│ ↓ │
│ 本地缓存行变 M │
│ │
└──────────────────────────────────────────────────────┘
5.3 常见原子操作
// 1. 原子加载/存储
std::atomic<int> x{0};
int val = x.load(); // 原子读
x.store(100); // 原子写
// 2. 原子交换
int old = x.exchange(200); // 原子交换,返回旧值
// 3. CAS (Compare-And-Swap)
int expected = 100;
bool success = x.compare_exchange_weak(expected, 200);
// 如果 x == expected,则 x = 200,返回 true
// 否则 expected = x,返回 false
// 4. 原子算术操作
x.fetch_add(1); // 原子加,返回旧值
x.fetch_sub(1); // 原子减
x.fetch_and(0xFF); // 原子与
x.fetch_or(0x01); // 原子或
// 5. 原子自增/自减
x++; // 等价于 fetch_add(1) + 1
x--; // 等价于 fetch_sub(1) - 1
5.4 CAS 与 ABA 问题
// ABA 问题示例
std::atomic<Node*> head;
// 线程1:尝试弹出栈顶
Node* old_head = head.load(); // A
Node* next = old_head->next; // B
// --- 线程1 被抢占 ---
// 线程2:弹出 A
head.compare_exchange_weak(old_head, old_head->next); // A → B
// 线程3:弹出 B,删除 A,重新压入 A(地址恰好相同)
delete old_head;
Node* new_A = new Node(); // 地址可能与旧 A 相同!
new_A->next = nullptr;
head.store(new_A);
// 线程1 恢复
head.compare_exchange_weak(old_head, next);
// CAS 成功!但 head 现在指向已删除的 B!
解决方案:带版本号的 CAS
struct stamped_ptr {
Node* ptr;
uint64_t version; // 版本号
};
std::atomic<stamped_ptr> head;
// 每次 CAS 都会增加版本号,ABA 问题变成 ABA' 问题
// A(版本1) → B(版本2) → A(版本3),CAS 会失败
5.5 原子操作的性能
单核无竞争:
┌─────────────────────────────────────┐
│ 原子操作 ≈ 普通操作 + LOCK 前缀开销 │
│ 耗时:~10-20ns │
└─────────────────────────────────────┘
多核无竞争:
┌─────────────────────────────────────┐
│ 可能需要缓存行所有权转移 │
│ 耗时:~20-50ns │
└─────────────────────────────────────┘
多核高竞争:
┌─────────────────────────────────────┐
│ 缓存行在多个 CPU 间反复失效 │
│ 耗时:~100-500ns │
│ CPU 占用率高(自旋等待) │
└─────────────────────────────────────┘
六、volatile 关键字
6.1 volatile 的真正含义
// volatile 告诉编译器:
// 1. 不要优化掉对这个变量的读写
// 2. 每次都要从内存读取,不要用寄存器缓存
// 3. 不要重排 volatile 变量的访问顺序
volatile int* p = (volatile int*)0xFFFF0000;
// 写入硬件寄存器
*p = 0x01; // 编译器不会优化掉
// 读取硬件状态
int status = *p; // 每次都从地址读取
6.2 volatile 不保证什么
volatile int counter = 0;
// ❌ volatile 不保证原子性
counter++; // 仍然是 读-改-写 三步操作
// 反汇编:
// mov eax, [counter] ; 读
// add eax, 1 ; 改
// mov [counter], eax ; 写
// 中间可能被中断!
volatile int x = 0, y = 0;
// ❌ volatile 不保证有序性(跨变量)
// 编译器可能重排 x 和 y 的写入顺序
x = 1;
y = 2;
// CPU 也可能乱序执行
volatile int ready = 0;
int data = 0;
// ❌ volatile 不保证可见性
// 线程1
data = 100;
ready = 1;
// 线程2
while (!ready); // 可能永远看不到 ready 变成 1
// 因为 CPU 缓存可能没有刷新
6.3 volatile 的正确用途
// ✅ 用途1:硬件寄存器访问
volatile uint32_t* UART_DR = (volatile uint32_t*)0x40001000;
*UART_DR = 'A'; // 写入串口数据寄存器
// ✅ 用途2:信号处理函数中的标志
volatile sig_atomic_t signal_received = 0;
void handler(int sig) {
signal_received = 1; // 异步安全
}
// ✅ 用途3:setjmp/longjmp 中的变量
volatile int state = 0;
if (setjmp(buf) == 0) {
state = 1;
longjmp(buf, 1);
}
// state 必须是 volatile,否则可能被恢复到旧值
6.4 volatile vs atomic
| 特性 | volatile | atomic |
|---|---|---|
| 禁止编译器优化 | ✅ | ✅ |
| 原子性 | ❌ | ✅ |
| 可见性 | ❌ | ✅ |
| 有序性 | ❌ | ✅ |
| 多线程安全 | ❌ | ✅ |
| 硬件寄存器 | ✅ | ❌ |
| 信号处理 | ✅ | ❌ |
七、信号量
7.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);
}
}
7.2 信号量 vs 互斥锁
| 特性 | 互斥锁 | 信号量 |
|---|---|---|
| 值 | 0 或 1 | 任意非负整数 |
| 用途 | 互斥访问 | 资源计数 + 互斥 |
| 所有权 | 有(谁加锁谁解锁) | 无 |
| 场景 | 保护临界区 | 生产者-消费者、限制并发数 |
7.3 信号量的典型应用
生产者-消费者问题
sem_t empty; // 空槽位数
sem_t full; // 已填充槽位数
sem_t mutex; // 互斥访问缓冲区
void producer() {
while (1) {
item = produce();
P(&empty); // 等待空槽位
P(&mutex); // 互斥访问
put_item(item);
V(&mutex);
V(&full); // 增加已填充数
}
}
void consumer() {
while (1) {
P(&full); // 等待有数据
P(&mutex);
item = get_item();
V(&mutex);
V(&empty); // 增加空槽位
consume(item);
}
}
限制并发连接数
#define MAX_CONNECTIONS 100
sem_t connection_sem;
sem_init(&connection_sem, 0, MAX_CONNECTIONS);
void handle_connection() {
P(&connection_sem); // 获取连接槽位
// 如果已有100个连接,新连接会等待
// 处理连接...
V(&connection_sem); // 释放槽位
}
7.4 信号量的内核实现
// Linux 内核信号量(简化)
struct semaphore {
raw_spinlock_t lock; // 保护信号量本身
unsigned int count; // 计数
struct list_head wait_list; // 等待队列
};
// down 操作(P)
void down(struct semaphore* sem) {
unsigned long flags;
spin_lock_irqsave(&sem->lock, flags);
if (sem->count > 0) {
sem->count--; // 有资源,直接获取
} else {
// 无资源,加入等待队列,睡眠
__down(sem);
}
spin_unlock_irqrestore(&sem->lock, flags);
}
八、条件变量
8.1 为什么需要条件变量?
// 问题:等待某个条件成立
// ❌ 错误方式:忙等待
while (!condition) {
// 浪费 CPU
}
// ❌ 错误方式:加锁等待
while (!condition) {
pthread_mutex_unlock(&mutex);
sleep(1); // 可能错过信号
pthread_mutex_lock(&mutex);
}
// ✅ 正确方式:条件变量
pthread_mutex_lock(&mutex);
while (!condition) {
pthread_cond_wait(&cond, &mutex); // 原子释放锁并等待
}
// 条件成立,继续执行
pthread_mutex_unlock(&mutex);
8.2 条件变量的工作原理
┌──────────────────────────────────────────────────────┐
│ │
│ 线程A(等待者) 线程B(通知者) │
│ │
│ lock(mutex) │
│ │ │
│ ↓ │
│ while (!condition) │
│ │ │
│ ↓ │
│ cond_wait(cond, mutex) │
│ │ │
│ ├──→ 原子释放 mutex │
│ │ │
│ ├──→ 加入等待队列,睡眠 │
│ │ lock(mutex) │
│ │ │ │
│ │ ↓ │
│ │ condition = true │
│ │ │ │
│ │ ↓ │
│ │ cond_signal(cond) │
│ │ │ │
│ │ ↓ │
│ │ unlock(mutex) │
│ │ │
│ ├──→ 被唤醒 │
│ │ │
│ ├──→ 重新获取 mutex │
│ │ │
│ ↓ │
│ 条件成立,继续执行 │
│ │
└──────────────────────────────────────────────────────┘
8.3 虚假唤醒问题
// 为什么用 while 而不是 if?
pthread_mutex_lock(&mutex);
// ✅ 正确
while (!condition) {
pthread_cond_wait(&cond, &mutex);
}
// ❌ 错误
if (!condition) {
pthread_cond_wait(&cond, &mutex);
}
// 可能被虚假唤醒,条件不一定成立!
pthread_mutex_unlock(&mutex);
虚假唤醒的原因:
- POSIX 标准允许条件变量在没有 signal 的情况下唤醒
- 多个等待者被同时唤醒,但只有一个能满足条件
- 中断可能导致提前返回
九、内存序
9.1 为什么需要内存序?
┌──────────────────────────────────────────────────────┐
│ 三级重排 │
├──────────────────────────────────────────────────────┤
│ │
│ 1. 编译器重排 │
│ 优化代码执行顺序 │
│ │
│ 2. CPU 乱序执行 │
│ 提高指令级并行 │
│ │
│ 3. 缓存可见性问题 │
│ 写入顺序与可见顺序不同 │
│ │
└──────────────────────────────────────────────────────┘
9.2 内存序的层次
// C++ 内存序(从弱到强)
// 1. relaxed:只保证原子性,无顺序保证
x.store(1, std::memory_order_relaxed);
// 2. acquire:之后的读写不能重排到前面
int v = x.load(std::memory_order_acquire);
// 3. release:之前的读写不能重排到后面
x.store(1, std::memory_order_release);
// 4. acq_rel:acquire + release
x.fetch_add(1, std::memory_order_acq_rel);
// 5. seq_cst:顺序一致性(默认)
x.store(1, std::memory_order_seq_cst);
9.3 内存序的硬件实现
┌──────────────────────────────────────────────────────┐
│ 内存屏障类型 │
├──────────────────────────────────────────────────────┤
│ │
│ LoadLoad 屏障 │
│ Load1; LoadLoad; Load2 │
│ 保证 Load1 在 Load2 之前完成 │
│ │
│ StoreStore 屏障 │
│ Store1; StoreStore; Store2 │
│ 保证 Store1 在 Store2 之前完成 │
│ │
│ LoadStore 屏障 │
│ Load1; LoadStore; Store2 │
│ 保证 Load1 在 Store2 之前完成 │
│ │
│ StoreLoad 屏障(最强) │
│ Store1; StoreLoad; Load2 │
│ 保证 Store1 对所有 CPU 可见后,才执行 Load2 │
│ │
└──────────────────────────────────────────────────────┘
; x86 内存屏障指令
mfence ; 全屏障
lfence ; 读屏障
sfence ; 写屏障
; ARM 内存屏障指令
dmb sy ; 全屏障
dmb ld ; 读屏障
dmb st ; 写屏障
十、其他同步机制
10.1 读写锁 (Read-Write Lock)
核心思想:
- 读操作:可以并发(读读不互斥)
- 写操作:必须独占(写写互斥、读写互斥)
- 适用场景:读多写少
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);
}
读写锁的变种:
| 类型 | 特点 | 问题 |
|---|---|---|
| 读优先锁 | 新读者可以插队 | 写者可能饥饿 |
| 写优先锁 | 有写者等待时,新读者等待 | 读者可能饥饿 |
| 公平锁 | 按请求顺序分配 | 无饥饿 |
10.2 自旋锁 (Spinlock)
核心思想:忙等待,不释放 CPU
// 自旋锁实现
void spin_lock(spinlock_t* lock) {
while (atomic_exchange(&lock->locked, 1) == 1) {
// 循环等待(自旋)
}
}
对比:
| 特性 | 自旋锁 | 互斥锁 |
|---|---|---|
| 等待方式 | 忙等待 | 睡眠等待 |
| CPU 占用 | 高 | 低 |
| 上下文切换 | 无 | 有 |
| 适用场景 | 短时间持有 | 长时间持有 |
| 适用上下文 | 不可睡眠的场景 | 可睡眠的场景 |
自旋锁的优化:
// 1. 简单自旋
while (atomic_exchange(&lock->locked, 1) == 1);
// 2. 带退避的自旋
while (1) {
if (atomic_exchange(&lock->locked, 1) == 0)
break;
for (int i = 0; i < backoff; i++)
cpu_pause();
backoff *= 2;
}
// 3. 读-写自旋锁
while (lock->locked == 1)
cpu_pause();
if (atomic_exchange(&lock->locked, 1) == 0)
break;
10.3 屏障 (Barrier)
核心思想:等待所有线程到达后,一起继续执行
pthread_barrier_t barrier;
pthread_barrier_init(&barrier, NULL, 3); // 3个线程
void* thread_func(void* arg) {
// 第一阶段
phase1_work();
// 等待所有线程完成第一阶段
pthread_barrier_wait(&barrier);
// 所有线程都到达后,一起进入第二阶段
phase2_work();
return NULL;
}
应用场景:
- 并行计算的分阶段处理
- 矩阵运算
- 多线程游戏
10.4 Futex (Fast Userspace Mutex)
核心思想:
- 无竞争时:完全在用户态完成(快)
- 有竞争时:进入内核等待(省CPU)
// 加锁
if (atomic_compare_exchange(&lock, 0, 1) == 0)
成功获取锁(用户态)
else
futex(&lock, FUTEX_WAIT, 1) // 进入内核等待
// 解锁
lock = 0
futex(&lock, FUTEX_WAKE, 1) // 唤醒一个等待者
优势:
- 无竞争:用户态原子操作,~10ns
- 有竞争:内核等待,~1μs
- 典型场景:大多数情况下无竞争
- 性能提升:10-100倍
10.5 RCU (Read-Copy-Update)
核心思想:读操作无锁,写操作复制后更新指针
初始状态:
┌─────────┐
│ 数据A │ ←─── 指针
└─────────┘
↑
读者1, 读者2
更新过程:
步骤1:复制数据
步骤2:修改副本
步骤3:更新指针(原子操作)
步骤4:等待宽限期结束,释放旧数据
// Linux 内核 RCU 示例
// 读者(无锁)
struct data* read_data() {
rcu_read_lock();
struct data* p = rcu_dereference(global_ptr);
// 使用 p...
rcu_read_unlock();
return p;
}
// 写者
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);
}
适用场景:
- ✅ 读多写少
- ✅ 数据量小
- ✅ 可以容忍短暂的不一致
- ❌ 写操作频繁
- ❌ 数据量大
10.6 Seqlock (Sequence Lock)
核心思想:用序列号检测读写冲突
struct seqlock {
unsigned sequence; // 序列号
spinlock_t lock; // 写者互斥锁
};
// 写者
void write_lock(seqlock_t* sl) {
spin_lock(&sl->lock);
sl->sequence++; // 变成奇数
}
void write_unlock(seqlock_t* sl) {
sl->sequence++; // 变成偶数
spin_unlock(&sl->lock);
}
// 读者(无锁)
int read_seqlock(seqlock_t* sl, struct data* out) {
unsigned seq1, seq2;
do {
seq1 = sl->sequence;
if (seq1 & 1) continue; // 正在写,等待
*out = shared_data;
seq2 = sl->sequence;
} while (seq1 != seq2); // 序列号变化,重读
return 0;
}
10.7 线程局部存储 (TLS)
核心思想:每个线程有变量的独立副本,避免同步问题
全局变量:
┌─────────┐
│ X │ ←─── 所有线程共享,需要同步
└─────────┘
线程局部变量:
┌─────────┐
│ X_T1 │ ←─── 线程1 独享
└─────────┘
┌─────────┐
│ X_T2 │ ←─── 线程2 独享
└─────────┘
// 方式1:__thread 关键字
__thread int thread_local_var;
// 方式2:thread_local (C11/C++11)
thread_local int tl_var;
// 方式3:pthread API
pthread_key_t key;
pthread_key_create(&key, NULL);
pthread_setspecific(key, value);
void* value = pthread_getspecific(key);
应用场景:
- errno(错误码)
- 线程安全的随机数
- 线程安全的内存分配器
- 线程池的任务队列
10.8 消息队列
核心思想:通过传递消息来通信,而不是共享内存
struct message_queue {
struct message* buffer;
int capacity;
int head;
int tail;
pthread_mutex_t mutex;
pthread_cond_t not_empty;
pthread_cond_t not_full;
};
void send_message(struct message_queue* q, struct message* msg) {
pthread_mutex_lock(&q->mutex);
while (queue_is_full(q)) {
pthread_cond_wait(&q->not_full, &q->mutex);
}
queue_push(q, msg);
pthread_cond_signal(&q->not_empty);
pthread_mutex_unlock(&q->mutex);
}
struct message* receive_message(struct message_queue* q) {
pthread_mutex_lock(&q->mutex);
while (queue_is_empty(q)) {
pthread_cond_wait(&q->not_empty, &q->mutex);
}
struct message* msg = queue_pop(q);
pthread_cond_signal(&q->not_full);
pthread_mutex_unlock(&q->mutex);
return msg;
}
10.9 无锁数据结构
无锁栈
struct node {
int data;
struct node* next;
};
void push(struct lockfree_stack* s, int data) {
struct node* new_node = malloc(sizeof(*new_node));
new_node->data = data;
do {
new_node->next = atomic_load(&s->top);
} while (!atomic_compare_exchange_weak(&s->top, &new_node->next, new_node));
}
int pop(struct lockfree_stack* s) {
struct node* top;
do {
top = atomic_load(&s->top);
if (top == NULL) return -1;
} while (!atomic_compare_exchange_weak(&s->top, &top, top->next));
int data = top->data;
free(top);
return data;
}
十一、总结与选择指南
11.1 功能对比
| 机制 | 原子性 | 可见性 | 有序性 | 适用场景 |
|---|---|---|---|---|
| 互斥锁 | ✅ | ✅ | ✅ | 通用 |
| 原子操作 | ✅ | ✅ | ✅ | 单变量 |
| volatile | ❌ | ❌ | ❌ | 硬件 |
| 信号量 | ✅ | ✅ | ✅ | 计数 |
| 条件变量 | ✅ | ✅ | ✅ | 等待 |
| 读写锁 | ✅ | ✅ | ✅ | 读多写少 |
| 自旋锁 | ✅ | ✅ | ✅ | 短临界区 |
| RCU | ❌ | ✅ | ✅ | 读多写少 |
| TLS | N/A | N/A | N/A | 避免共享 |
11.2 性能对比
| 机制 | 无竞争开销 | 有竞争开销 | 复杂度 |
|---|---|---|---|
| 互斥锁 | 10-50 ns | 1-10 μs | 低 |
| 读写锁 | 10-50 ns | 1-10 μs | 低 |
| 自旋锁 | 10-20 ns | 100-500 ns | 低 |
| 信号量 | 100-500 ns | 1-10 μs | 低 |
| 条件变量 | 100-500 ns | 1-10 μs | 低 |
| 原子操作 | 10-50 ns | 100-500 ns | 中 |
| RCU | 1-5 ns | 1-5 ns | 高 |
| Seqlock | 5-10 ns | 5-10 ns | 中 |
| TLS | 1-5 ns | 1-5 ns | 低 |
| 消息队列 | 100-500 ns | 1-10 μs | 中 |
| 无锁数据结构 | 20-100 ns | 100-500 ns | 高 |
11.3 选择指南
开始
│
↓
┌───────────────┐
│ 需要同步什么? │
└───────┬───────┘
│
┌─────────────┼─────────────┐
↓ ↓ ↓
单变量 多变量 等待条件
│ │ │
↓ ↓ ↓
┌─────────┐ ┌─────────┐ ┌─────────┐
│原子操作 │ │ 互斥锁 │ │条件变量 │
└─────────┘ └─────────┘ └─────────┘
│ │ │
↓ ↓ ↓
简单计数? 需要限制 需要计数?
│ 并发数量? │
↓ │ ↓
是 ↓ 否 ↓ 是 是 ↓ 否
│ │ │
↓ ↓ ↓
relaxed 信号量 信号量
内存序
11.4 一句话总结
| 机制 | 一句话 |
|---|---|
| 互斥锁 | 通用安全,首选方案 |
| 原子操作 | 单变量高性能,需要理解内存序 |
| volatile | 只用于硬件/信号,不用于多线程 |
| 信号量 | 资源计数 + 互斥,生产者消费者 |
| 条件变量 | 等待条件成立,配合锁使用 |
| 读写锁 | 读多写少场景,读读并发 |
| 自旋锁 | 短临界区,不可睡眠场景 |
| 屏障 | 多线程分阶段同步 |
| Futex | Linux 锁的基石,用户态+内核态混合 |
| RCU | 读无锁,写复制,内核常用 |
| Seqlock | 序列号检测冲突,读多写少 |
| TLS | 每线程独立副本,避免共享 |
| 消息队列 | 通信代替共享,解耦 |
| 无锁数据结构 | CAS 实现,高性能高复杂度 |
11.5 最佳实践
// 1. 默认选择:互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 简单、安全、通用
// 2. 简单计数器:原子操作
std::atomic<int> counter{0};
counter.fetch_add(1, std::memory_order_relaxed);
// 3. 需要同步:原子操作 + 内存序
std::atomic<bool> ready{false};
data = compute();
ready.store(true, std::memory_order_release);
// 4. 生产者-消费者:信号量
sem_t sem;
sem_init(&sem, 0, 0);
// 5. 等待条件:条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
// 6. 硬件寄存器:volatile
volatile uint32_t* reg = (volatile uint32_t*)0xFFFF0000;
附录:常见问题
Q1: 为什么锁是最简单通用的解决方案?
锁隐含了所有必要的保证:
- 原子性:临界区整体不可分割
- 可见性:锁释放时刷新缓存
- 有序性:锁获取/释放隐含内存屏障
Q2: 为什么原子操作不能完全替代锁?
- 只能保证单变量原子性
- 多变量需要锁
- 内存序复杂,容易出错
Q3: volatile 能用于多线程吗?
不能。volatile 只保证:
- 不被编译器优化
- 每次从内存读取
不保证:
- 原子性
- 可见性
- 有序性
Q4: 如何选择同步机制?
- 优先用锁
- 发现性能瓶颈时分析
- 简单计数器用原子操作
- 读多写少用读写锁/RCU
- 避免共享用TLS
文档版本: 1.0
最后更新: 2024年