原子操作传播/可见性(原子负载与原子RMW负载)

时间:2019-03-09 16:15:24

标签: c++ multithreading atomic propagation memory-visibility

上下文

我正在用C ++编写线程安全的protothread/coroutine library,并且正在使用原子使任务切换变得无锁。我希望它表现得更好。我对原子和无锁编程有一般的了解,但是我没有足够的专业知识来优化我的代码。我做了很多研究,但是很难找到具体问题的答案:在不同的内存顺序下,不同原子操作的传播延迟/可见性是什么?

当前假设

我读到对内存的更改是从其他线程传播的,这种方式可能会变得可见:

  1. 以不同的顺序给不同的观察者
  2. 有些延迟。

我不确定这种延迟的可见性和不一致的传播是仅适用于非原子读取还是原子读取,这可能取决于所使用的存储顺序。在x86机器上进行开发时,我无法测试弱序系统上的行为。

所有原子读取是否总是读取最新值,而不管操作的类型和所使用的内存顺序如何?

我非常确定,所有 read-modify-write (RMW)操作始终都会读取任何线程写入的最新值,而与所使用的内存顺序无关。对于顺序一致操作,似乎也是如此,但前提是对变量的所有其他修改也都是顺序一致。据说两者都很慢,这对我的任务不利。如果不是所有的原子读取都获得最新值,那么根据我目前的理解,我将不得不使用RMW操作仅读取原子变量的最新值,或者在while循环中使用原子读取。

写操作的传播(忽略副作用)是否取决于内存顺序和所使用的原子操作?

(仅当前一个问题的答案是并非所有原子读取都始终读取最新值时,此问题才重要。请仔细阅读,此处我不询问副作用的可见性和传播。我只关心原子变量本身的值。)这暗示着,根据用于修改原子变量的操作,可以保证随后的任何原子读取都将接收到原子变量的最新值。变量。因此,我必须在保证始终读取最新值的操作或使用宽松的原子读取的操作之间进行选择,并与这种特殊的写操作(确保对其他原子操作的修改具有即时可见性)配合使用。

3 个答案:

答案 0 :(得分:4)

原子无锁吗?

首先,让我们摆脱房间里的大象:在代码中使用atomic不能保证实现无锁。 atomic只是无锁实现的启动器。 is_lock_free()会告诉您它对于C ++实现和您使用的基础类型是否真正无锁。

最新值为多少?

在多线程领域,“最新”一词非常含糊。因为一个操作系统可能使一个线程处于睡眠状态的“最新”消息是什么,所以它可能不再是另一个活动线程的最新消息。

std::atomic仅通过确保R, M and RMW在一个线程中对一个原子执行的操作是原子执行的,而没有任何中断,并且所有其他线程在执行之前都可以看到该值,从而避免了竞争情况或之后的值,但介于两者之间。因此atomic通过在同一原子对象上的并发操作之间创建顺序来同步线程。

您需要将每个线程视为具有自己的时间的并行Universe,并且不知道并行Universe中的时间。就像在量子物理学中一样,您可以在一个线程中了解到关于另一线程的唯一事情就是可以观察到的东西(即,宇宙之间的“先于发生”的关系)。

这意味着您不应设想多线程时间,就好像所有线程上都存在绝对的“最新”一样。您需要将时间想象成相对于其他线程而言。这就是为什么原子不创建绝对最新的原因,而仅确保原子将具有的连续状态的顺序的原因。

传播

传播不取决于内存顺序或执行的原子操作。 memory_order是关于围绕原子操作的非原子变量(如围栏)的顺序约束。最好的解释当然是Herb Sutters presentation,如果您正在致力于多线程优化,那绝对值得一小时半。

尽管特定的C ++实现有可能以某种方式影响传播的原子操作,但是您不能依靠任何观察到的观察力,因为不能保证传播以相同的方式进行。下一个版本的编译器或另一个CPU架构上的另一个编译器。

但是传播很重要吗?

designing lock-free algorithms时,很容易读取原子变量以获取最新状态。但是,尽管这种只读访问是原子的,但紧随其后的动作却不是。因此,以下指令可能假定已经过时的状态(例如,因为在原子读取之后立即将线程发送到睡眠状态)。

采用if(my_atomic_variable<10),并假设您阅读9。假设您处在最佳状态,而9将是所有并发线程设置的绝对最新值。将其值与<10进行比较不是原子的,因此,当比较成功并且if分支时,my_atomic_variable可能已经具有新的值10。传播速度有多快,即使保证读取总是得到最新值也是如此。而且我什至没有提到ABA problem

读取的唯一好处是避免数据争用和UB。但是,如果要跨线程同步决策/操作,则需要使用RMW,例如compare-and-swap(例如atomic_compare_exchange_strong),以便原子操作的顺序可以得到可预测的结果。

答案 1 :(得分:1)

多线程是令人惊讶的领域。 首先,原子写入在写入后不排序。我读值并不意味着它是以前写的。有时,此类读取可能曾经看到(间接地,通过其他线程)同一线程随后进行的一些原子写入的结果。

顺序一致性显然与可见性和传播有关。当一个线程写一个原子的“顺序一致”时,它使所有先前的写操作对其他线程可见(传播)。在这种情况下,与写入有关的是(顺序一致的)读取。

通常,性能最高的操作是“松弛”原子操作,但它们提供的订购保证最低。原则上,存在因果关系悖论...:-)

答案 2 :(得分:0)

经过一番讨论,这是我的发现:首先,让我们定义一个原子变量的最新值的含义:在挂钟时间内,最新写入原子变量的内容就是外部观察者的观点。如果有多个同时最后写入(即在同一周期内在多个内核上),那么选择其中一个就无关紧要。

  1. 任何内存顺序的原子加载不能保证读取 latest 值。这意味着必须先传播写入,然后才能访问它们。这种传播可能相对于它们执行的顺序是乱序的,并且对于不同的观察者而言,顺序是不同的。

    这会产生 relativeivity 效果(就像在爱因斯坦的物理学中一样),每个线程都有自己的“真相”,这正是我们需要使用顺序一致性(或获取/释放)进行恢复的原因因果关系:如果我们仅使用宽松的负载,则甚至会导致因果关系中断和明显的时间循环,这可能是由于指令重新排序与无序传播相结合而发生的。内存排序将确保独立线程所感知的独立现实至少在因果上保持一致。

  2. 确保按上述定义的最新值操作原子读-修改-写(RMW)操作(例如交换,compare_exchange,fetch_add等)。这意味着强制进行写传播,并在内存上产生一个通用视图(如果您进行的所有读取均来自使用RMW操作的原子变量),而与线程无关。因此,如果您使用atomic.compare_exchange_strong(value,value, std::memory_order_relaxed)atomic.fetch_or(0, std::memory_order_relaxed),则可以保证感知到一个包含所有原子变量的全局修改顺序。 请注意,这不能保证您对非RMW读取的排序或因果关系。

现在,什么时候使用哪种读物?

如果您仅需要每个线程内的因果关系(关于按什么顺序发生的事情可能仍然有不同的看法,但是至少每个阅读者对世界有因果关系的看法),那么原子装入并获取/释放或顺序一致性就足够了。

但是,如果您还需要重新读取(因此您绝不能读取除全局(跨所有线程)最新值以外的值),则应使用RMW操作进行读取。仅凭这些因素并不会导致非原子和非RMW读取的因果关系,但是所有线程上的所有RMW读取在世界上都拥有完全相同的观点,并且始终是最新的。

因此,总结:如果允许使用不同的世界视图,请使用原子加载,但是如果您需要客观现实,请使用RMW加载。