哪些形式上的保证读操作不会在其他线程中看到写操作(带有竞争条件)?

时间:2019-06-19 18:41:54

标签: c++ multithreading language-lawyer stdatomic data-race

这是有关C ++标准的正式保证的问题。

当潜在的竞争条件存在时,什么保证了对共享变量(正常,非原子)的读取看不到写入,因此两次读取看到了两个“后续”(在程序顺序方面以及在有条件的)写入,并导致每次读取导致一次写入,并导致数据竞争:

// Global state
int x = 0, y = 0;

// Thread 1:
r1 = x;
if (r1 == 42) y = r1;

// Thread 2:
r2 = y;
if (r2 == 42) x = 42;

原子对象的标准explicitly says such behavior is allowed by the specification

  

[注意:以下要求确实允许r1 == r2 == 42   例如,x和y最初为零:

// Thread 1:
r1 = x.load(memory_order_relaxed);
if (r1 == 42) y.store(r1, memory_order_relaxed);
// Thread 2:
r2 = y.load(memory_order_relaxed);
if (r2 == 42) x.store(42, memory_order_relaxed);
     

但是,实现不应允许这种行为。 –尾注]

所谓的“内存模型”的哪个部分保护非原子对象免受这些可见交互作用所引起的交互作用

4 个答案:

答案 0 :(得分:10)

  

当潜在的竞争条件存在时,什么保证了读取共享变量(普通,非原子)看不到写入内容

没有这样的保证。

存在竞争条件时,程序的行为是不确定的:

  

[介绍种族]

     

如果两个动作可能同时发生,

     
      
  • 它们是由不同的线程执行的,或者
  •   
  • 它们是无序列的,至少一个是由信号处理程序执行的,并且它们都不都是由相同的信号处理程序调用执行的。
  •   
     

如果一个程序的执行包含两个潜在的并发冲突动作,其中至少一个不是原子动作,且两个动作都不会在另一个动作之前发生,则该程序的执行将引起数据争用,除了以下所述的信号处理程序的特殊情况。 任何此类数据竞争都会导致不确定的行为。 ...

特殊情况与该问题不是很相关,但出于完整性考虑,我将其包括在内:

  

两次访问同一类型volatile std::sig_­atomic_­t的对象都不会导致数据争用,即使两者都在同一线程中发生,即使在信号处理程序中发生了一次或多次也是如此。 ...

答案 1 :(得分:7)

  

所谓的“内存模型”的哪一部分可以保护非原子对象免受这些读取所引起的交互作用?

没有。实际上,情况恰恰相反,标准明确地将其称为未定义行为。在[intro.races]\21中,我们有

  

如果一个程序的执行包含两个潜在的并发冲突动作,其中至少一个不是原子动作,且两个动作都不会在另一个动作之前发生,则该程序的执行将引起数据争用,除了以下所述的信号处理程序的特殊情况。任何此类数据争用都会导致不确定的行为。

涵盖第二个示例。


规则是,如果您在多个线程中共享数据,并且这些线程中至少有一个写入该共享数据,则数据本身必须为atomic<>或您需要同步以确保在以下情况下发生写操作没有其他线程正在读取或写入。否则,您将面临数据争夺和不确定的行为。 (只要没有编写者,可以同时执行多个不同步的 readers 。)

请注意,volatile不是有效的同步机制。您需要原子/互斥对象/条件变量来保护共享访问。 (正确使用原子memory_order_acquirereleaseseq_cst可以使您创建自己的互斥,从而可以安全地读取或写入非原子变量,例如在执行确保该线程为其自身保留了该节点的操作之后,将其添加到循环缓冲区队列中的节点,这就是为什么ISO C ++ 11定义原子的内存顺序的原因是在写入器和读取器之间创建了“与...同步”关系)

std::atomic<>变量的同时非同步读/写不是UB,但是如果您的代码仅对某些可能的读和写顺序正确工作,则可能是逻辑上的花园式“竞赛条件”错误。写。

答案 2 :(得分:4)

其他人给了您答案,引用了标准的适当部分,明确指出您认为存在的保证不存在。似乎您正在解释该标准的一部分,即如果您使用memory_order_relaxed,则表示原子对象允许某种奇怪的行为,这意味着非原子对象不允许这种行为。这是推理的飞跃,该标准的其他部分明确声明了对非原子对象的行为未定义的推理。

实际上,这是线程1中可能发生的事件的顺序,这是完全合理的,但是即使硬件保证所有内存访问在CPU之间完全序列化,也会导致您认为被禁止的行为。请记住,该标准不仅要考虑硬件的行为,而且还要考虑优化器的行为,而优化器通常会积极地对代码进行重新排序和重写。

线程1可以由优化器重新编写为如下形式:

old_y = y; // old_y is a hidden variable (perhaps a register) created by the optimizer
y = 42;
if (x != 42) y = old_y;

优化器这样做可能有完全合理的理由。例如,它可能决定将42写入y的可能性要大得多,并且由于依赖性的原因,如果将存储在y中,则管道可能会更好发生早于而不是迟发生。

规则是,明显的结果必须看起来像 ,您编写的代码就是执行的代码。但是,并不要求您编写的代码与CPU实际执行的操作完全相似。

原子变量对编译器重新编写代码的能力施加了约束,并指示编译器发出特殊的CPU指令,这些指令对CPU对内存访问进行重新排序的能力施加了约束。涉及memory_order_relaxed的约束要比通常允许的约束强得多。如果不是原子的,通常允许编译器完全摆脱对xy的任何引用。

此外,如果它们是原子的,则编译器必须确保其他CPU将整个变量视为具有新值或旧值。例如,如果变量是一个跨越高速缓存行边界的32位实体,并且修改涉及更改高速缓存行边界两侧的位,则一个CPU可能会看到从未写入的变量值,因为它只能看到对高速缓存行边界一侧的位的更新。但这不适用于用memory_order_relaxed修改的原子变量。

这就是为什么数据争用被标准标记为未定义行为的原因。可能发生的事情的空间可能比您的想像力要大得多,而且肯定比任何标准所能合理涵盖的空间都要宽。

答案 3 :(得分:1)

(Stackoverflow抱怨我在上方添加了太多评论,因此我将它们整理成一个答案并作了一些修改。)

您从C ++标准工作草案N3337中引用的拦截错误。

  

[注意:以下要求确实允许r1 == r2 == 42   例如,x和y最初为零:

     

// Thread 1: r1 = x.load(memory_order_relaxed); if (r1 == 42) y.store(r1, memory_order_relaxed); // Thread 2: r2 = y.load(memory_order_relaxed); if (r2 == 42) x.store(42, memory_order_relaxed);

一种编程语言永远都不允许这种“ r1 == r2 == 42”的发生。 这与内存模型无关。这是因果关系所必需的,因果关系是基本逻辑方法,也是任何编程语言设计的基础。它是人与计算机之间的基本契约。任何内存模型都应遵守。否则,它是一个错误。

此处的因果关系由线程内操作之间的线程内相关性反映,例如数据相关性(例如,在同一位置写入后读取)和控制相关性(例如,分支中的操作)等。它们不能被任何语言规范所违反。任何编译器/处理器设计都应在其提交结果(即,外部可见结果或程序可见结果)中尊重依赖性。

内存模型主要是关于多处理器之间的内存操作排序,尽管弱模型可能使一个处理器中发生的因果关系被另一处理器违反(或看不到),但是它永远不应违反线程内依赖性。

在您的代码段中,两个线程都具有(线程内)数据相关性(load-> check)和控制相关性(check-> store),以确保它们各自的执行(在线程内)是有序的。这意味着,我们可以检查后一个操作的输出,以确定前一个操作是否已执行。

然后,我们可以使用简单的逻辑来推断,如果r1r2均为42,则必须有一个依赖周期,这是不可能的,除非您删除一个条件检查,这实质上打破了依赖周期。这与内存模型无关,但与线程内数据相关。

在C ++ std中定义了因果关系(或更准确地说,这里是线程内依赖关系),但是在早期的草稿中没有明确定义因果关系,因为依赖关系更多地是微体系结构和编译器术语。在语言规范中,通常将其定义为操作语义。例如,由“ if语句”形成的控制依赖关系在您引用为“如果条件为true时,将执行第一个子语句。”的草稿的相同版本中进行定义。这定义了顺序执行顺序。

也就是说,编译器和处理器可以安排if分支的一个或多个操作在if条件解决之前执行。但是,无论编译器和处理器如何调度操作,在解决if条件之前,都不能提交if分支的结果(即对程序可见)。应该区分语义要求和实现细节。一种是语言规范,另一种是编译器和处理器如何实现语言规范。

实际上,当前的C ++标准草案对https://timsong-cpp.github.io/cppwp/atomics.order#9中的此错误进行了细微的更改。

  

[注意:在以下示例中,建议同样不允许:r1 == r2 == 42,其中x和y最初再次为零:

     

// Thread 1: r1 = x.load(memory_order_relaxed); if (r1 == 42) y.store(42, memory_order_relaxed); // Thread 2: r2 = y.load(memory_order_relaxed); if (r2 == 42) x.store(42, memory_order_relaxed);