线程同步机制详解

5 阅读20分钟

线程同步机制详解

目录

  1. 线程栈的位置
  2. 线程间通信方式
  3. 原子性、可见性、有序性问题
  4. 锁的原理与实现
  5. 原子操作
  6. volatile 关键字
  7. 信号量
  8. 条件变量
  9. 内存序
  10. 其他同步机制
  11. 总结与选择指南

一、线程栈的位置

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

特性volatileatomic
禁止编译器优化
原子性
可见性
有序性
多线程安全
硬件寄存器
信号处理

七、信号量

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);

虚假唤醒的原因

  1. POSIX 标准允许条件变量在没有 signal 的情况下唤醒
  2. 多个等待者被同时唤醒,但只有一个能满足条件
  3. 中断可能导致提前返回

九、内存序

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读多写少
TLSN/AN/AN/A避免共享

11.2 性能对比

机制无竞争开销有竞争开销复杂度
互斥锁10-50 ns1-10 μs
读写锁10-50 ns1-10 μs
自旋锁10-20 ns100-500 ns
信号量100-500 ns1-10 μs
条件变量100-500 ns1-10 μs
原子操作10-50 ns100-500 ns
RCU1-5 ns1-5 ns
Seqlock5-10 ns5-10 ns
TLS1-5 ns1-5 ns
消息队列100-500 ns1-10 μs
无锁数据结构20-100 ns100-500 ns

11.3 选择指南

                    开始
                      │
                      ↓
              ┌───────────────┐
              │ 需要同步什么? │
              └───────┬───────┘
                      │
        ┌─────────────┼─────────────┐
        ↓             ↓             ↓
    单变量         多变量         等待条件
        │             │             │
        ↓             ↓             ↓
   ┌─────────┐   ┌─────────┐   ┌─────────┐
   │原子操作 │   │  互斥锁  │   │条件变量 │
   └─────────┘   └─────────┘   └─────────┘
        │             │             │
        ↓             ↓             ↓
   简单计数?     需要限制      需要计数?
        │        并发数量?         │
        ↓             │             ↓
    是 ↓ 否          ↓ 是         是 ↓ 否
        │             │             │
        ↓             ↓             ↓
   relaxed       信号量        信号量
   内存序

11.4 一句话总结

机制一句话
互斥锁通用安全,首选方案
原子操作单变量高性能,需要理解内存序
volatile只用于硬件/信号,不用于多线程
信号量资源计数 + 互斥,生产者消费者
条件变量等待条件成立,配合锁使用
读写锁读多写少场景,读读并发
自旋锁短临界区,不可睡眠场景
屏障多线程分阶段同步
FutexLinux 锁的基石,用户态+内核态混合
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: 如何选择同步机制?

  1. 优先用锁
  2. 发现性能瓶颈时分析
  3. 简单计数器用原子操作
  4. 读多写少用读写锁/RCU
  5. 避免共享用TLS

文档版本: 1.0
最后更新: 2024年