Skip to content
📖 Auto 版 Modern C本书目录

第二十一章:原子操作与内存一致性 ​

级别 3 -- 经验

使用原子操作和内存排序进行无锁编程——C 并发的最深层次,编译器和 CPU 都是你要对抗的对手。

原子操作是所有高级并发原语的基础。互斥量使用原子操作。条件变量使用原子操作。即使是多线程程序中最简单的"这个标志是否设置?"检查也需要原子操作才能保证正确性。本章涵盖了本书中技术要求最高的内容。


21.1 先行发生关系 ​

当两个线程访问相同的内存时,这些访问"发生"的顺序并不总是清楚的。编译器可能重排指令。CPU 可能重排内存操作。缓存层次结构可能延迟可见性。

先行发生(happened-before)关系定义了一个操作是否保证对另一个操作可见:

c
// C 深入:先行发生
// 线程 1:              线程 2:
// x = 1;                if (flag == 1)
// flag = 1;                 assert(x == 1);  // 可能失败

没有适当的同步,线程 2 可能看到 flag == 1 但 x == 0。编译器或 CPU 可能重排存储,或者缓存可能在线程 2 读取 flag 时尚未传播对 x 的写入。

C 深入: 先行发生关系由 C 标准定义(以及之前的 Java 内存模型)。它是一种偏序关系:并非所有操作都彼此有序。只有通过同步(互斥量加锁/解锁、具有适当内存排序的原子操作)连接的操作才能建立先行发生关系。

关键洞察:程序顺序不是内存顺序。你编写的代码不是 CPU 执行的顺序,也不是其他线程观察效果的顺序。

Auto 的方法。 Auto 的 Actor 模型完全避免了这个问题。每个 Actor 拥有自己的数据。消息按顺序传递。没有共享可变状态意味着没有内存排序问题。


21.2 提供同步的 C 库调用 ​

C11 通过 <stdatomic.h> 提供原子操作:

c
// C 深入:原子操作
#include <stdatomic.h>
#include <stdio.h>
#include <threads.h>

static _Atomic int counter = 0;

int worker(void *arg) {
    for (int i = 0; i < 100000; i++) {
        atomic_fetch_add(&counter, 1);
    }
    return 0;
}

int main(void) {
    thrd_t t1, t2;
    thrd_create(&t1, worker, NULL);
    thrd_create(&t2, worker, NULL);
    thrd_join(t1, NULL);
    thrd_join(t2, NULL);

    printf("Counter: %d\n", atomic_load(&counter));  // 200000
    return 0;
}

_Atomic 类型限定符使对该对象的操作成为原子的。atomic_fetch_add 函数原子性地递增计数器。不需要互斥量。

C 深入: 原子操作使用 CPU 级别原语实现:

  • x86:LOCK 前缀指令(例如 LOCK XADD)
  • ARM:LDXR/STXR 独占加载/存储对
  • 某些架构使用全局比较并交换循环

_Atomic 类型可能具有与非原子对应物不同的大小或对齐方式。_Atomic(int) 在主流平台上总是无锁的,但 _Atomic(struct large) 可能使用内部锁。

常见原子操作:

操作函数描述
加载atomic_load(&x)原子读取
存储atomic_store(&x, val)原子写入
获取并加atomic_fetch_add(&x, 1)加并返回旧值
获取并减atomic_fetch_sub(&x, 1)减并返回旧值
比较并交换atomic_compare_exchange_strongCAS 操作
交换atomic_exchange(&x, val)原子交换
标志测试并设置atomic_flag_test_and_set简单自旋锁原语

基于 MIT 许可发布。