为什么线程同步不需要volatile关键字?

我读到volatile关键字不适合线程同步,事实上根本不需要它。

虽然我知道使用这个关键字是不够的,但我不明白为什么它完全没必要。

例如,假设我们有两个线程,线程A仅从共享变量读取,线程B仅写入共享变量。 通过例如pthreads互斥锁实现适当的同步。

IIUC,没有volatile关键字,编译器可能会查看线程A的代码并说:“这里的变量似乎没有被修改,但是我们有很多读取; 让我们只读一次,缓存值并优化掉所有后续读取。“它也可以查看线程B的代码并说:”我们在这里有很多写这个变量但没有读取; 因此,不需要写入值,因此我们可以优化所有写入。“

两种优化都是不正确的。 和 挥发性会阻止一个人。 因此,我可能会得出结论,虽然volatile不足以同步线程,但仍然需要在线程之间共享任何变量。 (注意:我现在读到,实际上不需要volatile来防止写入错误;所以我没有想法如何防止这种不正确的优化)

我明白我错在这里。 但为什么?

例如,假设我们有两个线程,线程A仅从共享变量读取,线程B仅写入共享变量。 通过例如pthreads互斥锁实现适当的同步。

IIUC,没有volatile关键字,编译器可能会查看线程A的代码并说:“这里的变量似乎没有被修改,但是我们有很多读取; 让我们只读一次,缓存值并优化掉所有后续读取。“它也可以查看线程B的代码并说:”我们在这里有很多写这个变量但没有读取; 因此,不需要写入值,因此我们可以优化所有写入。“

与大多数线程同步原语一样,pthreads互斥操作具有明确定义的内存可见性语义 。

平台支持pthreads,或者不支持。 如果它支持pthread,它支持pthreads互斥。 这些优化是安全的还是不安全的。 如果它们是安全的,那就没问题了。 如果它们不安全,那么任何制作它们的平台都不支持pthreads互斥锁。

例如,你说“这里似乎没有修改变量”,但确实如此 – 另一个线程可以在那里修改它。 除非编译器能够certificate其优化不能破坏任何符合程序,否则它无法实现。 符合程序可以修改另一个线程中的变量。 编译器支持POSIX线程,或者不支持。

实际上,大多数情况都是在大多数平台上自动发生的。 只是阻止编译器知道内部互斥操作的作用。 另一个线程可以做的任何事情,互斥操作本身都可以。 因此编译器必须在进入和退出这些函数之前“同步”内存。 例如,它不能在调用pthread_mutex_lock中在寄存器中保留一个值,因为只要它知道, pthread_mutex_lock就会在内存中访问该值。 或者,如果编译器具有关于互斥函数的特殊知识,那么将包括了解跨这些调用的其他线程可访问的缓存值的无效性。

需要volatile的平台几乎无法使用。 对于特定情况,您需要每个函数或类的版本,在这些情况下,对象可能对另一个线程可见,或者从另一个线程可见。 在许多情况下,你几乎必须使一切变得volatile而不是寄存器中的缓存值是一个性能非启动。

正如您可能已经多次听到的那样,C语言中指定的volatile的语义只是不会与线程混用。 它不仅不够,还会禁用许多完全安全且几乎必不可少的优化。

缩短已经给出的答案,您不需要使用volatile与互斥量,原因很简单:

  • 如果编译器知道互斥操作是什么(通过识别pthread_ *函数或因为你使用了std::mutex ),它就知道如何处理优化方面的访问( std::mutex甚至需要)
  • 如果编译器无法识别它们,则pthread_ *函数对它完全不透明,并且任何涉及任何类型的非本地持续时间对象的优化都不能跨越不透明函数

使答案更短,不使用互斥锁或信号量,这是一个错误。 一旦线程B释放互斥锁(并且线程A得到它),寄存器中包含来自线程B的共享变量值的任何值都保证被写入缓存或内存,以防止线程A运行时的竞争条件读取此变量。

保证这一点的实现依赖于体系结构/编译器。

关键字volatile告诉编译器将变量的任何写入或读取视为“可观察的副作用”。 就是这样。 当然,可观察到的副作用不能被优化掉,并且必须按照程序指示的顺序出现在外面世界; 编译器可能不会相互重新排序可观察的副作用 然而,编译器可以自由地对非可观察对象进行重新排序。 因此, volatile仅适用于访问内存映射硬件,Unix风格的信号处理程序等。 对于线程间并发,请使用std::atomic或更高级别的同步对象,如mutexcondition_variablepromise/future