首先简单看下这个类的使用,使用很简单,就是定义了一种增减操作为原子操作的类型。下面是实例代码,为ACE6.0自带的examples里面的代码,实现了生产者消费者这个经典的模型。
#include "ace/Synch.h" #include "ace/Task.h" #include "ace/Log_Msg.h" #include "ace/Atomic_Op.h" #if defined(RUNNING_ON_UNSAFE_MULTIPROCESSOR) typedef ACE_Atomic_Op<ACE_Thread_Mutex, unsigned int> SafeUInt; typedef ACE_Atomic_Op<ACE_Thread_Mutex, int> SafeInt; #else typedef ACE_Atomic_Op<ACE_Null_Mutex, unsigned int> SafeUInt; typedef ACE_Atomic_Op<ACE_Null_Mutex, int> SafeInt; #endif /* RUNNING_ON_UNSAFE_MULTIPROCESSOR) */ static const unsigned int Q_SIZE = 2; static const int MAX_PROD = 10; class Producer : public ACE_Task_Base { public: Producer (int *buf, SafeUInt &in, SafeUInt &out) : buf_(buf), in_(in), out_(out) { } int svc (void) { SafeInt itemNo = 0; while (1) { // Busy wait. do { } while (in_.value () - out_.value () == Q_SIZE); itemNo++; buf_[in_.value () % Q_SIZE] = itemNo.value (); in_++; ACE_DEBUG ((LM_DEBUG, ACE_TEXT ("Produced %d\n"), itemNo.value ())); if (check_termination (itemNo.value ())) break; } return 0; } int check_termination (int item) { return (item == MAX_PROD); } private: int * buf_; SafeUInt& in_; SafeUInt& out_; }; class Consumer : public ACE_Task_Base { public: Consumer (int *buf, SafeUInt &in, SafeUInt& out) : buf_(buf), in_(in), out_(out) { } int svc (void) { while (1) { int item; // Busy wait. do { } while (in_.value () - out_.value () == 0); item = buf_[out_.value () % Q_SIZE]; out_++; ACE_DEBUG ((LM_DEBUG, ACE_TEXT ("Consumed %d\n"), item)); if (check_termination (item)) break; } return 0; } int check_termination (int item) { return (item == MAX_PROD); } private: int * buf_; SafeUInt& in_; SafeUInt& out_; }; int ACE_TMAIN (int, ACE_TCHAR *[]) { int shared_buf[Q_SIZE]; SafeUInt in = 0; SafeUInt out = 0; Producer producer (shared_buf, in, out); Consumer consumer (shared_buf, in, out); producer.activate(); consumer.activate(); producer.wait(); consumer.wait(); return 0; }
几点说明:
1.RUNNING_ON_UNSAFE_MULTIPROCESSOR为用户自定义的宏,用来选择是否使用这个Atomic的类,一般来说多线程的程序都要使用
2.ACE_Null_Mutex为一个空Mutex,一个什么也没有实现的空的Mutex,目的就是在上面这种场合里,使模板通用新更强。ACE里面的注释:
Implement a do nothing <ACE_Mutex>, i.e., all the methods are no ops.
“在大多数机器体系结构上,对基本类型的改变都是原子的,也就是说使整型变量的值增大并不需要使用同步源语,但在大多数多处理器上,事情并不一定是这样,而是取决于机器的内存次序属性。ACE_Atomic_Op这个类重载了所有基本的运算,确保在进行运算之前都会使用同步守卫。”这是《ACE程序员指南-网络与系统编程的实用设计模式》上的话,有以下几点值得探讨下。
因为在运算的时候都使用了同步守卫,而操作系统可能会提供底层的直接把操作转成原子操作的接口,一般锁机制比原子操作函数需要耗费更多资源。例如windows平台上的接口有两个API函数来实现,对变量的加1,减1:
LONG WINAPI InterlockedIncrement (__inout LONG volatile *lpAddend );
LONG WINAPI InterlockedDecrement (__inout LONG volatile *lpAddend );
在Linux平台上提供了对于基本类型和位的原子操作接口:
atomic_t use_cnt; //重定义的类型
atomic_set(&use_cnt, 2);
atomic_add(4, &use_cnt);
atomic_inc(use_cnt);
位操作函数接口:
unsigned long word = 0;
set_bit(0, &word); /*第0位被设置*/
set_bit(1, &word); /*第1位被设置*/
clear_bit(1, &word); /*第1位被清空*/
change_bit(0, &word); /*翻转第0位*/
此外不光是基本类型操作还有一些函数接口也是被规定成原子操作的(待补充)。
有两个因素,一是是否多核,二是是否多线程。在写多CUP或者多线程的程序的时候就要注意这个问题了。
一是编译器,二是是否多CUP,还有平台是32或者64位时候对这个也有影响。编译器的不同,对于i++或者++i这种操作可能翻译成汇编的语句可能不一样,一般来说i++汇编成三步:从内存读入寄存器,寄存器值加一,从寄存器写回内存,这个即使在单CUP上多线程也不是原子操作。但是对于++i,有的编译器是三步,有的编译器可能一步,完全取决于编译器,如果要写跨平台的开发,那么最好加锁,或者使用这个ACE_Atomic_Op模板类。
下面是我的VC6.0上i++和++i对应的汇编,都是一样的,分了三步。
: i++;
0040102F mov eax,dword ptr [ebp-4]
00401032 add eax,1
00401035 mov dword ptr [ebp-4],eax
: ++i;
0040102F mov eax,dword ptr [ebp-4]
00401032 add eax,1
00401035 mov dword ptr [ebp-4],eax
可以看出,多线程下不加锁,都可能造成不安全。
GNU C中x++也不是原子操作
x++由3条指令完成。x++在单CPU下不是原子操作。
对应3条汇编指令
movl x, %eax
addl $1, %eax
movl %eax, x
volatile 关键字确实与原子操作有预定关联,但他们之间的关系并不像很多人想象的那么单纯:
(1).C/C++ 中的 volatile 关键字提供了以下保证:
a.对声明为 volatile 的变量进行的任何操作都不会被优化器去除,即使它看起来没有意义(例如:连续多次对某个变量赋相同的值),因为它可能被某个在编译时未知的外部设备或线程访问。
b.被声明为 volatile 的变量不会被编译器优化到寄存器中,每次读写操作都保证在内存(详见下文)中完成。
c.在不同表达式内的多个 volatile 变量间的操作顺序不会被优化器调换(即:编译器保证多个 volatile 变量在 sequence point 之间的访问顺序不会被优化和调整)。
(2)volatile *不* 提供如下保证
1)volatile 声明不保证读写和运算操作的原子性。
2)volatile 声明不保证对其进行的读写操作直接发生在主内存。相反,CPU 会尽可能让这些读写操作发生在 L1/L2 等 cache 上。除非:a.发生了一个未命中的读请求。
b.所有级别的 cache 均已被配置为通过式写(write through)。
c.目标地址为 non-cacheable 区(主要是其它设备映射到内存地址空间的通信接口。例如:网卡的板载缓冲区、显卡板载显存、WatchDog 寄存器等等)。3)编译器仅保证在生成目标码时不调整 volatile 变量的访问顺序,但通常并不保证该变量不受处理器的 out-of-order 特性影响。目前唯一一个已知的特例是安腾(IA64)处理器版的 VC:在生成 IA64 Target 时,VC 会自动在所有 volatile 访问之间添加内存屏障(详见下文)以保证访问顺序。但 ISO 标准并未要求编译器实现类似机制。实际上,其它编译器(或是面向其它平台的 VC)也都没有类似保证。也就是说,通常认为 volatile 并不保证代码在处理器上的执行顺序,如果需要类似的保证,程序员应当自己使用内存屏障操作。
(3)而原子操作要求做到以下保证:
对原子量的 '读出-计算-写入' 操作序列是原子的,在以上动作完成之前,任何其它处理器和线程均无法访问该原子量。
原子操作必须保证缓存一致性。即:在多处理器环境中,原子操作要同时保持所有处理器上的各级 cache 之间、以及它们与主存间的访问一致性。
可见,使用 volitale 关键字并不足以保证操作的原子语义。volitale 关键字的主要设计目的是支持 C/C++ 程序与内存映射设备间的通信。但这并不是说 volitale 关键字对原子操作没有任何帮助:
参考:
《ACE程序员指南-网络与系统编程的实用设计模式》
http://software.intel.com/zh-cn/blogs/2010/01/14/cpucpu
http://baiy.cn/doc/cpp/advanced_topic_about_multicore_and_threading.htm