详解C++中的内存同步模式(memory order)分享!

内存模型中的同步模式(memory model synchronization modes)

原子变量同步是内存模型中最让人感到困惑的地方.原子(atomic)变量的主要作用就是同步多线程间的共享内存访问,一般来讲,某个线程会创建一些数据,然后给原子变量设置标志数值(译注:此处的原子变量类似于一个flag);其他线程则读取这个原子变量,当发现其数值变为了标志数值之后,之前线程中的共享数据就应该已经创建完成并且可以在当前线程中进行读取了.不同的内存同步模式标识了线程间数据共享机制的”强弱”程度,富有经验的程序员可以使用”较弱”的同步模式来提高程序的执行效率.

每一个原子类型都有一个 load() 方法(用于加载操作)和一个 store() 方法(用于存储操作).使用这些方法(而不是普通的读取操作)可以更清晰的标示出代码中的原子操作.

  atomic_var1.store(atomic_var2.load()); // atomic variables     vs   var1 = var2;  // regular variables

这些方法还支持一个可选参数,这个参数可以用于指定内存模型的同步模式.

目前这些用于线程间同步的内存模式共有 3 种,我们依此来看下~

顺序一致模式(sequentially consistent)

第一种模式是顺序一致模式(sequentially consistent),这也是原子操作的默认模式,同时也是限制最严格的一种模式.我们可以通过 std::memory_order_seq_cst 来显示的指定这种模式.这种模式下,线程间指令重排的限制与在顺序性代码中进行指令重排的限制是一致的.

观察以下代码:

   -Thread 1-    -Thread 2-   y = 1      if (x.load() == 2)   x.store (2);    assert (y == 1)

虽然代码中的 x 和 y 是没有关联的两个变量,但是代码中指定的内存模型(译注:代码中没有显示指定,则使用默认的内存模式,即顺序一致模式)保证了线程 2 中的断言不会失败.线程 1 中 对 y 的写入 先发生于(happens-before) 对 x 的写入,如果线程 2 读取到了线程 1 对 x 的写入(x.load() == 2),那么线程 1 中 对 x 写入 之前的所有写入操作都必须对线程 2 可见,即使对于那些和 x 无关的写入操作也是如此.这意味着优化操作不能重排线程 1 中的两个写入操作(y = 1 和 x.store (2)),因为当线程 2 读取到线程 1 对 x 的写入之后,线程 1 对 y 的写入也必须对线程 2 可见.

(译注:编译器或者 CPU 会因为性能因素而重排代码指令,这种重排操作对于单线程程序而言是无感知的,但是对于多线程程序而言就不是了,拿上面代码举例,如果将 x.store (2) 重排于 y = 1 之前,那么线程 2 中即使读取发现 x == 2 了,但此时 y 的数值也不一定是 1)

加载操作也有类似的优化限制:

         a = 0         y = 0         b = 1   -Thread 1-       -Thread 2-   x = a.load()      while (y.load() != b)   y.store (b)        ;   while (a.load() == x)  a.store(1)    ;

线程 2 一直循环到 y 发生数值变更,然后对 a 进行赋值;线程 1 则一直在等待 a 发生数值变化.

从顺序性代码的角度来看,线程 1 中的代码 ‘while (a.load() == x)’ 似乎是一个无限循环,编译器编译这段代码时也可能会直接将其优化为一个无限循环(译注:优化为 while (true); 之类的指令);但实际上,我们必须保证每次循环都对 a 执行读取操作(a.load()) 并且将其与 x 进行比较,否则线程 1 和 线程 2 将不能正常工作(译注:线程 1 将进入无限循环,与正确的执行结果不一致).

从实践的角度讲,所有的原子操作都相当于优化屏障(译注:用于阻止优化操作的指令).原子操作(load/store)可以类比为副作用未知的函数调用,优化操作可以在原子操作之间任意的调整代码顺序,但是不能越过原子操作(译注:原子操作类似于是优化调整的边界),当然,线程的私有数据并不受此影响,因为这些数据其他线程并不可见.

顺序一致模式也保证了所有线程间(原子变量(使用 memory_order_seq_cst 模式)的修改顺序)的一致性.以下代码中所有的断言都不会失败(x 和 y 的初始值为 0):

   -Thread 1-    -Thread 2-          -Thread 3-   y.store (20);  if (x.load() == 10) {    if (y.load() == 10)   x.store (10);   assert (y.load() == 20)   assert (x.load() == 10)            y.store (10)           }

 从顺序性代码的角度来看,似乎这是(所有断言都不会失败)理所当然的,但是在多线程环境下,我们必须同步系统总线才能达到这种效果(以使线程 3 与线程 2 观察到的原子变量(使用 memory_order_seq_cst 模式)变更顺序一致),可想而知,这往往需要昂贵的硬件同步.

由于保证顺序一致的特性, 顺序一致模式成为了原子操作中默认使用的内存模式, 当程序员使用这种模式时,一般不太可能获得意外的程序结果.

宽松模式(relaxed)

本文来自网络收集,不代表计算机技术网立场,如涉及侵权请联系管理员删除。

ctvol管理联系方式QQ:251552304

本文章地址:https://www.ctvol.com/c-cdevelopment/482997.html

(0)
上一篇 2020年11月9日
下一篇 2020年11月9日

精彩推荐