1:i++
对于i++这样的操作,其实是分3步执行的,读取i的值,增加i的值,回写i的新值。这3步每一步都是原子操作,但是组合在一起就不一定是原子操作了。可以参看下图中两个线程对i的争抢示例。我们期待线程A与B中的i++可以顺序执行,最终存储器中的结果是2;但是实际上由于两个线程并行执行,结果可能得到1(当然也有可能得到2,运气好的话)。通常i++这样的操作我们称为“读-改-写”操作。
1.1:Lock指令
由于的确有业务逻辑要求多线程中类似于i++这样的“读-改-写”操作是原子性操作。处理器因此为软件(即程序员/编译器)提供了一种原生的总线封锁机制,即lock指令前缀。当生成的代码前有lock前缀时,例如伪代码lock i++,则整条指令在访存/修改变量/回写期间,别的设备/代码都不可以触碰被锁住的变量。从上面的例子来看就是,一旦线程A进入load i阶段,线程B就不可以再执行load i。线程B会阻塞,一直等待到线程A的store i完成,才能继续往下执行load i(此时读取到的是线程A的结果i=1),i++, store i,最后将i=2写回到内存中。
有了lock指令前缀,程序的正确性问题解决了。但是同时也带了性能问题,其中一个线程不得不等待,直到另一个线程释放“锁”。
出现需要加锁的情况的本质原因是数据变量在多线程之间被共享且发生修改。所以改进程序算法,尽量避免数据共享才是终极解决办法。但是有时候业务逻辑不得不要求数据共享从而必须加锁。程序的正确性是刚需,性能与锁不可兼得。
注:lock前缀在没有高速缓存的机器上让处理器封锁总线,有高速缓存的机器上做cache lock操作,总体目标是保证指令原子性执行。
1.2:atomic
atomic对int、char、bool等数据结构进行了原子性封装,在多线程环境中,对std::atomic对象的访问不会造成竞争-冒险。利用std::atomic可实现数据结构的无锁设计。
所谓的原子操作,取的就是“原子是最小的、不可分割的最小个体”的意义,它表示在多个线程访问同一个全局资源的时候,能够确保所有其他的线程都不在同一时间内访问相同的资源。也就是他确保了在同一时刻只有唯一的线程对这个资源进行访问。这有点类似互斥对象对共享资源的访问的保护,但是原子操作更加接近底层,因而效率更高。
在以往的C++标准中并没有对原子操作进行规定,我们往往是使用汇编语言,或者是借助第三方的线程库,例如intel的pthread来实现。在新标准C++11,引入了原子操作的概念,并通过这个新的头文件提供了多种原子操作数据类型,例如,atomic_bool,atomic_int等等,如果我们在多个线程中对这些类型的共享资源进行操作,编译器将保证这些操作都是原子性的,也就是说,确保任意时刻只有一个线程对这个资源进行访问,编译器将保证,多个线程访问这个共享资源的正确性。从而避免了锁的使用,提高了效率。
关于automic的高效提现可参考C++11之atomic原子操作。
1.3:GCC的内嵌原子操作函数
详细内容可参考__sync_*系列原子操作函数及相关memory_order等6种。
1.4:volatile
volatile并不保证原子性,只保证可见性。对于被该关键字修饰的变量,每次读取时都去寄存器里读,而不是缓存。
参考: