原子操作atomic

本文详细介绍了原子操作在多线程编程中的重要性,包括原子操作的概念、C语言中的原子操作函数、原子变量与普通变量的区别,以及原子锁(如自旋锁)的实现原理。着重强调了原子操作如何避免竞态条件,确保数据一致性。
摘要由CSDN通过智能技术生成

1. 什么是原子操作?

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

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

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

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

#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 用于存储,将原子变量设置为指定的值。
#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

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

atomic_load / atomic_store

  • 作用: 分别用于加载和存储原子变量的值。
  • atomic_load 用于加载,返回原子变量的当前值;atomic_store 用于存储,将原子变量设置为指定的值。
#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

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

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

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

2. 原子变量

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

#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 宏,它的定义可能类似于以下方式:

#define ATOMIC_VAR_INIT(value) (value)

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

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

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

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

int common_variable = 0; // 普通变量
atomic_int atomic_variable = ATOMIC_VAR_INIT(0); // 原子变量

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

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

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

    #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 关键字,它们通常被分配在栈上。这意味着它们的生命周期受限于函数的执行期间。

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

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

4. 原子锁的实现

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

#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 的函数原型:

_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 的使用:

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

#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 用于解锁。

  使用这个自旋锁:

#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 解锁。这样,自旋锁确保在同一时刻只有一个线程能够进入临界区。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值