原文链接:atomic-vs-non-atomic-operations

在网上已经写了很多关于原子操作的文章,但是通常都集中在原子的读-修改-写(RMW. read-modify-write)操作。但是这些并是所有的原子操作。同样重要的属于原子操作的还是有load(译注:读)和store(译注:写)。在这篇文章中,我将会在处理器层面和C/C++语言层面,比较原子性和非原子性的load和store。顺便,我们将会阐明以下在C++11中的“数据竞争”概念。

如果一个共享变量的操作,它能相对于其他线程,能够一步完成,那么这个操作就是原子性的操作。当对一个共享变量执行原子性的store操作,其他线程只能观察到它已经修改完后的数据。当对一个共享变量执行原子性的load操作,它会读取单一时刻所显示的完整的值。非原子性的store和load不会有上述的保证。

离开上述的保证,无锁编程(lock-free programming)将变得不可能,因为不能在相同时间,让多个线程操作同一个共享变量。我们可以将此明确表达为一个规则:

任何时间,两个线程并发地操作在一个共享变量上,这些操作中的一个执行一个写动作,所有的线程都必须使用原子操作。

如果你违反这个规则,其中有个线程使用了非原子操作,那么你将会陷入一个在C++11标准中称之为数据竞争(不要和Java中的data race概念,以及更通用的race condition搞混淆)的情形。C++11标准没有告诉编程人员为什么数据竞争是不好的。但是如果你引发了数据竞争,那么就会得到一个"未定义行为(undefined behavior)"的结果。数据竞争是不好的真正理由只有一个:它们会导致“撕裂读”(torn reads)和“撕裂写”(torn writes。译注:就是一个非完整的读写)。

一个内存操作可能是非原子的,因为它使用了多条CPU指令,甚至即使使用单条CPU指令,也可能是非原子的。也可能是因为程序员写的可移植代码。但是不能简单地做出这个假设。让我们看几个例子。

由于多条CPU指令的非原子操作

假设有一个64位的全局变量,初始化为0.

  1 uint64_t sharedValue = 0;

此时,将一个64位的值更新到此变量:

  1 void storeValue()
2 {
3 sharedValue = 0x100000002;
4 }

在32位的 x86平台上,使用GCC编译此函数,会产生以下汇编代码:

  1 $ gcc -O2 -S -masm=intel test.c
2 $ cat test.s
3 ...
4 mov DWORD PTR sharedValue, 2
5 mov DWORD PTR sharedValue+4, 1
6 ret
7 ...

如你所见,编译器实现一个64位整形的赋值是通过两个单独的机器指令。第一条指令将低32位设置为0x00000002,第二条指令将高32位设置为0x00000001。很明显,这个赋值操作不是原子操作。如果sharedValue被不同线程并发访问,将会出错。

1. 如果一个线程在两条指令之间对sharedValue的访问时独占的,那么在内存中,sharedValue将会被设置为0x0x0000000000000002,
   一个“撕裂写(a torn write)”。此时,如果另外一个线程读取sharedValue的值,那么将会读到一个完全虚假的值。
2. 更遭的是,如果一个线程在两条指令之间进行独占访问,此时另一个在第一个线程恢复前修改变量sharedValue,会
   导致一个永久性的“撕裂写(torn write)”:高32位来源于一个线程,低32位来源于另一个线程。
3. 在多核设备中,线程都没必要进行一个会导致“撕裂写”的资源抢占。因为当一个线程调用sharedValue,在不同核心上的
   任意线程在某个时刻都可能会去读sharedValue,此时的sharedValue可能处于修改的一半当中。

并发地从sharedValue读也会带来一些问题:

  1 uint64_t loadValue()
2 {
3 return sharedValue;
4 }
5
6 $ gcc -O2 -S -masm=intel test.c
7 $ cat test.s
8 ...
9 mov eax, DWORD PTR sharedValue
10 mov edx, DWORD PTR sharedValue+4
11 ret
12 ...
13

同样,编译器用两条机器指令实现读取操作:第一条指令读取低32位的值到eax寄存器,然后第二条指令读取高32位的值到edx寄存器。在这种情况下,并发地发生一个写的操作,此时会产生一个“撕裂读(torn read)”。即使这个并发的写是原子操作。

这些问题并不只是存在于理论上。Mintomic的测试套件中包含了一个叫test_load_store_64_fail的测试用例。一个线程使用普通的赋值操作符更新一个64位变量的值,另一个线程周期性地执行一个从相同变量的读取操作,对每次读取回来的结果进行校验。在x86多核机器上,和预期一样,此测试会经常失败。

非原子性的CPU指令

即使执行单条CPU指令,一个内存操作也可能是非原子性的。例如:在ARMv7指令集中,包含了一个strd指令,实现将两个32位的寄存器的值存储到一个64位的变量中。

  1 strd r0, r1, [r2]

在一些ARMv7处理器中,这条指令时非原子性的。当处理器碰到这条指令时,实际上是执行2条32位的单独存储动作。再一次,任何运行在其他核心的线程都可能会观察到一个“撕裂读(torn write)”。有意思的是,“撕裂读(torn write)”甚至可能会发生在单核设备中:因为系统中断。在2条32位存储指令中间,可能会发生线程上下文的调度切换。这种情况下,当线程从中断中恢复后,将会重新执行一次strd指令。

另外一个例子,是发生在大家熟知的x86平台上。一个32位的mov指令只有在内存操作数是自然对齐的情况下才是原子性的!其他情况下是非原子性的。换句话说,一个32位的整形,只有它的内存地址是4的整数倍情况下,原子性才能有保证。Mintomic有另一个测试用例test_load_store_32_fail,可以验证此种情况。在写本文的时候(译注:2013年6月),这个测试用例在x86平台上总是成功的。但是如果你将测试变量sharedInt的地址强制修改为非对齐的内存地址,那么测试结果将会失败。在我的Core 2 Quad Q6600机器上,如果sharedInt是跨越了单条缓存行界限(crosses a cache line boundary),那么测试就会失败。

  1 // Force sharedInt to cross a cache line boundary:
2 #pragma pack(2)
3 MINT_DECL_ALIGNED(static struct, 64)
4 {
5 char padding[62];
6 mint_atomic32_t sharedInt;
7 }
8 g_wrapper;

对于特定处理的情况已经说的够多了,接下来看看在C/C++语言层面的原子性。

所有的C/C++操作都假设是非原子性的

在C和C++中,每一个操作都被假定为非原子性的,即使是普通的32位整形赋值。除非编译器或硬件厂商有特殊说明。

  1 uint32_t foo = 0;
2
3 void storeFoo()
4 {
5 foo = 0x80286;
6 }
7

语言标准中没有提及关于以上情况的原子性。也许整形赋值是原子性的,也许不是。因为非原子性的操作不做任何保证,所以在C中定义普通的整形赋值时非原子性的。

在实际中,我们通常更了解我们的目标平台。例如:在所有的现代x86,x64,Itanium,SPARC,ARM和PowerPC处理器中,普通的32位整形,只要内存地址是对齐的,那么赋值操作就是原子操作。你可以通过查看处理器手册或者编译器文档来证实。在游戏产业,很多32位的赋值时依赖于这个特别的保证。

尽管如此,当写真正的可移植的C和C++代码时,有一个长期的伪装的传统就是,我们只知道语言标准中所记录的,除此之外,一概不知。可移植的C/C++代码是要运行在每台可能的设备上,过去的设备,现在的设备以及想象中的设备。从我个人来说,我喜欢想象有台机器,只能被一开始的混乱所改变。

在这样的机器上,你绝对不会想在同一时间执行并发的读操作,即使是普通的赋值。你可能最终只会读到一个完全随机的值。
      在C++11中,有一种方式可以真正执行可移植的load原子操作和store原子操作:C++11 atomic库。使用C++11 atomic库,即使是运行在想象的机器上,也可以执行原子性的load和store。即使在C++11 atomic库的内部秘密地使用互斥锁使每个操作变得原子性。同样还有一个我上个月发布的叫Mintomic的库(译注:2013年6月,此库目前已废。)。虽然支持的平台可能不多,但是在几个老的编译器上还是可以正常工作的,它是手工优化的并且保证是无锁的。

不严格的(Relaxed)原子操作

让我们回到原来的sharedValue例子。我们将会使用Mintomic对其进行重写。这样在Mintomic支持的平台上,所有的操作都是原子性的了。首先,必须将sharedValue声明为Mintomic的原子数据类型的一种。

  1 #include <mintomic/mintomic.h>
2
3 mint_atomic64_t sharedValue = { 0 };
4

mint_atomic64_t类型在不同的平台上,保证原子访问都有正确的内存对齐。这很重要。因为在一些平台的编译器中并不做出类似的保证。比如ARM上的和Xcode 3.2.5绑定的GCC4.2版,就不保证普通的uint64_t是8字节对齐的。

在修改sharedValue时,不再调用普通的、非原子的赋值操作,而是调用mint_store_64_relaxed

  1 void storeValue()
2 {
3 mint_store_64_relaxed(&sharedValue, 0x100000002);
4 }

同样的,在读取sharedValue变量的值时,我们使用mint_load_64_relaxed

  1 uint64_t loadValue()
2 {
3 return mint_load_64_relaxed(&sharedValue);
4 }

使用C++11的术语来说,上述方法是无数据竞争(data race-free)的。在执行并发操作时,绝对不可能存在“撕裂读”或“撕裂写”。不管是运行在ARMv6/ARMv7,x86,x64或PowerPC。

下面是C++11的版本

  1 #include <atomic>
2
3 std::atomic<uint64_t> sharedValue(0);
4
5 void storeValue()
6 {
7 sharedValue.store(0x100000002, std::memory_order_relaxed);
8 }
9
10 uint64_t loadValue()
11 {
12 return sharedValue.load(std::memory_order_relaxed);
13 }
14

你可能注意到,不管Mintomic还是C++11版本的代码都使用了relaxed语义的原子操作,也就是带有_relaxed后缀的内存序列参数。

特别地,关于relaxed语义的原子操作,在此原子操作的之前或者之后的指令都可能被影响,也就是被乱序执行。可能是因为编译器指令乱序或者处理器的指令乱序。编译器可能还是在重复的relaxed原子操作上做一些优化,就像在非原子性的操作上一样。在所有的情况下,这个操作都是原子操作。

当并发地操作共享变量,一贯地使用C++11 atomic库或者Mintomic是个好习惯,即使是你知道在你所针对的平台上,普通的load或store操作已经是原子操作。一个atomic库的方法可以起到一个提示作用,提示这个变量是并发访问的。

[译]Atomic VS. Non-Atomic 操作的更多相关文章

  1. Java中的Atomic包

    Atomic包的作用 方便程序员在多线程环境下,无锁的进行原子操作 Atomic包核心 Atomic包里的类基本都是使用Unsafe实现的包装类,核心操作是CAS原子操作: 关于CAS compare ...

  2. java多线程详解(8)-volatile,Atomic比较

    在变成过程中我们需要保证变量的线程安全,在java中除了使用锁机制或者Threadlocal等保证线程安全,还提供了 java.util.concurrent.atomic.Atomic*(如Atom ...

  3. C++11 并发指南六(atomic 类型详解四 C 风格原子操作介绍)

    前面三篇文章<C++11 并发指南六(atomic 类型详解一 atomic_flag 介绍)>.<C++11 并发指南六( <atomic> 类型详解二 std::at ...

  4. C++11 并发指南六( <atomic> 类型详解二 std::atomic )

    C++11 并发指南六(atomic 类型详解一 atomic_flag 介绍)  一文介绍了 C++11 中最简单的原子类型 std::atomic_flag,但是 std::atomic_flag ...

  5. golang sync/atomic

    刚刚学习golang原子操作处理的时候发现github上面一个比较不错的golang学习项目 附上链接:https://github.com/polaris1119/The-Golang-Standa ...

  6. 并发之java.util.concurrent.atomic原子操作类包

    15.JDK1.8的Java.util.concurrent.atomic包小结 14.Java中Atomic包的原理和分析 13.java.util.concurrent.atomic原子操作类包 ...

  7. golang语言中sync/atomic包的学习与使用

    package main; import ( "sync/atomic" "fmt" "sync" ) //atomic包提供了底层的原子级 ...

  8. boost并发编程boost::atomic

    三个用于并发编程的组件: atomic,thread,asio(用于同步和异步io操作)   atomic atomic,封装了不同计算机硬件的底层操作原语,提供了跨平台的原子操作功能,解决并发竞争读 ...

  9. CAS的实现Atomic类库

    atomic 原子(atomic)本意是"不能被进一步分割的最小粒子",而原子操作(atomic operation)意为"不可被中断的一个或一系列操作".在多 ...

  10. JUC 中的 Atomic 原子类总结

    1 Atomic 原子类介绍 Atomic 翻译成中文是原子的意思.在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割的.在我们这里 Atomic 是指一个操作是不可中断的.即使是 ...

随机推荐

  1. [转载]mysql慢日志文件分析处理

    原文地址:mysql慢日志文件分析处理作者:maxyicha mysql有一个功能就是可以log下来运行的比较慢的sql语句,默认是没有这个log的,为了开启这个功能,要修改my.cnf或者在mysq ...

  2. php100 编程小技巧

    用单引号代替双引号来包含字符串,这样做会更快一些.因为PHP会在双引号包围的字符串中搜寻变量,单引号则 不会,注意:只有echo能这么做,它是一种可以把多个字符串当作参数的“函数”(译注:PHP手册中 ...

  3. Responsive设计——不同设备的分辨率写法

    1.1024px显屏 @media screen and (max-width : 1024px) { /* 样式写在这里 */ } 2.800px显屏 @media screen and (max- ...

  4. JAVA对象是如何占用内存的

      本文使用的是32位的JVM ,jdk1.6.本文基本是翻译的,加上了一些自己的理解,原文见文章底下链接.     在本文中,我们讨论如何计算或者估计一个JAVA对象占多少内存空间.(注意,使用 C ...

  5. 設定 Bootstrap/SASS/Bower/gulp (Windows平台)

    請注意:在進行以下步驟前,你會需要先安裝git,可以參考這篇 git安裝教學 前言 時至今日,幾乎每個人都在討論bootstrap.less 或 sass.我們知道它們是比較新的前端技術,而且有開始愈 ...

  6. Mac下使用sublime text 2开发Python

    入门虽易, 熟练不易, 且行且珍惜 简介:这只是简单介绍一个好的文本工具sublime text的使用,如果要获得详细的教程可以去看这本书<Sublime Productivity>和一些 ...

  7. Huawei HG556a A版 刷 openwrt

    一直想玩玩openwrt,调研了一下 HG556a尽管散热很烂,但性价比超高,于是淘宝入手一台A版,A版和C版区别为wifi芯片: 到货后在网上找了几个教程便开始动手刷openwrt,但刷机的过程中还 ...

  8. [Learn Android Studio 汉化教程]第三章:使用 Android Studio 编程

    [Learn Android Studio 汉化教程]第三章:使用 Android Studio 编程 本章包含如何在 Android Studio 中书写或生成代码. Android Studio ...

  9. mac 下 sphinx + mysql + php 实现全文搜索(xampp)(1)

      原理: 使用sphinx 中的indexer 生成索引数据 service/web 端 利用searched 调用索引数据 步骤: 下载 sphinx: 下载地址:http://sphinxsea ...

  10. 从JAVA多线程理解到集群分布式和网络设计的浅析

    对于JAVA多线程的应用非常广泛,现在的系统没有多线程几乎什么也做不了,很多时候我们在何种场合如何应用多线程成为一种首先需要选择的问题,另外关于java多线程的知识也是非常的多,本文中先介绍和说明一些 ...