GCC的内存原子化操作函数接口

1 原子化操作

在并发编程中,一个操作或一组操作是原子操作、可线性化操作、不可分操作或不可中断操作(atomic, linearizable, indivisible, uniterruptible),表示该操作执行时不可被中断的。操作的原子性能够保证操作在执行时免受中断、信号、并发进程线程的影响。另外,原子操作大多只有两种结果,要么成功并改变系统中对应的状态,要么没有相关效果。

原子化经常由互斥来保证,可以在硬件层面建立一个缓存一致性协议,也可以在软件层面使用信号量或加锁。因此,一个原子操作不是必须实际上马上生效,而操作系统让这个操作看起来是直接发生的,这能够让操作系统保持一致。正是如此,只要不影响性能,用户可以忽略较底层的实现细节。

2 函数接口

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。

取值并进行对应操作的接口 如下所示:

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

直接操作并返回结果的接口 如下所示:

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

比较并交换的函数接口

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指令的一个术语。

内存栅栏又叫内存屏障,是一种能够让CPU或编译器约束内存操作指令执行顺序的屏蔽指令。这表示在内存栅栏前的指令能够保证执行时先于内存栅栏后的指令。由于大多数现代CPU采用性能优化会导致指令执行变序时,所以内存栅栏是十分必要的。这样的指令变序对于单线程程序一般不会有很大影响,但是在并发编程情况下如果不加以控制就会导致不可预知的结果。

内存栅栏的典型应用场景就是用于实现多设备之间的共享内存的底层机器码。这些代码包括原始同步机制、多核系统上的无锁数据结构、与计算机硬件交互的设备驱动。

内存栅栏对于无锁编程来说十分重要的。

内存栅栏与volatile关键字
内存栅栏分为读栅栏(read barrier)、写栅栏(write barrier)、获取栅栏(acquire barrier)、释放栅栏(release barrier)等。内存栅栏并不能保证数值的是“最新的”或“新鲜的”,它只能控制内存访问的相对顺序。

“写栅栏”用于控制写操作的顺序。由于相对于CPU的执行速度来说,向内存中写入顺序是比较慢的,通常会有一个写入请求队列,所以实际的写入操作发生在指令发起之后,队列中指令的顺序可能会被重新排序。写栅栏能够防止指令变序。

“读栅栏”用于控制读操作的顺序。由于预先执行(CPU会提前将内存中的数据读回来),并且CPU有缓存区(CPU会从缓存中而不是内存中读取数据),读操作可能会出现变序。

volatile关键字值能通知编译器生成的输出码从内存中重新读取数据,但是不会告诉CPU在如何读取数据、在哪里读取数据。

“获取栅栏”能够保证特定指令块之前的执行顺序。例如获取读,在向读队列中加入读操作,“获取栅栏”意味着在这条操作之后可以出现指令变序,而这条操作之前不会出现指令变序。

“释放栅栏”能够保证特定指令块之后的执行顺序。例如释放写,在向写队列中加入写操作,“释放栅栏”意味着在这条写操作之前的指令不会变序到该指令之后,而这条该操作的之后的指令可能会变序到该指令之前。

获取栅栏和释放栅栏是又叫半栅栏(half barrier),这是因为它们只能防止单方向的指令变序。

4 操作原子化能够解决多进程访问共享内存的问题吗?

原子化操作是对于CPU而言的指令操作,它不关心线程还是进程,它只关心这一系列的指令是不可分割的。所以,进程间可以使用原子操作完成内存的操作同步。

Comments

Comments powered by Disqus