# 1 概念
# 1.1 多核处理器的多线程同步
在多核处理器或多个处理器的环境下,多线程同步则更为复杂。因为对于一般通用操作系统而言, 核心A是不知道核心B正在处理啥的,尽管对于某些嵌入式实时操作系统(RTOS)而言, 一个核心可以给另一个核心发送信号以通知某些事件,但这些均用于特定应用场景,而通用应用级操作系统 不会采用这些同步手段,代价太高。正因为如此,在多核多处理器环境下,如果我们要对多线程所共享的一个 对象的修改做到数据一致性,那么只能通过原子操作指令!
为何原子操作能起作用?因为原子操作指令通过锁总线等手段,确保多线程对同一共享对象的读-修改-写是 原子的。所谓“原子(atomic)”就是指,一组操作在执行时作为一个整体进行,而不会被打断。
# 1.2 原子操作的种类
随着科技的进步,现代多核处理器纷纷引入了无锁(Lock-Free)原子操作,比如 比较与交换(Compare and Swap,简称CAS), 加载时锁定/有条件存储(Load-locked Store-conditional,简称LL-SC)。 x86处理器使用前者,Alpha、Power、MIPS、RISC-V、ARMv7、ARMv8等则使用后者, 而从ARMv8.1开始也引入了CAS原子操作。
# 2 stdatomic.h
# 2.1 类型定义
原子操作库在C11标准中属于可选库,对于一些低端的处理器,尤其像8位的单片机MCU那种完全不具备
原子操作指令的系统环境,则可以不提供此库。因此C11标准引入了__STDC_NO_ATOMICS__
这个
预定义宏来指示当前系统环境下的C11编译器实现是否提供了原子操作标准库。如果定义了这个宏,
则说明当前环境下的C11编译器实现没有支持原子操作的标准库。如果支持原子操作标准库,
我们就能引入 <stdatomic.h>
这个头文件,这里面声明了所以能被当前C11编译器支持的原子类型
以及原子操作函数接口。ubuntu 里的位置为:
/usr/lib/gcc/x86_64-linux-gnu/4.9/include/stdatomic.h
。
为了能给程序员提供当前编译环境能支持何种原子操作,C11标准列出了以下这些与定义宏:
// 指示当前编译器能支持 _Bool 类型的无锁原子操作
ATOMIC_BOOL_LOCK_FREE
// 指示当前编译器能支持 signed char、unsigned char、以及char类型的无锁原子操作
ATOMIC_CHAR_LOCK_FREE
// 指示当前编译器能支持 char16_t 类型的无锁原子操作
ATOMIC_CHAR16_T_LOCK_FREE
// 指示当前编译器能支持 char32_t 类型的无锁原子操作
ATOMIC_CHAR32_T_LOCK_FREE
// 指示当前编译器能支持 wchar_t 类型的无锁原子操作
ATOMIC_WCHAR_T_LOCK_FREE
// 指示当前编译器能支持 short、unsigned short 类型的无锁原子操作
ATOMIC_SHORT_LOCK_FREE
// 指示当前编译器能支持 int、unsigned int 类型的无锁原子操作
ATOMIC_INT_LOCK_FREE
// 指示当前编译器能支持 long、unsigned long 类型的无锁原子操作
ATOMIC_LONG_LOCK_FREE
// 指示当前编译器能支持 long long、unsigned long long 类型的无锁原子操作
ATOMIC_LLONG_LOCK_FREE
// 指示当前编译器能支持 ptrdiff_t、intptr_t、uintptr_t 类型的无锁原子操作
ATOMIC_POINTER_LOCK_FREE
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
有了这些预定义宏之后,我们就能判定在当前编译环境下可用那些原子操作了。 当然,如果当前编译环境能支持原子操作的话,那么它至少应该能支持 atomic_flag 类型。 该类型是一个原子标志对象,用于flag test and set原子操作。 如果当前的CPU不支持flag test and set,但支持SWAP,那么也可以用SWAP来实现该操作。 此外,其他lock-free的原子操作也都能实现flag test and set, 包括原子加法、原子逻辑操作、CAS等。因此 原子标志操作 属于整个原子操作中最最基本的操作方式。
当前C11标准所提供支持的整数原子类型如下所示:
typedef _Atomic(_Bool) atomic_bool;
typedef _Atomic(char) atomic_char;
typedef _Atomic(signed char) atomic_schar;
typedef _Atomic(unsigned char) atomic_uchar;
typedef _Atomic(short) atomic_short;
typedef _Atomic(unsigned short) atomic_ushort;
typedef _Atomic(int) atomic_int;
typedef _Atomic(unsigned int) atomic_uint;
typedef _Atomic(long) atomic_long;
typedef _Atomic(unsigned long) atomic_ulong;
typedef _Atomic(long long) atomic_llong;
typedef _Atomic(unsigned long long) atomic_ullong;
// 对于没有定义过char16_t的编译环境,
// 也可能会用 _Atomic(uint_least16_t) 类型来定义其相应的原子类型
typedef _Atomic(char16_t) atomic_char16_t;
// 对于没有定义过char32_t的编译环境,
// 也可能会用 _Atomic(uint_least32_t) 类型来定义其相应的原子类型
typedef _Atomic(char32_t) atomic_char32_t;
typedef _Atomic(wchar_t) atomic_wchar_t;
typedef _Atomic(int_least8_t) atomic_int_least8_t;
typedef _Atomic(uint_least8_t) atomic_uint_least8_t;
typedef _Atomic(int_least16_t) atomic_int_least16_t;
typedef _Atomic(uint_least16_t) atomic_uint_least16_t;
typedef _Atomic(int_least32_t) atomic_int_least32_t;
typedef _Atomic(uint_least32_t) atomic_uint_least32_t;
typedef _Atomic(int_least64_t) atomic_int_least64_t;
typedef _Atomic(uint_least64_t) atomic_uint_least64_t;
typedef _Atomic(int_fast8_t) atomic_int_fast8_t;
typedef _Atomic(uint_fast8_t) atomic_uint_fast8_t;
typedef _Atomic(int_fast16_t) atomic_int_fast16_t;
typedef _Atomic(uint_fast16_t) atomic_uint_fast16_t;
typedef _Atomic(int_fast32_t) atomic_int_fast32_t;
typedef _Atomic(uint_fast32_t) atomic_uint_fast32_t;
typedef _Atomic(int_fast64_t) atomic_int_fast64_t;
typedef _Atomic(uint_fast64_t) atomic_uint_fast64_t;
typedef _Atomic(intptr_t) atomic_intptr_t;
typedef _Atomic(uintptr_t) atomic_uintptr_t;
typedef _Atomic(size_t) atomic_size_t;
typedef _Atomic(ptrdiff_t) atomic_ptrdiff_t;
typedef _Atomic(intmax_t) atomic_intmax_t;
typedef _Atomic(uintmax_t) atomic_uintmax_t;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
绝大部分处理器都没有提供针对浮点数的原子操作指令,因此C11标准也没有提供任何针对浮点数的原子类型!
# 2.3 原子操作函数接口
C11所提供的原子操作函数接口一般都有两个版本。第一个版本是不带有存储器次序参数的,
第二个是带有存储器次序参数的,并且函数名也以 _explicit
结尾。
1. 原子标志相关操作
对于原子标志的相关操作,C11标准提供了初始化、标志测试与设置、标志清除这三个接口。 原子标志 atomic_flag 对象本身只有两种状态(即只有两种取值), 设置状态(编译器实现一般用 1 或 true 来表示)以及 清零状态(编译器实现一般用 0 或 false 来表示)。
C11标准为原子标志类型提供了一个用于初始化的宏—— ATOMIC_FLAG_INIT , 我们应该用这个宏对一个原子标志对象进行初始化,代码示例:
volatile atomic_flag g_flag = ATOMIC_FLAG_INIT;
int main(void)
{
// 这里展示了如何在函数内对已声明的g_flag进行初始化。
// 由于atomic_flag通常被定义为一种结构体形式,
// 而ATOMIC_FLAG_INIT则通常被定义为针对结构体atomic_flag的初始化器,
// 即:{ ... } 的形式。
// 因此我们这里使用C99标准所引入的匿名结构体对象的表示语法
// 对g_flag进行初始化
g_flag = (atomic_flag)ATOMIC_FLAG_INIT;
}
2
3
4
5
6
7
8
9
10
11
12
初始化之后,原子标志对象即处于“清零状态”。
2. 初始化原子变量(不保证原子性)
// C17 中被弃用
#define ATOMIC_VAR_INIT(VALUE)»■(VALUE)
/* 初始化一个原子对象 */
// 接收一个参数,用于指定该原子对象的初始值。
// 必须注意的是,初始值的类型要与原子对象的整数类型相兼容。
#define atomic_init(PTR, VAL) \
atomic_store_explicit (PTR, VAL, __ATOMIC_RELAXED)
#define ATOMIC_FLAG_INIT»■■■{ 0 }
2
3
4
5
6
7
8
9
10
3. 操作原子变量(保证原子性)