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

与顺序一致模式相对的就是 std::memory_order_relaxed 模式,即宽松模式.由于去除了先发生于(happens-before)这个关系限制, 宽松模式仅需极少的同步指令即可实现.这种模式下,不同于之前的顺序一致模式,我们可以对原子变量操作进行各种优化了,譬如执行死代码删除等等.

看一下之前的示例:

  -Thread 1-  y.store (20, memory_order_relaxed)  x.store (10, memory_order_relaxed)    -Thread 2-  if (x.load (memory_order_relaxed) == 10)   {    assert (y.load(memory_order_relaxed) == 20) /* assert A */    y.store (10, memory_order_relaxed)   }    -Thread 3-  if (y.load (memory_order_relaxed) == 10)   assert (x.load(memory_order_relaxed) == 10) /* assert B */

由于线程间不再需要同步(译注:由于使用了宽松模式,原子操作之间不再形成同步关系,这里的不需要同步指的是不需要原子操作间的同步),所以代码中的任一断言都可能失败.

由于没有了先发生于(happens-before)的关系,从单一线程的角度来看,其他线程不再存在对其可见的特定原子变量写入顺序.如果使用时不是非常小心,宽松模式会导致很多非预期的结果.这个模式唯一保证的一点就是: 一旦线程 2 观察到了线程 1 中对某一原子变量的写入数值,那么线程 2 就不会再看到线程 1 对该变量更早的写入数值.

我们还是来看个示例(假定 x 的初始值为 0):

  -Thread 1-  x.store (1, memory_order_relaxed)  x.store (2, memory_order_relaxed)    -Thread 2-  y = x.load (memory_order_relaxed)  z = x.load (memory_order_relaxed)  assert (y <= z)

代码中的断言不会失败.一旦线程 2 读取到 x 的数值为 2,那么线程 2 后面对 x 的读取操作将不可能取得数值 1(1 较 2 是 x 更早的写入数值).这一特性导致了一个结果:
如果代码中存在多个对同一变量的宽松模式读取,但是这些读取之间存在对其他引用(可能是之前同一变量的别名)的宽松模式读取,那么我们不能把这多个对同一变量的宽松模式读取合并(多个读取并成一个).

这里还有一个假定就是某一线程对于原子变量的宽松写入将在一段合理的时间内对另一线程可见(通过宽松读取).这意味着,在一些非缓存一致的体系架构上, 宽松操作需要主动的去刷新缓存(当然,刷新操作可以进行合并,譬如在多个宽松操作之后再进行一次刷新操作).

宽松模式最常用的场景就是当我们仅需要一个原子变量,而不需要使用该原子变量同步线程间共享内存的时候.(译注:譬如一个原子计数器)

获得/释放模式(acquire/release)

第三种模式混合了之前的两种模式.获得/释放模式类似于之前的顺序一致模式,不同的是该模式只保证依赖变量间产生先发生于(happens-before)的关系.这也使得独立读取操作和独立写入操作之间只需要比较少的同步.

假设 x 和 y 的初始值为 0 :

   -Thread 1-   y.store (20, memory_order_release);     -Thread 2-   x.store (10, memory_order_release);     -Thread 3-   assert (y.load (memory_order_acquire) == 20 && x.load (memory_order_acquire) == 0)     -Thread 4-   assert (y.load (memory_order_acquire) == 0 && x.load (memory_order_acquire) == 10)

代码中的两个断言可能同时通过,因为线程 1 和线程 2 中的两个写入操作并没有先后顺序.

但是如果我们使用顺序一致模式来改写上面的代码,那么这两个写入操作中必然有一个写入先发生于(happens-before)另一个写入(尽管运行时才能确定实际的先后顺序),并且这个顺序是多线程一致的(通过必要的同步操作),所以代码中如果一个断言通过,那么另一个断言就一定会失败.

如果我们在代码中使用非原子变量,那么事情会变的更复杂一些,但是这些非原子变量的可见性同他们是原子变量时是一致的(译注:参看下面代码).任何原子写入操作(使用释放模式)之前的写入对于其他同步的线程(使用获取模式并且读取到了之前释放模式写入的数值)都是可见的.

   -Thread 1-   y = 20;   x.store (10, memory_order_release);     -Thread 2-   if (x.load(memory_order_acquire) == 10)    assert (y == 20);

线程 1 中对 y 的写入(y = 20)先发生于对 x 的写入(x.store (10, memory_order_release)),因此线程 2 中的断言不会失败(译注:这里说的有些简略,扩展来讲的话应该是线程 1 中 对 y 的写入 先发生于 对 x 的写入, 而线程 1 中 对 x 的写入 又同步于线程 2 中 对 x 的读取, 由于线程 2 中 对 x 的读取 又先发生于 对 y 的断言,于是线程 1 中 对 y 的写入 先发生于线程 2 中 对 y 的断言,这个 对 y 的断言 也就不会失败了).由于有上述的同步要求,原子操作周围的共享内存(非原子变量)操作一样有优化上的限制(译注:不能随意对这些操作进行优化,以上面代码为例,优化操作不能将 y = 20 重排于 x.store (10, memory_order_release) 之后).

消费/释放模式(consume/release)

消费/释放模式是对获取/释放模式进一步的改进,该模式下,非依赖共享变量的先发生于关系不再成立.

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

ctvol管理联系方式QQ:251552304

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

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

精彩推荐