原子性和线程安全

原子性和线程安全 (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操作,可能发生下面的情况:
 CPU0CPU1
0
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] 的过程

 CPU0CPU1
0
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
0
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上,这样呢就不用担心因为多核而产生的线程安全问题;


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值