并发编程概述:内存屏障、volatile、原子变量和互斥锁

本文论述基于常规概念,不面向具体平台架构。

并发编程三条特性:

  • 原子性
    原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败。

  • 可见性
    可见性是指当一个线程修改了共享变量后,其他线程能够立即看见这个修改。

  • 有序性
    有序性是指程序指令按照预期的顺序执行而非乱序执行,乱序又分为编译器乱序和CPU执行乱序。

系统架构

首先展示一幅手绘图,概略描述单核和多核时的系统架构:

architecture

Register

寄存器。CPU指令执行,寄存器存放指令和数据,线程切换会进行寄存器保存和恢复操作。对线程而言,寄存器可以认为是私有的,即线程“工作内存”。多线程下,指令并发执行,操作各自工作内存,产生了原子性问题。

Cache

CPU缓存。CPU缓存提升了数据加载速度的同时,也带来了缓存一致性问题,该问题通过MESI协议来解决。但MESI协议下,一个CPU可能需要等待另一个CPU响应后才能继续执行,导致了阻塞,影响性能。所以增加了StoreBuffer和InvalidateQueue,也就是需要store时先放到StoreBuffer里,然后继续执行下一条指令,等到其他CPU响应返回后再处理对应store;收到invalidate通知时也不立即处理,而是先放到InvalidateQueue,并立即给予对方响应,然后等到合适时机再一起处理。这种优化提升了CPU执行能力,但也使得MESI协议的操作无法立即得到处理,出现异步,产生了可见性和有序性问题。

Memory

内存。

单核环境下,多线程并发存在原子性问题和编译器乱序问题,因为Cache使用同一份,不存在可见性和CPU乱序问题。

多核环境下,多线程并发存在原子性、可见性和有序性问题。

内存屏障

针对上述情况,编译屏障可以解决编译乱序问题,内存屏障可以解决可见性和CPU乱序问题。

编译屏障作用于编译期,阻止编译器因为优化等对代码进行重排序。

内存屏障作用于指令执行期,又分为读屏障和写屏障。

读屏障

针对InvalidateQueue,确保其被处理。

LoadLoad屏障:对于这样的指令序列load1; loadload; load2,load1一定在load2之前执行完,CPU不会对load1和load2进行重排序。

LoadStore屏障:对于这样的指令序列load1; loadstore; store1,load1一定在store1之前执行完,CPU不会对load1和store1进行重排序。

写屏障

针对StoreBuffer,确保其写入缓存。

StoreStore屏障:对于这样的指令序列store1; storestore; store2,store1一定在store2之前执行完,store1的写入操作对其他CPU可见,CPU不会对store1和store2进行重排序。

StoreLoad屏障:对于这样的指令序列store1; storeload; load1,store1一定在load1之前执行完,store1的写入操作对其他CPU可见,CPU不会对store1和load1进行重排序。

在内存屏障实现上,一般在单核上,即为编译器屏障,在多核上,包括编译屏障和读写屏障。

valatile、原子变量和锁

明白了以上知识,相关概念就很好理解了。

volatile

volatile读:在该操作前添加LoadLoad屏障,在该操作后添加LoadStore屏障。

volatile写:在该操作前添加StoreStore屏障,在该操作后添加StoreLoad屏障。

所以volatile可以具有可见性和有序性,但不具有原子性

原子变量

原子变量在实现上,可以采用CAS操作,所以具有原子性。

互斥锁

互斥锁在实现上,保证了一个同步块同一时间只能有一个线程进入,所以具有原子性,其又包含内存屏障语义,所以具有可见性和有序性。