原子操作在多核编程中的使用

时间:2022-10-12 00:55:20

 

现代操作系统中,一般都提供了原子操作来实现一些同步操作,所谓原子操作,也就是一个独立而不可分割的操作。在单核环境中,一般的意义下原子操作中线程不会被切换,线程切换要么在原子操作之前,要么在原子操作完成之后。更广泛的意义下原子操作是指一系列必须整体完成的操作步骤,如果任何一步操作没有完成,那么所有完成的步骤都必须回滚,这样就可以保证要么所有操作步骤都未完成,要么所有操作步骤都被完成。

例如在单核系统里,单个的机器指令可以看成是原子操作(如果有编译器优化、乱序执行等情况除外);在多核系统中,单个的机器指令就不是原子操作,因为多核系统里是多指令流并行运行的,一个核在执行一个指令时,其他核同时执行的指令有可能操作同一块内存区域,从而出现数据竞争现象。多核系统中的原子操作通常使用内存栅障(memory barrier)来实现,即一个CPU核在执行原子操作时,其他CPU核必须停止对内存操作或者不对指定的内存进行操作,这样才能避免数据竞争问题。

Win32 API中常用的原子操作主要有三类,一种是加11操作,一种是比较交换操作,另外一种是赋值(写)操作。

1.        原子加11操作

原子加减操作主要是对指定的变量进行加1或减1操作,原子加1操作API如下:

LONG InterlockedIncrement( LONG volatile* Addend);

执行原子加1操作,Addend的值会被加1,返回值为原始Addend1后的值。需要注意的是,当操作结束后,如果再读取Addend的值不一定等于原始Addend1后的值,因为这时有可能其他线程会修改Addend的值。

原子减1操作的API如下:

LONG InterlockedDecrement( LONG volatile* Addend);

执行原子减1操作,Addend的值会被减1,返回值为原始Addend1后的值。和原子加操作一样,当操作结束后,如果再读取Addend的值不一定等于原始Addend1后的值,因为这时有可能其他线程会修改Addend的值。

因此,在使用这类加1和减1的原子操作时,如果要取加1或减1后的值,一定要取返回值,不能去取原始变量的值。在《多核程序设计技术-通过软件多线程提升性能》一书的p190191页中,也讲解了如何安全使用原子操作进行计数的例子,基本思想仍然是取原子操作的返回值。

2.        比较并交换操作(Compare and Swap, CAS

比较并交换操作的作用是在一个原子操作内,先将目标值和老的值进行比较,如果相等就将新的值赋给目标变量。目的是为了防止将新的值赋给目标变量前,在读写操作之间目标变量遭到其他线程的修改。

CAS内部操作过程如下面的伪代码所示:

BOOL  CAS(LONG *target, LONG new, LONG old) {

atomically { //以下操作为一个整体,是不可分割的

if (*target == old) {

*target = new;

return true;

}

else {

return false;

}

}

}

注意上面伪代码只是演示CAS的操作过程,实际上整个操作的实现都是不可分割的。

在微软API里,CAS原子操作对应的是InterlockedCompareExchange()和如果是交换指针,那么对应的操作是InterlockedCompareExchangePointer()

InterlockedCompareExchange()带有全局的内存栅障,不带全局内存栅障的对应操作为InterlockedCompareExchangeAcquire()或者InterlockedCompareExchangeRelease()

InterlockedCompareExchange()函数的原型如下:

LONG InterlockedCompareExchange( LONG volatile*Destination, LONG Exchange, LONG Comperand );

这个操作是先将Comperand(相当于前面CASold参数)的值和Destination(相当于前面CAStarget参数)指向变量的值进行比较,如果相等就将Exchange相当于前面CASnew参数)变量的值赋给Destination指向的变量。返回值为未修改前的Destination位置的初始值。

下面用AtomicCAS()来表示CAS操作,AtomicCAS的源代码实现如下:

BOOL AtomicCAS(LONG volatile *dest, LONG newvalue, LONG oldvalue)

{

       LONG    ret;

       ret = InterlockedCompareExchange(dest, newvalue, oldvalue);

       if ( ret == oldvalue )

{

              return TRUE;

       }

       else

{

       return FALSE;

}

}

 

CAS操作是一个最基本的原子操作,其他原子操作其实都可以由这个原子操作通过简单的算法来实现。比如前面讲过的原子加1操作就可以用如下代码实现:

LONG AtomicIncrement(LONG volatile *Target)

{

       LONG     Old;

 

       do {

              Old = *Target;

 

       }while (!AtomicCAS(Target, Old + 1, Old));

 

       return Old;

}

上面这段代码使用了一个循环来进行操作,因为每次执行完“Old = *Target;”这条语句后,有可能线程刚好发生切换,其他线程有可能修改掉Target的值,如果此时还将Old+1的值赋给Target的话,那么将发生错误,此时AtomicCAS操作将会失败,这时需要回滚(即重新循环)进行操作,直到AtomicCAS操作成功为止。

从上面的用CAS实现AtomicIncrement代码也可以看出CAS的作用就是防止对共享变量两次操作之间进行线程切换,所以需要在一个原子操作内先将目标值和老的值进行比较,如果和老的值相等,表明没有其他线程修改过目标变量的值,此时将目标的值修改掉就安全了

 

 

 

3.        原子写操作

原子写操作的作用是对共享变量进行写操作,在微软的API中,原子写操作的对应函数为:

LONG InterlockedExchange( LONG volatile* Target, LONG Value);

InterlockedExchange的作用为将Value的值赋给Target指向的变量,返回Target指向变量未被赋值前的值。

下面用AtomicWrite()来表示原子赋值操作,

#define AtomicWrite(x, y)     InterlockedExchange(x, y)

原子写操作也可以用前面的CAS操作来实现,代码如下:

LONG AtomicWrite(LONG volatile *Target, LONG Value)

{

       LONG     Old;

 

       do {

              Old = *Target;

 

       }while (!AtomicCAS(Target, Value, Old));

 

       return Old;

}

 上面讲过的这些原子操作中,CAS原子操作属于比较复杂的原子操作,使用CAS原子操作编程又叫无锁编程。无锁编程的难度非常高,非专业人士请勿尝试。

原子加11操作是一类非常实用的原子操作,也是程序员比较容易掌握的原子操作,比如下面两篇文章中就使用了原子加11操作。

 

 

摘自:http://software.intel.com/zh-cn/blogs/2009/03/31/400001287/