什么是原子操作呢?即操作本身无法再被划分为更细的步骤。我们一般都是在多线程环境中,才会需要原子操作的支持。因为当多个线程中对共享资源进行原子操作时,编译器和 CPU 将能够保证这些操作的正确执行。原子操作就是说同一时刻只会有一个线程在操作共享资源。当该线程将整个原子操作全部执行完毕后,其他线程才可以继续执行同样的操作。
C11 提供了 stdatomic.h 的头文件,可以方便地使用原子操作能力。与原子操作相关的常用C标准库函数:
- atomic_flag_test_and_set : 将一个atomic_flag的值设置为真,并返回旧值
- atomic_flag_clear:将一个atomic_flag的值设值为假
- atomic_init:初始化g个已经存在的原子对象
- atomic_fetch_add_explicit:原子加法
- atomic_is_lock_free:检查指定对象是否是lock-free的
- atomic_exchange:原子地交换两个值
- atomic_compare_exchange_weak:比较并原子地交换两个值(允许失败)
- atomic_compare_exchange_strong:比较并原子地交换两个值
- atomic_signal_fence:在线程和信号处理之间建立内存栅栏
- atomic_thread_fence:在线程之间建立内存栅栏
#include <threads.h>
#include <stdio.h>
#include <stdatomic.h>#define THREAD_COUNT 10
#define THREAD_LOOP 100000000#ifndef __STDC_NO_ATOMICS__ // 判断编译器是否对原子操作提供支持
_Atomic long counter = 0; // 原子类型的全局变量,线程们的共享资源
#endifint run(void *arg){for(int i = 0; i < THREAD_LOOP; i++){atomic_fetch_add_explicit(&counter,1,memory_order_relaxed);//使用原子加法}printf("Thread %d terminates.\n",*((int*)arg));return thrd_success;
}int main(void){
#if !defined(__STDC_NO_THREADS__) || !defined(__STDC_NO_ATOMICS__)int ids[THREAD_COUNT];thrd_t threads[THREAD_COUNT];for(int i = 0; i < THREAD_COUNT; i++){ids[i] = i+1;thrd_create(&threads[i],run,ids+i);//创建线程}for(int i = 0; i < THREAD_COUNT; i++){thrd_join(threads[i],NULL);//等待线程执行完成}printf("Counter value is: %ld.\n",counter);
#endifreturn 0;
}
~/Desktop$ gcc atomic.c -o atomic
~/Desktop$ ./atomic
Thread 9 terminates.
Thread 10 terminates.
Thread 2 terminates.
Thread 3 terminates.
Thread 7 terminates.
Thread 4 terminates.
Thread 5 terminates.
Thread 8 terminates.
Thread 6 terminates.
Thread 1 terminates.
Counter value is: 1000000000.
_Atomic
C11 新引入的 关键字,修饰全局变量 counter,将它定义为一个原子类型,也可以使用C 标准库为我们封装好的宏 atomic_long来声明。
atomic_fetch_add_explicit
函数来完成对 counter 变量的累加过程,使线程在进行数据累加时独占整个变量。该函数的第三个参数指定当前操作需要满足的内存顺序。编译器和处理器可能会采用指令重排来优化程序的运行效率,当在多核 CPU 上运行存在线程间数据依赖的多线程应用时,程序的正确性可能会出现问题。可以通过指定各个原子操作的具体内存顺序来解决这个问题。内存顺序枚举值用途:
- memory_order_relaxed:对执行顺序不做任何保证,编译器和处理器可以按照需求进行适当的优化。
- memory_order_release:必须完成所有之前的写操作,才能执行本条写操作
- memory_order_acquire:所有后续的读操作,必须在本条执令完成后才能执行
与使用互斥量相比,原子操作可以更加清晰和方便地抽象并行代码,而不需要频繁进行加锁与释放锁的操作。从性能角度来看,原子操作的执行通常直接依赖于 CPU 提供的相应的原子机器指令。而使用互斥量则需要让线程阻塞,还要频繁进行上下文切换,比较之下,原子操作的性能一般会更好。