c语言atomic实现

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

什么是原子操作?

原子操作,就是一个不可中断的操作,它要么完整地执行完毕,要么完全不执行,中间不会被任何其他线程或中断打断,在多线程编程中,原子操作是确保共享数据安全访问和修改的基石,因为它可以避免竞态条件

c语言atomic实现
(图片来源网络,侵删)

一个经典的例子是 count++ 操作,在高层次上,它看起来是一个单一操作,但在底层,它通常至少包含三个步骤:

  1. 读取 count 的当前值到寄存器。
  2. 增加 寄存器中的值。
  3. 写回 寄存器中的新值到 count 的内存地址。

如果没有原子性,在多线程环境下,可能会发生以下情况:

  • 线程A读取 count (值为5)。
  • 线程B读取 count (值也为5)。
  • 线程A将寄存器中的值加1,变为6,并写回 count
  • 线程B也将寄存器中的值加1,变为6,并写回 count

count 的值是6,而不是预期的7,这就是一个典型的竞态条件。

原子操作通过硬件指令(如 x86 的 LOCK 前缀指令)或操作系统/编译器的内部机制来保证这三个步骤作为一个整体执行,从而避免上述问题。

c语言atomic实现
(图片来源网络,侵删)

C11 标准中的原子操作

C11 标准在 <stdatomic.h> 头文件中引入了一套完整的原子操作API,这是在现代C语言中进行无锁编程的标准方法。

1 核心概念

  • _Atomic 类型限定符:你可以用它来声明一个原子变量。
    _Atomic int atomic_counter;
    // 或者使用 typedef
    typedef _Atomic int atomic_int;
    atomic_int atomic_counter;
  • atomic_flag:一个最简单的原子布尔类型,它只支持 test-and-setclear 操作,非常适合用作自旋锁。
  • memory_order:内存序,它定义了原子操作与内存操作之间的可见性和顺序关系,这是原子操作中最复杂也最重要的部分。

2 内存序

内存序决定了其他线程何时能看到一个原子变量的修改,以及编译器和CPU如何重排指令。

C11 定义了六种内存序:

内存序 描述 适用场景
memory_order_relaxed 最宽松,只保证原子操作本身的原子性,不保证任何顺序。 简单的计数器,不依赖其他变量的顺序。
memory_order_consume 当前线程依赖该原子变量的值(数据依赖)的后续读取操作,不能被重排到该原子操作之前。 较少使用,memory_order_acquire 通常是更好的选择。
memory_order_acquire 获取,保证在该原子操作之后的所有读/写操作,都不能被重排到该原子操作之前 用于加锁或获取数据,确保你看到的是最新的、完整的数据。
memory_order_release 释放,保证在该原子操作之前的所有读/写操作,都不能被重排到该原子操作之后 用于解锁或发布数据,确保所有修改对其他线程可见。
memory_order_acq_rel 获取和释放,结合了 acquirerelease 的语义。 在读取和写入都发生的原子操作中使用(如 fetch_add)。
memory_order_seq_cst 顺序一致性最严格,所有线程都按照一个统一的、全局的顺序来观察所有原子操作,默认选项,但性能开销最大。 需要绝对确定性的行为,不关心性能的场景。

重要提示:如果你不确定使用哪种内存序,memory_order_seq_cst 是最安全的选择,如果你追求性能,则需要仔细理解并选择合适的序。

c语言atomic实现
(图片来源网络,侵删)

C11 原子操作函数详解

<stdatomic.h> 提供了一系列宏函数来操作原子变量,这些函数通常命名为 atomic_...

1 基本操作

假设我们有一个原子变量 atomic_int my_var;

函数 描述 示例
atomic_init 静态初始化原子变量。 atomic_int my_var = ATOMIC_VAR_INIT(10);
atomic_load 原子地读取一个值。 int val = atomic_load(&my_var);
atomic_store 原子地存储一个值。 atomic_store(&my_var, 20);
atomic_exchange 原子地交换一个新值,并返回旧值。 int old_val = atomic_exchange(&my_var, 30);

2 "Fetch-Op" 模式

这类函数先获取旧值,然后执行一个操作(如加、与、或等),最后存储新值,并返回旧值,这是最常用的模式。

函数 描述 示例
atomic_fetch_add 原子地加一个值。 atomic_fetch_add(&my_var, 1); // my_var += 1
atomic_fetch_sub 原子地减一个值。 atomic_fetch_sub(&my_var, 1); // my_var -= 1
atomic_fetch_and 原子地执行按位与。 atomic_fetch_and(&my_var, 0xFF);
atomic_fetch_or 原子地执行按位或。 atomic_fetch_or(&my_var, 0x01);
atomic_fetch_xor 原子地执行按位异或。 atomic_fetch_xor(&my_var, 0x01);
atomic_fetch_min 原子地取最小值。 atomic_fetch_min(&my_var, 100);
atomic_fetch_max 原子地取最大值。 atomic_fetch_max(&my_var, 100);

所有 fetch-op 函数都默认使用 memory_order_seq_cst,但可以指定内存序: atomic_fetch_add_explicit(&my_var, 1, memory_order_relaxed);

3 比较并交换

这是原子操作中最强大的指令,是实现无锁数据结构(如无锁队列、栈)的核心。

atomic_compare_exchange_strongatomic_compare_exchange_weak

这两个函数的作用是:“如果当前值等于预期值,就用新值替换它”。

  • strong 版本:保证在值不等于预期值时绝对不会“伪失败”(spuriously fail),除非真的被其他线程修改了,否则不会返回 false。
  • weak 版本:可能会“伪失败”,即使值没有改变也可能返回 false,但它的性能通常更好,因为某些硬件实现上 weak 版本更高效。

使用模式(关键!):CAS 操作通常在一个循环中使用。

int expected = 10;
int new_val = 20;
// 尝试将 my_var 从 10 变为 20
while (!atomic_compare_exchange_strong(&my_var, &expected, new_val)) {
    // 如果失败,说明 my_var 的值已经被其他线程修改了。
    // expected 的值已经被自动更新为 my_var 的当前值。
    // 我们需要重新加载 my_var 的当前值到 expected 中,然后再次尝试。
    // 注意:expected 必须是指向一个变量的指针,而不是字面量。
    expected = atomic_load(&my_var); // 重新加载当前值
}

完整代码示例

下面是一个使用 C11 原子操作实现一个线程安全的计数器的例子。

示例1:简单的原子计数器

#include <stdio.h>
#include <stdatomic.h>
#include <threads.h>
#define NUM_THREADS 4
#define INCREMENTS_PER_THREAD 1000000
// 声明一个原子整数
atomic_int counter;
// 线程函数,对计数器进行递增
int thread_func(void* arg) {
    for (int i = 0; i < INCREMENTS_PER_THREAD; i++) {
        // 使用 memory_order_relaxed 因为我们只关心计数器的最终值,
        // 不关心各个线程操作的相对顺序。
        atomic_fetch_add(&counter, 1);
    }
    return 0;
}
int main() {
    // 初始化计数器
    atomic_init(&counter, 0);
    thrd_t threads[NUM_THREADS];
    // 创建并启动线程
    for (int i = 0; i < NUM_THREADS; i++) {
        if (thrd_create(&threads[i], thread_func, NULL) != thrd_success) {
            perror("Failed to create thread");
            return 1;
        }
    }
    // 等待所有线程完成
    for (int i = 0; i < NUM_THREADS; i++) {
        thrd_join(threads[i], NULL);
    }
    // 打印最终结果
    printf("Final counter value: %d\n", atomic_load(&counter));
    // 预期结果是 NUM_THREADS * INCREMENTS_PER_THREAD
    if (atomic_load(&counter) == NUM_THREADS * INCREMENTS_PER_THREAD) {
        printf("Test passed!\n");
    } else {
        printf("Test failed!\n");
    }
    return 0;
}

示例2:使用 atomic_flag 实现自旋锁

atomic_flag 是一个简单的测试并置位标志,非常适合用作自旋锁。

#include <stdio.h>
#include <stdatomic.h>
#include <threads.h>
// atomic_flag 可以用 ATOMIC_FLAG_INIT 初始化为 false (未锁定)
atomic_flag spinlock = ATOMIC_FLAG_INIT;
// 受保护的共享资源
int shared_resource = 0;
void lock() {
    // 尝试将 flag 设置为 true。
    // flag 原本就是 true (已被锁定),则循环等待 (自旋)。
    while (atomic_flag_test_and_set(&spinlock)) {
        // 空循环,等待锁被释放
        // 在实际应用中,可以考虑加入一些让渡 CPU 的指令,如 thrd_yield()
    }
}
void unlock() {
    // 将 flag 清除,释放锁
    atomic_flag_clear(&spinlock);
}
int thread_func(void* arg) {
    for (int i = 0; i < 100000; i++) {
        lock();
        // 临界区:安全地修改共享资源
        shared_resource++;
        unlock();
    }
    return 0;
}
int main() {
    shared_resource = 0;
    thrd_t t1, t2;
    thrd_create(&t1, thread_func, NULL);
    thrd_create(&t2, thread_func, NULL);
    thrd_join(t1, NULL);
    thrd_join(t2, NULL);
    printf("Final shared_resource value: %d\n", shared_resource);
    // 预期结果是 200000
    if (shared_resource == 200000) {
        printf("Lock test passed!\n");
    }
    return 0;
}

平台特定的原子操作

虽然 C11 是标准,但在某些追求极致性能的场景下,开发者可能会直接使用平台特定的原子指令,这些指令通常能提供更底层的控制和更好的性能。

  • GCC/Clang 内置函数:

    • __sync_fetch_and_add(&ptr, val)
    • __sync_lock_test_and_set(&ptr, val)
    • __atomic_fetch_add(&ptr, val, __ATOMIC_SEQ_CST)
    • GCC 也支持 C11 的 <stdatomic.h>,并且其 __atomic_* 内置函数是 C11 的超集。
  • MSVC 内置函数:

    • InterlockedIncrement((LONG volatile*)Addend)
    • InterlockedExchange((LONG volatile*)Target, Value)
    • InterlockedCompareExchange((LONG volatile*)Destination, Exchange, Comparand)
  • Windows API:

    • InterlockedIncrement, InterlockedDecrement, InterlockedExchange, InterlockedCompareExchange 等。
  • x86 汇编指令:

    • LOCK XADD (交换并相加)
    • LOCK CMPXCHG (比较并交换)

注意:使用平台特定代码会降低程序的可移植性,除非你明确知道目标平台并且对性能有极致要求,否则优先使用 C11 标准。

方法 优点 缺点
C11 <stdatomic.h> 标准、可移植、高级抽象,编译器会自动选择最优的底层实现。 内存序概念复杂,性能可能略低于平台特定代码(但通常差异不大)。
平台特定/GCC内置 性能极高,提供最底层的控制。 代码不可移植,可读性差,容易出错。

对于绝大多数C多线程应用,强烈推荐从 C11 的原子操作开始,它们提供了良好的性能、可移植性和安全性,是现代C语言并发编程的正确方向。

-- 展开阅读全文 --
头像
c语言链接access
« 上一篇 04-05
织梦采集完如何导出?步骤方法是什么?
下一篇 » 04-05

相关文章

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

目录[+]