天天看点

原子性和线程安全

原子性和线程安全 (xingchongatgmaildotcom)

指令原子性

在单核时代,每条指令的执行都是原子的,即指令执行的中间状态是外部不可见的。这是cpu必须要保证的。从逻辑上来说,cpu只需要保证中断/异常触发的时候,当时要执行的指令要么完成,要么看起来和没开始一样。这叫做状态一致 (state consistent). 保证了状态一致性也就保证了原子性。

在多核时代,情况就不一样了,一条指令的执行就不一定是原子的。比如x86上的inc 指令可以将一个memory值加1;           inc [mem];   // 对mem进行 +1 操作; 我们可以把这条指令的执行理解成三步:                 1. load [mem] -> reg;                2. reg = reg+1;                3. store reg -> [mem];

假设一个多线程程序,两个线程分别运行在CPU0和CPU1上,都去对同一个内存执行inc操作,可能发生下面的情况:

CPU0 CPU1
Begin: [mem] = 0
1    load [mem] -> reg   // reg = 0
2   load [mem] -> reg   // (reg = 0)
3    reg = reg + 1          // reg = 1    reg = reg + 1          // reg = 1
4    store reg -> [mem] // [mem] = 1
5   store reg -> [mem] // [mem] = 1
6 End: [mem] = 1

上表中++ 操作在两个CPU上交替进行,+1 操作在两个CPU上执行,但结果确是 +1; 而非+2。这就是由于指令执行的非原子性造成的。 为了解决这个问题呢, x86 cpu引入了支持多核的原子操作:lock前缀。           lock inc [mem]; 加了lock之后了,在指令执行中间其他cpu核就不能访问那块内存。 下表显示了两个CPU分别执行 lock inc [mem] 的过程

CPU0 CPU1
Begin: [mem] = 0
1    load [mem] -> reg   // reg = 0
2    reg = reg + 1          // reg = 1
3    store reg -> [mem] // [mem] = 1
4   load [mem] -> reg   // reg = 1
5   reg = reg + 1          // reg = 2
6   store reg -> [mem] // [mem] = 2
7 End: [mem] = 2

原子性与多核:

inc  [mem] lock inc [mem]
单核 原子 原子
多核 非原子 原子

如果你的多线程程序要支持在多核上运行;lock前缀是必要的;

gvar++线程安全吗? 

gvar 是一个全局变量,gvar++线程安全吗?多核上?单核上? 首先我们要看编译器是怎么编译gvar++,大体来说有两种情况: 第一种:编译成,inc [gvar]; inc指令可以直接对memory操作; 第二种:先把memory值加载到寄存器,寄存器+1,然后写回内存。       load reg = [gvar];       reg = reg + 1;       store [gvar] = reg; 前面讨论过,对于inc [gvar],CPU内部实现也是三步操作:load,add,store; 但是从原子性的角度考虑是有差别的,第一种情况只有一条指令,在单核上是原子的;第二种情况有三条指令,即使在单核上也是非原子的。

试想如果线程切换发生在load/store中间,会发生什么呢?看下表的执行流程 (单CPU)

线程0 线程1
Begin: gvar [mem] = 0
1    load [mem] -> reg   // reg = 0
2    调度发生,切换到线程1
3   load [mem] -> reg   // reg = 0
4   reg = reg + 1          // reg = 1
5   store reg -> [mem] // [mem] = 1
6  调度发生,切换到线程0
7    reg = reg + 1          // reg = 1
8    store reg -> [mem] // [mem] = 1
9 End: gvar [mem] = 1

结论是: gvar ++ 多核上必然非原子的             单核上就看编译出来的代码了

程序怎么写来保证gvar++原子性

三种方法: 1.  通过另外的锁;(这不是本文要介绍的内容;) 2.  写汇编(内嵌汇编)来使用"lock"前缀; 3. 使用编译器builtin函数。编译器实现了很多内置函数,包括这些院子操作,供程序调用。具体参看 ( http://gcc.gnu.org/onlinedocs/gcc-4.4.2/gcc/Atomic-Builtins.html)。对于inc操作,可以调用__sync_fetch_and_add; 看如下的示例代码: $ cat main.c

int func(int i)

{

    return __sync_fetch_and_add(&i, 1);

}

$ gcc main.c -o main -c -O2 $ objdump -d main 00000000 <func>:

   0:   55                      push   %ebp

   1:   b8 01 00 00 00    mov    $0x1,%eax

   6:   89 e5                  mov    %esp,%ebp

   8:   f0 0f c1 45 08      lock xadd %eax,0x8(%ebp)

   d:   5d                      pop    %ebp

   e:   c3                      ret

__sync_fetch_and_add函数被inline进了调用函数,也是通过lock前缀来实现的:lock xadd %eax,0x8(%ebp)

推荐使用编译器builtin函数,自己写汇编的话容易出错;

Affinity: 如果你不想用原子操作,但是也想保证多线程程序正确性,还有一招:affinity ( http://www.kernel.org/doc/man-pages/online/pages/man2/sched_setaffinity.2.html) 通过使用affinity, 可以让某一个程序或者线程邦定在某个CPU上,这样呢就不用担心因为多核而产生的线程安全问题;

继续阅读