【问题标题】:C: thread safety and order of operationsC:线程安全和操作顺序
【发布时间】:2018-11-21 15:10:18
【问题描述】:

考虑以下 C 代码:

static sig_atomic_t x;
static sig_atomic_t y;

int foo()
{
    x = 1;
    y = 2;
}

第一个问题:C 编译器能否决定将foo 的代码“优化”为y = 2; x = 1(即y 的内存位置在x 的内存位置之前更改)?这将是等效的,除非涉及多个线程或信号。

如果第一个问题的答案是“是”:如果我真的想要保证x 存储在y 之前,我该怎么办?

【问题讨论】:

    标签: c thread-safety


    【解决方案1】:

    是的,编译器可能会更改两个赋值的顺序,因为重新排序不是“可观察的”按照 C 标准的定义,例如,赋值没有副作用 (同样,按照 C 标准的定义,它不考虑外部观察者的存在)。

    在实践中,您需要某种屏障/栅栏来保证顺序,例如,使用多线程环境提供的服务,或者可能是 C11 stdatomic.h(如果可用)。

    【讨论】:

      【解决方案2】:

      C 标准指定了一个称为可观察行为 的术语。这意味着,编译器/系统至少有一些限制:不允许重新排序包含volatile 限定操作数的表达式,也不允许重新排序输入/输出。

      除了那些特殊情况,任何事情都是公平的。它可以在 x 之前执行 y,也可以并行执行它们。它可能会优化整个代码,因为代码中没有可观察到的副作用。以此类推。

      请注意线程安全和执行顺序是不同的东西。线程是由程序员/库显式创建的。上下文切换可能会中断任何非原子的变量访问。这是另一个问题,解决方案是使用互斥锁、_Atomic 限定符或类似的保护机制。


      如果顺序很重要,您应该volatile-限定变量。在这种情况下,语言做出以下保证:

      C17 5.1.2.3 § 6(可观察行为的定义):

      对 volatile 对象的访问严格按照抽象机的规则进行评估。

      C17 5.1.2.3 § 4:

      在抽象机中,所有表达式都按照语义指定的方式进行评估。

      “语义”几乎是整个标准,例如指定; 由序列点组成的部分。 (在这种情况下,C17 6.7.6“完整的结束 declarator 是一个序列点。”术语“sequenced before”在 C17 5.1.2.3 §3 中指定。

      所以给出这个:

      volatile int x = 1;
      volatile int y = 1;
      

      那么保证初始化的顺序是x在y之前,因为第一行的;保证了排序顺序,volatile保证程序严格遵循标准中规定的求值顺序。


      现在就像现实世界中发生的那样,volatile 不保证在多核系统的许多编译器实现中存在内存屏障。这些实现不符合要求。

      机会主义编译器可能声称程序员必须使用系统特定的内存屏障来保证执行顺序。但是在volatile 的情况下,这是不正确的,如上所述。他们只是想逃避责任并将其交给程序员。 C 标准不关心 CPU 是否有 57 个内核、分支预测和指令流水线。

      【讨论】:

      • volatile 的操作不需要(必须)设置内存屏障,因为未定义并发读/写访问。
      • @LWimsey 我刚刚在这个答案中引用了为什么它并发 execution 是明确定义的。测序在 C17 5.1.2.3 §3 中定义。数据存储器的并发访问是另一回事。内存屏障的目的是保证执行顺序,而不是保证数据的线程安全。如果你使用volatile关键字,那么在底层机器代码中正确实现内存屏障就是C编译器的工作。
      • 您的报价是关于volatile 对象的评估顺序。编译器必须确保根据抽象机器的规则对它们进行评估,但这并不意味着其他线程以相同的顺序观察到这些操作的效果。事实上,关于其他线程的排序推理是没有意义的,因为volatile 不会使对象无数据竞争。我不确定您如何看待“concurrent execution is well-defined”和“Concurrent access of data memory is another story”之间的区别,但是并发读取/写访问....
      • .... 根据 C17 5.1.2.4 §35,在非原子对象上的未定义行为:如果程序的执行包含不同线程中的两个冲突操作,则该程序的执行包含数据竞争,其中至少一个不是原子的,并且两者都不会在另一个之前发生。任何此类数据竞争都会导致未定义的行为。 由于对 volatile 对象的操作仅在单个线程中定义良好(无需额外同步),因此不涉及内存屏障。 Microsoft 编译器在 volatile 操作上使用了内存屏障,但这是基于比必要的更强大的保证。
      • @LWimsey 我的意思是每个线程的执行顺序是明确定义的。如果编译器要并行执行以使其由多个内核执行(C 标准并未禁止),它仍必须遵循抽象机的规则。这包括指令缓存、分​​支预测和流水线执行等内容。
      猜你喜欢
      • 1970-01-01
      • 2019-07-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多