C11标准库中的atomic原子操作

Intro

C11标准也推出了很久,只是对于嵌入式编程,一方面许多嵌入式工具链版本号较低,另一方面Linux内核编译选项还是c89的标准,以至于很多便利的特性都没用上,头文件还是用#ifndef宏来隔离、局部变量都定义在函数开始处、循环变量不能定义在循环中,更不用提动态数组了。

C标准和libc库一直在演进,加入了许多新特性,这里说说atomic原子操作。Linux内核作为操作系统,不可避免的需要内建atomic操作,一方面为了防止在多线程中data race,另一方面,比起各种锁,atomic开销小很多。关于内核的atomic,有很多文章进行介绍和说明:

需要注意的是,内核中的很多原子操作的语义和C11中的定义是不同的,例如cmpxchg。

Standard

在C11标准中,首次引入原子操作,正式将其标准化。然而在我现在的系统ubuntu14.04上,仍然不能使用man来查看其相关手册,需要看标准文档。ansi.org上的文档估计要$60吧,不过在网上还能免费下载到草案文档WG14 draft version N1570。里面7.17 Atomics <stdatomic.h>章节中给出了详尽的定义。这里简单的介绍一下。

首先,定义了所需要引用的头文件stdatomic.h。在我的系统上,该头文件并没有在/usr/include目录下,而是/usr/lib/gcc/x86_64-linux-gnu/4.9/include/stdatomic.h

插一句题外话,从标准中还可以看到stdbool.hstdint.h, stddef.h等等头文件,定义了很多以前标准中没有、然而特别实用的类型,特别是很多C++里的类型。

其次,标准定义了__STDC_NO_THREADS__宏,用来在编译时检测是否支持stdatomic。同时还有一系列的宏和函数用来判断各种数据类型在当前的实现中是否支持原子操作,例如ATOMIC_CHAR_LOCK_FREE, atomic_is_lock_free

然后,标准引入了memory order and consistency,在不同的memory order下,原子操作的效率和严格性不尽相同,具体可以看这里这里

同时,标准定义了许多原子数据类型,例如atomic_char, atomic_int, atomic_size_t。必须使用这些数据类型,因为其类型内部可能包含其他数据来保证原子性操作。

初始化原子变量可以使用如下函数,但不保证原子性(当然一般也不会在多线程中进行初始化):

  • ATOMIC_VAR_INIT
  • atomic_init
  • ATOMIC_FLAG_INIT

操作原子变量则使用如下函数,保证原子性:

  • atomic_store
  • atomic_load
  • atomic_exchange
  • atomic_compare_exchange_strong, atomic_compare_exchange_weak
  • atomic_fetch_add, atomic_fetch_sub, atomic_fetch_or, atomic_fetch_xor, atomic_fetch_and
  • atomic_flag_test_and_set
  • atomic_flag_clear

以上函数还都有xxx_explicit版本,多出一个memory_order参数,用来显示指定order。

gcc

在C11之前,gcc对原子操作的支持是通过builtin函数实现的,即__sync前缀的函数。后来修改为__atomic前缀。

在C11发布之后,gcc通过stdatomic.h提供标准接口。其实如果查看gcc的stdatomic.h,可以发现里面的函数都还是由__atomic的builtin函数来实现的。

gcc在4.9版本之后才正式、完备的支持stdatomic,在编译命令中加上-std=c11-std=gnu11即可。如果是之前的版本,那只能使用builtin函数了。

关于gcc的atomic情况,可以gcc的wiki页,里面包含很多有用的链接。

Example & Docs

我写了一个简单的测试程序,启动偶数个线程,每个线程循环固定次数对同一个atomic_int变量操作,一半进行加一操作,另一半进行减一操作。

另外,文章C11 Lock-free Stack详细地讲解了如何使用C11 atomic实现一个无锁的stack数据结构,深入浅出,很值得读一读。

除了以上简单的介绍之外,网上还有很多对c11 atomic的不同角度介绍和分析的文章: