_atomic c语言

99ANYc3cd6
预计阅读时长 23 分钟
位置: 首页 C语言 正文

_Atomic 是 C11 标准引入的一个关键字,用于定义原子类型,它的核心目的是解决多线程编程中的数据竞争问题。

_atomic c语言
(图片来源网络,侵删)

为什么需要 _Atomic?—— 数据竞争的根源

想象一个简单的场景:两个线程同时试图增加一个共享的计数器。

#include <stdio.h>
#include <pthread.h>
int counter = 0;
void* thread_func(void* arg) {
    for (int i = 0; i < 1000000; ++i) {
        counter++; // 这一步不是原子的!
    }
    return NULL;
}
int main() {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, thread_func, NULL);
    pthread_create(&t2, NULL, thread_func, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    printf("Final counter value: %d\n", counter); // 结果可能不是 2000000
    return 0;
}

counter++ 这个看似简单的操作,在底层通常由三个步骤组成:

  1. :从内存中读取 counter 的当前值。
  2. :在 CPU 寄存器中将这个值加 1。
  3. :将计算后的新值写回内存。

假设线程 1 执行到步骤 1(读取值为 100),但还没来得及写回,线程 2 也执行了步骤 1,也读取到了 100。

  • 线程 1 将 100 加 1,得到 101,写回内存。
  • 线程 2 也将 100 加 1,得到 101,写回内存。

结果,两次增加操作只让计数器从 100 增加到了 101,而不是预期的 102,这就是数据竞争,它会导致程序行为不确定且难以调试。

_atomic c语言
(图片来源网络,侵删)

_Atomic 的作用就是将“读-修改-写”这样的操作打包成一个不可分割的原子的操作。 在多核处理器上,这通常通过 CPU 的指令(如 LOCK 前缀)和内存屏障来实现,一旦一个原子操作开始,其他任何线程都不能同时操作同一个变量,直到该操作完成。


_Atomic 的语法和使用

C11 引入了 _Atomic 关键字来声明原子变量,它的基本语法如下:

_Atomic type_name variable_name;
_Atomic int atomic_counter;
_Atomic double atomic_price;
_Atomic struct point { int x; int y; } atomic_point;

为了方便使用,C11 标准还提供了一个 atomic_intatomic_bool 等类型的别名,这些别名在 <stdatomic.h> 头文件中定义。在实际编程中,强烈推荐使用这些标准别名。

上面的代码通常这样写:

_atomic c语言
(图片来源网络,侵删)
#include <stdatomic.h>
atomic_int atomic_counter;
atomic_bool atomic_flag;

重要规则

  1. 必须使用 <stdatomic.h>:所有原子操作相关的函数和类型都定义在这个头文件中。
  2. 不能直接赋值:你不能像普通变量一样直接对原子变量进行赋值或读取,必须使用专门的函数来访问和修改它们。
    • 错误示例
      atomic_int x = 10;
      x = 20; // 错误!不能直接赋值
      int y = x; // 错误!不能直接读取
    • 正确示例
      atomic_int x = ATOMIC_VAR_INIT(10); // 初始化
      atomic_store(&x, 20); // 使用函数存储
      int y = atomic_load(&x); // 使用函数加载

原子操作函数

<stdatomic.h> 提供了一系列宏形式的函数来进行原子操作,这些函数都以 atomic_ 开头,并且需要传入原子变量的地址

操作类别 函数 描述
加载 atomic_load(p) 从原子指针 p 指向的位置读取值。
存储 atomic_store(p, val) 将值 val 存储到原子指针 p 指向的位置。
读-修改-写 atomic_fetch_add(p, val) 原子地读取 *p,然后将其与 val 相加,并将结果写回 *p,返回操作前的旧值。
atomic_fetch_sub(p, val) 原子地减法。
atomic_fetch_and(p, val) 原子地按位与。
atomic_fetch_or(p, val) 原子地按位或。
atomic_fetch_xor(p, val) 原子地按位异或。
比较交换 atomic_compare_exchange_weak(p, expected, desired) 核心同步原语*p 的当前值等于 *expected,则将 *p 更新为 desired,如果成功,返回 true (1),否则返回 false (0)。weak 版本可能在条件不满足时也“失败”,但性能可能更高。
atomic_compare_exchange_strong(p, expected, desired) 同上,但保证在条件不满足时绝不“失败”,更可靠,但性能可能稍低。
标志位操作 atomic_flag_test_and_set(p) 原子地将 *p 设置为 1,并返回它之前的值。
atomic_flag_clear(p) 原子地将 *p 设置为 0。

示例:使用 atomic_fetch_add 修复计数器

#include <stdio.h>
#include <pthread.h>
#include <stdatomic.h>
atomic_int counter = ATOMIC_VAR_INIT(0); // 原子初始化
void* thread_func(void* arg) {
    for (int i = 0; i < 1000000; ++i) {
        atomic_fetch_add(&counter, 1); // 原子地增加
    }
    return NULL;
}
int main() {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, thread_func, NULL);
    pthread_create(&t2, NULL, thread_func, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    printf("Final counter value: %d\n", atomic_load(&counter)); // 使用原子加载读取
    // 结果保证是 2000000
    return 0;
}

在这个例子中,atomic_fetch_add 确保了 counter++ 的整个操作是原子的,从而避免了数据竞争。

示例:使用 atomic_compare_exchange_weak 实现锁

CAS 是构建更复杂同步原语(如自旋锁)的基础。

#include <stdio.h>
#include <pthread.h>
#include <stdatomic.h>
// atomic_flag 是一个只能设置为 0 或 1 的原子布尔类型,非常适合用作锁
atomic_flag lock = ATOMIC_FLAG_INIT; // 初始化为 0 (未锁定)
void spin_lock() {
    // 尝试将 lock 从 0 变为 1
    // lock 已经是 1,说明已被锁定,循环重试
    while (atomic_flag_test_and_set(&lock)) {
        // 等待...
    }
}
void spin_unlock() {
    // 将 lock 清零,释放锁
    atomic_flag_clear(&lock);
}
// 共享资源
int shared_data = 0;
void* thread_func(void* arg) {
    for (int i = 0; i < 100000; ++i) {
        spin_lock();      // 加锁
        shared_data++;    // 安全地修改共享数据
        spin_unlock();    // 解锁
    }
    return NULL;
}
int main() {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, thread_func, NULL);
    pthread_create(&t2, NULL, thread_func, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    printf("Final shared_data value: %d\n", shared_data); // 应该是 200000
    return 0;
}

_Atomicvolatile 的区别

这是一个非常重要的概念,初学者经常混淆它们。

特性 _Atomic volatile
目的 保证操作的原子性,防止数据竞争。 防止编译器优化掉看似“不必要”的访问,保证内存可见性。
解决的问题 多线程环境下,多个线程同时读写一个变量导致的数据不一致 硬件寄存器、内存映射I/O等,变量的值可能被硬件异步改变,或者被编译器优化掉。
机制 通过CPU指令(如LOCK前缀)和内存屏障实现。 告诉编译器“每次都从内存读取,不要使用缓存的值”,但不保证操作是原子的
关系 一个原子类型自动是 volatile,因为原子操作必须保证每次都从内存读写,所以编译器不能对其进行缓存优化。 volatile 不保证原子性volatile int x; x++; 仍然不是原子的,同样存在数据竞争问题。

简单总结

  • 如果你的变量会被多个线程同时修改,你需要 _Atomic
  • 如果你的变量会被硬件或信号处理器异步修改,或者你想让编译器不要“耍小聪明”,你需要 volatile
  • 对于多线程编程,_Atomic 是必须的,而 volatile 通常不足以解决同步问题。

内存顺序

_Atomic 操作还有一个高级但重要的概念:内存顺序,它决定了原子操作之间以及原子操作与普通内存访问之间的可见性和重排序规则。

<stdatomic.h> 提供了不同的内存顺序枚举类型:

  • memory_order_relaxed:最宽松的顺序,只保证原子操作本身是原子的,但不保证与其他操作的顺序。
  • memory_order_acquire:在一个线程中,acquire 之后的读操作不能被重排到 acquire 之前,常用于加锁后。
  • memory_order_release:在一个线程中,release 之前的写操作不能被重排到 release 之后,常用于解锁前。
  • memory_order_acq_relacquirerelease 的组合。
  • memory_order_seq_cst顺序一致性,默认也是最强的内存顺序,它保证所有线程看到的原子操作顺序都是一致的,就像所有操作都在一个全局时钟下按顺序执行一样,性能开销最大,但最容易推理。

默认情况下,C11 的原子操作使用 memory_order_seq_cst 对于性能要求极高的场景,开发者可以使用更宽松的内存顺序来优化,但这需要非常小心,否则容易引入难以发现的 bug。

atomic_store(&x, 10, memory_order_relaxed);
atomic_store(&y, 20, memory_order_relaxed);

这两个存储操作可能被编译器或CPU重排序。


  1. _Atomic 是 C11 引入的用于定义原子类型的关键字,旨在解决多线程环境下的数据竞争问题。
  2. 它将“读-修改-写”等操作打包成一个不可分割的单元。
  3. 必须使用 <stdatomic.h> 中的函数(如 atomic_load, atomic_store, atomic_fetch_add, atomic_compare_exchange_weak)来操作原子变量,不能直接赋值或读取。
  4. atomic_flag 是一个简单的原子布尔类型,非常适合用于实现自旋锁。
  5. _Atomicvolatile 是两个完全不同的概念:_Atomic 保证原子性,volatile 保证内存可见性,且 _Atomic 自动具有 volatile 的特性。
  6. _Atomic 操作支持不同的内存顺序,从最宽松的 relaxed 到最强的 seq_cst(默认),用于在性能和正确性之间进行权衡。

掌握 _Atomic 是编写高效、可靠的多线程 C 程序的基础,对于简单的计数器、标志位等场景,它比使用重量级的互斥锁(pthread_mutex_t)性能更高。

-- 展开阅读全文 --
头像
dede如何获取当前时间?
« 上一篇 04-21
dede发布文章为何会错位?
下一篇 » 04-21

相关文章

取消
微信二维码
支付宝二维码

目录[+]