原子操作

定义

	所谓原子操作,就是“不可中断的一个或一系列操作”
  • 单核CPU:

  在单核CPU中,能够在一条指令中完成的操作都可以看作为原子操作。无论是设备中断请求、时间片超时、程序主动schedule放弃CPU,抑或是发生抢占,从而引起线程调度中断当前程序操作,都是发生在指令间的。计算机的每条指令的执行天生就是原子性不可中断的,这是由硬件设计决定的。
  对程序而言,一条语句对应几条指令,是由编译器、指令集(或者说硬件架构设计)决定的,即是由软件硬件共同决定。编译器决定了上层语言被翻译成什么指令,而指令决定了操作是否是原子性的。如,对CISC来说,是能够直接操作内存数据的,可以由一条指令完成,这就是原子操作;而在RICS中,数据需要先load到寄存器进行操作,再store回内存,无法在一条指令中完成,这就不是原子操作。
  当指令集提供了一条指令能完成读写的操作时,该读写操作就成为原子操作。而指令依赖硬件通路,最终原子操作将由硬件保证。

  • 多核CPU

      在单核CPU中,内存的访问总是只由一个核心来进行的。当存在多个核心时,每个核心上执行的程序同时执行一条指令能完成的操作,同时对同一地址的内存进行访问,这时得到的结果可能不是预期的,此时依然不能说它们是原子操作。
      回顾之前的定义:原子操作是“不可中断的一个或一系列操作”。这里的“中断”并不翻译成操作被中断停止执行,更确切地应该指在执行的整个过程中,对所用到的资源是独占的。
      多个核心之间产生资源竞争,这就违背了原子操作的定义。想要操作依然是原子操作,就需要对资源进行“锁定”。这便依赖于硬件的功能。
      如在x86体系中,CPU提供了HLOCK pin引线,允许CPU在执行某一个指令(仅仅是一个指令)时拉低HLOCK pin引线的电位,直到这个指令执行完毕才放开,从而锁住了总线。如此在同一总线的CPU就暂时无法通过总线访问内存了,这样就保证了多核处理器的原子性。同时可以看到,这种机制是牺牲了性能的。

实现

  • 硬件实现

      处理器实现原子操作,或者说资源锁定,主要有两种手段。一是总线加锁,二是缓存锁定。
    1)总线加锁:所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。
      缺点:总线缓存阻止了被阻塞处理器和所有内存之间的通信,而输出LOCK#信号的CPU可能只需要锁住特定的一块内存区域,因此总线锁定开销较大。

    2)缓存锁定:缓存锁定是某个CPU对缓存数据进行更改时,会通知缓存了该数据的该数据的CPU抛弃缓存的数据或者从内存重新读取。
      缺点:操作的数据不能被缓存在处理器内部,或者操作的数据跨多个缓存行,或处理器不支持缓存锁定时,无法使用缓存锁定。

    MESI缓存一致性协议

  • 软件实现

     CAS(compare and swap):比较并交换。
     	cas(var, val_1, val_2)
     	----变量var的值与期望值val_1比较,如果等于val_1就将var和val_2的值交换。
     	----指令集提供了CAS指令,如x86的CMPXCHG(加上LOCK prefix),
     		能够完成atomic的compare-and-swap(CAS)。借助硬件实现机制(总线锁、缓存锁),
     		保证了多核CPU下指令的原子操作性。
     	
     操作系统中mutex的实现:
     	借助CAS指令,对内存的一块地址空间写值。0代表没上锁状态,1代表已上锁状态。
     	当初始值为0(没上锁状态)时,cas(var, 0, 1)成功改写值为1,上锁成功。
     	此时另一个线程使用cas(var, 0, 1)获取锁必然失败。
     	这时,让没有抢到锁的线程不断在while里面循环进行compare-and-swap,直到前面的线程放手(对应的内存被赋值0),
     	就是spin lock。
     	如果需要长时间的等待,这样反复CAS轮询就比较浪费资源,
     	这个时候程序可以向操作系统申请被挂起,然后持锁的线程解锁了以后再通知它。
     		
     程序中直接使用CAS:
     	gcc下__sync_val_compare_and_swap函数,提供了对CAS指令封装的接口,供上层语言调用。
     	类似的如__sync_fetch_and_add,__sync_lock_test_and_set。
     	C++提供了atomic<T>模板类型,及六种原子序,对原子操作进行进一步的跨平台抽象封装
    
C++11的六种原子序:

typedef enum memory_order {
    memory_order_relaxed, 
    memory_order_consume, 
    memory_order_acquire, 
    memory_order_release,
    memory_order_acq_rel,
    memory_order_seq_cst 
} memory_order;

如何理解 C++11 的六种 memory order

应用

  • 无锁队列
EnQueue(x) //进队列
{
    //准备新加入的结点数据
    q = new record();
    q->value = x;
    q->next = NULL;
 
    do {
        p = tail; //取链表尾指针的快照
    } while( CAS(p->next, NULL, q) != TRUE); //如果没有把结点链在尾指针上,再试
 
    CAS(tail, p, q); //置尾结点
}

问题

  • ABA问题

     1)进程P1在共享变量中读到值为A
     2)P1被抢占了,进程P2执行
     3)P2把共享变量里的值从A改成了B,再改回到A,此时被P1抢占。
     4)P1回来看到共享变量里的值没有被改变,于是继续执行。
    
  • 解决

      	使用double-CAS(双保险的CAS),例如,在32位系统上,我们要检查64位的内容
     	1)一次用CAS检查双倍长度的值,前半部是目标数据:指针,后半部分是一个计数器。
     	2)只有这两个都一样,才算通过检查,要把指针赋新的值。并把计数器累加1。
    

总结

  在单核处理器中,单条指令操作本身就是原子性的,单条指令能完成的操作即为原子操作。对于需要多条指令完成的操作,当关闭中断禁止调度时,所有操作都为原子操作。
  对于多核CPU而言,原子操作的保证需要依赖硬件机制,主要有总线锁和缓存锁。在此基础上,指令集提供如CAS的指令,执行上锁操作,再访问数据,解决访问冲突,使操作原子化。
  借助这种指令,通过while循环可以设计自旋锁,或可睡眠锁等,达到锁住临界区的目的。
  操作系统层面提供了锁的封装,同样,编译器也提供了相应的API封装。如gcc的__sync_val_compare_and_swap,__sync_fetch_and_add,__sync_lock_test_and_set。
  而C++11标准中,抽象出六种原子序,为原子变量和原子操作提供了跨平台实现方案。

  • 6
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值