GCC的内存原子化操作函数接口
1 原子化操作
在并发编程中,一个操作或一组操作是原子操作、可线性化操作、不可分操作或不可中断操作(atomic, linearizable, indivisible, uniterruptible),表示该操作执行时不可被中断的。操作的原子性能够保证操作在执行时免受中断、信号、并发进程线程的影响。另外,原子操作大多只有两种结果,要么成功并改变系统中对应的状态,要么没有相关效果。
原子化经常由互斥锁来保证,可以在硬件层面建立一个缓存一致性协议,也可以在软件层面使用信号量或加锁。因此,一个原子操作不是必须实际上马上生效,而操作系统让这个操作看起来是直接发生的,这能够让操作系统保持一致。正是如此,只要不影响性能,用户可以忽略较底层的实现细节。
2 GCC函数接口
GCC提供了原子化的操作接口,能够支持长度为1、2、4、8字节的整形变量或指针。
In most cases, these builtins are considered a full barrier. That is, no memory operand will be moved across the operation, either forward or backward. Further, instructions will be issued as necessary to prevent the processor from speculating loads across the operation and from queuing stores after the operation.
在大多数情况下,这些内建函数是完全内存屏障(full barrier)的,以上摘自 GCC Manual。
2.1 先取值并进行对应操作
type __sync_fetch_and_add (type *ptr, type value, ...)
type __sync_fetch_and_sub (type *ptr, type value, ...)
type __sync_fetch_and_or (type *ptr, type value, ...)
type __sync_fetch_and_and (type *ptr, type value, ...)
type __sync_fetch_and_xor (type *ptr, type value, ...)
type __sync_fetch_and_nand (type *ptr, type value, ...)
这些函数接口的执行逻辑如下代码段所示:会执行名称相对应的运算,并将内存中之前存放的值取出并返回。
{ tmp = *ptr; *ptr op= value; return tmp; }
{ tmp = *ptr; *ptr = ~(tmp & value); return tmp; } // nand
注意 :从GCC 4.4开始 __sync_fetch_and_nand
是按照 *ptr = ~(*ptr & value)
实现的,而不是 *ptr = ~*ptr & value
2.2 直接操作并返回最终结果
type __sync_add_and_fetch (type *ptr, type value, ...)
type __sync_sub_and_fetch (type *ptr, type value, ...)
type __sync_or_and_fetch (type *ptr, type value, ...)
type __sync_and_and_fetch (type *ptr, type value, ...)
type __sync_xor_and_fetch (type *ptr, type value, ...)
type __sync_nand_and_fetch (type *ptr, type value, ...)
这些函数接口的执行逻辑如下代码段所示:
{ *ptr op= value; return *ptr; }
{ *ptr = ~(*ptr & value); return *ptr; } // nand
注意 :从GCC 4.4开始 __sync_nand_and_fetch
是按照 *ptr = ~(*ptr & value)
实现的,而不是 *ptr = ~*ptr & value
2.3 比较并交换
bool __sync_bool_compare_and_swap (type *ptr, type oldval type newval, ...)
type __sync_val_compare_and_swap (type *ptr, type oldval type newval, ...)
3 内存屏障(Memory Barrier)
在上文中提到了memory barrier,这是CPU指令中的一个术语。内存屏障又叫内存栅栏(memory fence),是一种能够让CPU或编译器约束内存操作指令执行顺序的屏蔽指令。这表示在内存屏障前的指令能够保证在执行时先于内存屏障后的指令。
现代CPU使用的性能优化操作会导致指令在执行时变序,这样的指令变序对于单线程程序一般不会有很大影响,但是在多线程并发编程情况下需要小心地控制,否则会产生不可预知的结果。
内存屏障的典型应用场景是多设备之间的共享内存的底层机器码。这些代码包括原始同步机制、多核系统上的无锁数据结构、与计算机硬件交互的设备驱动。内存屏障对于无锁编程(lock-free)来说是十分重要的。
3.1 内存屏障的分类
内存屏障分为读屏障(read barrier)、写屏障(write barrier)、获取屏障(acquire barrier)、释放屏障(release barrier)等。
“写屏障”用于控制写操作的顺序。由于相对于CPU的执行速度来说,向内存中写入数据是比较慢的,通常会有一个写入请求队列,所以实际的写入操作发生在指令发起之后,队列中指令的顺序可能会被重新排序。写屏障能够防止写操作指令变序。
“读屏障”用于控制读操作的顺序。由于预先执行(CPU会提前将内存中的数据读回来),并且CPU有缓存区(CPU会从缓存中而不是内存中读取数据),读操作可能会出现变序。
“获取屏障”能够保证特定指令块之前的执行顺序。例如获取读,在向读队列中加入读操作,“获取屏障”意味着在这条操作之后可以出现指令变序,而这条操作之前不会出现指令变序。
“释放屏障”能够保证特定指令块之后的执行顺序。例如释放写,在向写队列中加入写操作,“释放屏障”意味着在这条写操作之前的指令不会变序到该指令之后,而这条该操作的之后的指令可能会变序到该指令之前。
“获取屏障”和“释放栅栏”是又叫半栅栏(half barrier),这是因为它们只能防止单方向的指令变序。
3.2 内存屏障与volatile关键字
内存屏障并不能保证数值的是“最新的”或“新鲜的”,它只能控制内存访问的相对顺序。
volatile关键字值能通知编译器生成的输出码从内存中重新读取数据,但是不会告诉CPU在如何读取数据、在哪里读取数据。
4 操作原子化能够解决多进程访问共享内存的问题吗?
原子化操作是对于CPU而言的指令操作,它不关心线程还是进程,它只关心这一系列的指令是不可分割的。所以,进程间可以使用原子操作完成共享内存的操作同步。