原子操作atomic

目录

  • [1. 什么是原子操作?](#1. 什么是原子操作?)
    • [1.1 原子操作相关函数](#1.1 原子操作相关函数)
  • [2. 原子变量](#2. 原子变量)
  • [3. 原子变量与普通变量的区别](#3. 原子变量与普通变量的区别)
  • [4. 原子锁的实现](#4. 原子锁的实现)
  • [5. 实例代码](#5. 实例代码)

1. 什么是原子操作?

原子操作是计算机科学中的概念,指的是在执行期间不能被中断的一组操作。在多线程环境中,确保原子操作的执行是不可分割的,要么完全执行,要么完全不执行。这种特性使得在并发编程中更容易管理共享资源,避免竞态条件和数据不一致。

原子操作的概念和实现起源于计算机科学和并发编程的领域。在多核处理器和多线程应用程序普及之前,原子操作并不是那么重要。然而,随着硬件的发展和计算机体系结构的演变,原子操作成为处理并发和保证数据一致性的关键工具。

C语言引入原子操作的头文件 <stdatomic.h> 是在 C11 标准中的一项重要改进。它提供了一套通用的原子类型和原子操作函数,为程序员提供了更直接、更高效的多线程编程工具。在此之前,一些编译器提供了自己的原子操作扩展,但它们通常是非标准的。

在C语言中,原子操作通常通过 <stdatomic.h> 头文件中定义的原子操作函数来实现。这些函数提供了一组能够确保原子性的操作,如比较交换、加法、减法等。

c 复制代码
#include <stdatomic.h>

atomic_int my_atomic_variable = ATOMIC_VAR_INIT(0);

void increment_atomic_variable() {
    atomic_fetch_add(&my_atomic_variable, 1);
}

1.1 原子操作相关函数

在 C 语言中,<stdatomic.h> 提供了一系列原子操作函数,用于对原子变量执行操作。这些原子操作函数包括带有 __ 前缀和不带有 __ 前缀的两种版本。下面是一些常见的原子操作函数及其区别的示例:

  1. 带有 __ 前缀的原子操作函数:

__atomic_load / __atomic_store

  • 作用: 分别用于加载和存储原子变量的值。
  • __atomic_load 用于加载,返回原子变量的当前值;__atomic_store 用于存储,将原子变量设置为指定的值。
c 复制代码
#include <stdatomic.h>

atomic_int my_atomic_variable = ATOMIC_VAR_INIT(42);

int loaded_value = __atomic_load(&my_atomic_variable, __ATOMIC_RELAXED);
__atomic_store(&my_atomic_variable, 100, __ATOMIC_RELAXED);

__atomic_exchange

  • 作用: 原子地将原子变量的值与给定值进行交换。
  • 返回交换前的原子变量值。
c 复制代码
int previous_value = __atomic_exchange(&my_atomic_variable, 55, __ATOMIC_RELAXED);
  1. 不带有 __ 前缀的原子操作函数:

atomic_load / atomic_store

  • 作用: 分别用于加载和存储原子变量的值。
  • atomic_load 用于加载,返回原子变量的当前值;atomic_store 用于存储,将原子变量设置为指定的值。
c 复制代码
#include <stdatomic.h>

atomic_int my_atomic_variable = ATOMIC_VAR_INIT(42);

int loaded_value = atomic_load(&my_atomic_variable);
atomic_store(&my_atomic_variable, 100);

atomic_exchange

  • 作用: 原子地将原子变量的值与给定值进行交换。
  • 返回交换前的原子变量值。
c 复制代码
int previous_value = atomic_exchange(&my_atomic_variable, 55);
  1. 命名方式: 带有 __ 前缀的函数更加底层,使用的是原子类型的操作,更接近硬件级别的实现。不带有 __ 前缀的函数是对带有 __ 前缀的函数的封装,更易用但可能效率稍低。

  2. 可移植性: 不带有 __ 前缀的函数更符合 C 标准,因此具有更好的可移植性。带有 __ 前缀的函数可能依赖于具体的编译器实现。

在实际使用中,可以根据具体需求和平台选择适合的函数。通常情况下,不带有 __ 前缀的函数已经足够满足大多数需求,而且更具可移植性。

2. 原子变量

原子变量是一种特殊的变量类型,它在多线程环境中能够进行原子操作。C11 标准引入了 <stdatomic.h> 头文件,通过 atomic 关键字,我们可以声明原子变量。原子变量通过原子操作函数确保了对它们的操作是原子的,防止了竞态条件。

c 复制代码
#include <stdatomic.h>

atomic_int my_atomic_variable = ATOMIC_VAR_INIT(0);

ATOMIC_VAR_INIT 是一个宏,用于初始化原子变量。这个宏接受一个参数,将其用作原子变量的初始值。在上述例子中,ATOMIC_VAR_INIT(0) 的作用是将一个整数值 0 用作 atomic_int 类型的原子变量的初始值。

<stdatomic.h> 头文件中,原子类型的初始化通常可以使用 ATOMIC_VAR_INIT 宏,它的定义可能类似于以下方式:

c 复制代码
#define ATOMIC_VAR_INIT(value) (value)

这个宏的本质是一个简单的宏,它接受一个值并将其返回。在 C 语言中,结构体和数组等类型的初始化通常需要使用特定的语法,但对于原子变量,可以使用 ATOMIC_VAR_INIT 来简化初始化过程。

在代码中,ATOMIC_VAR_INIT(0)0 作为初始值传递给 atomic_int 类型的原子变量 atomic_variable。这是一种方便的初始化原子变量的方式。

3. 原子变量与普通变量的区别

与普通变量相比,原子变量提供了更强的同步保证。在多线程环境中,普通变量的读写可能发生竞态条件,而原子变量通过原子操作函数确保了操作的原子性。这种保证使得在并发环境中更容易编写线程安全的代码。

c 复制代码
int common_variable = 0; // 普通变量
atomic_int atomic_variable = ATOMIC_VAR_INIT(0); // 原子变量

原子变量的存储位置通常由编译器和程序的具体实现决定,而不是由原子变量本身决定。这些与普通变量一致。原子变量可以存储在堆区、栈区或全局区,具体取决于它们的声明位置和作用域。

  1. 全局区/静态存储区: 如果原子变量是在全局作用域声明,或者使用 static 关键字在函数内部声明,它们通常被分配在全局区或静态存储区。这意味着它们在程序的整个生命周期内存在。

    c 复制代码
    #include <stdatomic.h>
    
    atomic_int global_atomic_variable = ATOMIC_VAR_INIT(0);
    
    int main() {
        // ...
        return 0;
    }
  2. 堆区: 如果原子变量是通过动态内存分配函数(如 malloccalloc 等)在堆上分配的,那么它们将存储在堆区。这通常涉及到使用指针管理原子变量。

    c 复制代码
    #include <stdatomic.h>
    #include <stdlib.h>
    
    int main() {
        atomic_int *heap_atomic_variable = malloc(sizeof(atomic_int));
        *heap_atomic_variable = ATOMIC_VAR_INIT(0);
    
        // ...
    
        free(heap_atomic_variable); // 记得释放内存
        return 0;
    }
  3. 栈区: 如果原子变量是在函数内部声明且没有使用 static 关键字,它们通常被分配在栈上。这意味着它们的生命周期受限于函数的执行期间。

    c 复制代码
    #include <stdatomic.h>
    
    void someFunction() {
        atomic_int local_atomic_variable = ATOMIC_VAR_INIT(0);
    
        // ...
    }
    
    int main() {
        someFunction();
        return 0;
    }

总的来说,原子变量的存储位置与普通变量类似,取决于它们的声明方式和作用域。原子操作主要关注于对变量的原子性操作,而不是变量的存储位置。

4. 原子锁的实现

原子锁是一种同步机制,用于确保在同一时刻只有一个线程能够进入临界区。自旋锁是一种常见的原子锁,它使用忙等待的方式来获取锁。

c 复制代码
#include <stdatomic.h>

typedef struct {
    atomic_flag flag;
} spinlock_t;

void spinlock_lock(spinlock_t *lock) {
    while (atomic_flag_test_and_set(&lock->flag)) {
        // 在这里可以进行自旋等待或者调用 __mm_pause() 以提高性能
    }
}

void spinlock_unlock(spinlock_t *lock) {
    atomic_flag_clear(&lock->flag);
}

atomic_flag_test_and_set 是 C11 标准中定义的原子操作函数。它用于原子地测试并设置一个 atomic_flag,通常用于实现自旋锁或其他类似的同步机制。

以下是 atomic_flag_test_and_set 的函数原型:

c 复制代码
_Bool atomic_flag_test_and_set(volatile atomic_flag *flag);
  • flag 是一个指向 atomic_flag 的指针,表示需要进行测试和设置的标志。
  • 返回值是一个 _Bool 类型,表示在进行操作前,flag 的当前值是否为 true。如果当前值为 true,则返回 true,表示标志已经被设置;如果当前值为 false,则返回 false,表示标志在进行测试和设置前是未设置的。

这个操作可以被视为一个原子的比较和设置操作。在多线程环境中,它确保在进行测试和设置的整个过程中不会被中断,从而防止竞态条件。

下面是一个简单的例子,演示了 atomic_flag_test_and_set 的使用:

c 复制代码
#include <stdatomic.h>

atomic_flag my_flag = ATOMIC_FLAG_INIT;

void acquire_lock() {
    while (atomic_flag_test_and_set(&my_flag)) {
        // 如果标志已经被设置,继续自旋等待
    }
}

void release_lock() {
    atomic_flag_clear(&my_flag);  // 清除标志,释放锁
}

在这个例子中,acquire_lock 函数尝试获取锁,它使用 atomic_flag_test_and_set 来原子地测试并设置 my_flag。如果 my_flag 已经被设置,说明锁已经被其他线程占用,当前线程将继续自旋等待。release_lock 函数用于释放锁,它使用 atomic_flag_clear 来清除 my_flag,表示锁已经释放。

5. 实例代码

以下是一个简单的自旋锁的 C 语言实现:

自旋锁是一种同步机制,它使用忙等待(自旋)的方式来获取锁。在使用自旋锁时,如果锁已被其他线程占用,当前线程将一直循环检测锁是否被释放,而不是立即进入休眠状态。只有当锁被释放时,当前线程才能成功获取锁。

c 复制代码
#include <stdatomic.h>

typedef struct {
    atomic_flag flag;
} spinlock_t;

void spinlock_init(spinlock_t *lock) {
    atomic_flag_clear(&lock->flag);
}

void spinlock_lock(spinlock_t *lock) {
    while (atomic_flag_test_and_set(&lock->flag)) {
        // 如果锁已经被占用,继续自旋等待
    }
}

void spinlock_unlock(spinlock_t *lock) {
    atomic_flag_clear(&lock->flag);
}

上述代码中,spinlock_t 结构体包含一个原子标志(atomic_flag),用于表示锁的状态。spinlock_init 用于初始化自旋锁,spinlock_lock 用于上锁,spinlock_unlock 用于解锁。

使用这个自旋锁:

c 复制代码
#include <stdio.h>
#include <pthread.h>

spinlock_t my_lock;

void *worker_thread(void *arg) {
    int thread_id = *(int *)arg;

    spinlock_lock(&my_lock);

    // 临界区代码
    printf("Thread %d is in critical section.\n", thread_id);

    spinlock_unlock(&my_lock);

    return NULL;
}

int main() {
    spinlock_init(&my_lock);

    pthread_t thread1, thread2;
    int id1 = 1, id2 = 2;

    pthread_create(&thread1, NULL, worker_thread, &id1);
    pthread_create(&thread2, NULL, worker_thread, &id2);

    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    return 0;
}

这个例子创建了两个线程,它们共享一个自旋锁。每个线程在进入临界区之前先调用 spinlock_lock 上锁,然后在退出临界区时调用 spinlock_unlock 解锁。这样,自旋锁确保在同一时刻只有一个线程能够进入临界区。

相关推荐
52Hz1181 天前
力扣230.二叉搜索树中第k小的元素、199.二叉树的右视图、114.二叉树展开为链表
python·算法·leetcode
苦藤新鸡1 天前
56.组合总数
数据结构·算法·leetcode
萧曵 丶1 天前
Docker 面试题
运维·docker·容器
七牛云行业应用1 天前
3.5s降至0.4s!Claude Code生产级连接优化与Agent实战
运维·人工智能·大模型·aigc·claude
小草cys1 天前
鲲鹏920服务器安装openEuler后无法联网,但物理网线已连接
运维·服务器·openeuler
LiLiYuan.1 天前
【Cursor 中找不到LeetCode 插件解决办法】
算法·leetcode·职场和发展
Charlie_lll1 天前
力扣解题-[3379]转换数组
数据结构·后端·算法·leetcode
Volunteer Technology1 天前
FastDFS+Nginx
运维·nginx
captain3761 天前
Java队列(Queue)
算法·链表