Mat*_*att 12 c multithreading portability volatile thread-safety
看着经过一大堆 的 其他 问题 和 他们的 答案,我得到的印象是有什么在C“挥发性”关键字表示正好没有广泛的协议。
即使标准本身似乎也不够清晰,以至于每个人都无法理解其含义。
除其他问题外:
总结一下问题,似乎(经过大量阅读)“ volatile”保证了类似的结果:该值将不但从/向寄存器,而且至少向内核的L1缓存中读/写,其顺序与读/写出现在代码中。但这似乎没有用,因为在同一线程内从寄存器中读取/写入寄存器已经足够,而与L1缓存进行协调并不能保证与其他线程进行协调。我无法想象仅与L1缓存进行同步的重要性。
用途1
唯一广泛同意使用volatile的似乎是旧的或嵌入式系统,其中某些内存位置通过硬件映射到I / O功能,例如内存中的某个位(直接在硬件中)控制灯光。 ,或内存中的某个位告诉您键盘键是否按下(因为它是通过硬件直接连接到键的)。
看来,“用1”不移植的代码,其目标包括多核系统发生。
USE 2
与“ use 1”没什么不同,是可由中断处理程序(可以控制灯光或存储来自按键的信息)随时读取或写入的内存。但是为此已经存在一个问题,即取决于系统,中断处理程序可能会在 具有自己的内存缓存的不同内核上运行,并且“ volatile”不能保证所有系统上的缓存一致性。
因此,“使用2”似乎超出了“易失性”所能提供的范围。
用途3
我看到的唯一其他无可争议的用途是防止通过指向编译器未意识到的同一内存的不同变量的不同变量对访问进行错误优化。但这可能只是无可争议的,因为人们没有在谈论它-我只看到其中一个提及。而且我认为C标准已经认识到“不同”的指针(例如指向函数的不同args)可能指向同一项目或附近的项目,并且已经指定编译器必须生成即使在这种情况下也可以工作的代码。但是,我无法在最新的标准(500页!)中快速找到此主题。
那么“使用3”也许根本不存在?
因此,我的问题是:
在多核系统的可移植C代码中,“ volatile”是否可以保证任何东西?
浏览最新标准后,答案似乎至少是非常有限的:
1.该标准针对特定类型“ volatile sig_atomic_t”反复指定特殊处理。但是该标准还说,在多线程程序中使用信号功能会导致不确定的行为。因此,该用例似乎仅限于单线程程序与其信号处理程序之间的通信。
2.该标准还为setjmp / longjmp指定了“ volatile”的明确含义。(在其他问题和答案中给出了重要示例代码)。
因此,更精确的问题变成了:
除了(1)允许单线程程序从其信号处理程序接收信息之外,还是(2)允许setjmp,“ volatile”对于多核系统的便携式C代码是否有任何保证?代码以查看在setjmp和longjmp之间修改的变量?
这仍然是一个是/否问题。
如果为“是”,那么最好显示一个无错误的可移植代码示例,如果省略了“ volatile”,则该示例会出现错误。如果为“ no”,那么我认为对于多核目标,在这两种非常特殊的情况下,编译器可以随意忽略“ volatile”。
我不是专家,但是cppreference.com上似乎有一些不错的信息volatile。这是要点:
通过volatile限定类型的左值表达式进行的每次访问(读和写)都被认为是可观察到的副作用,用于优化目的,并且严格按照抽象机的规则进行评估(也就是说,所有写操作均在以下位置完成)在下一个序列点之前的某个时间)。这意味着在单个执行线程中,相对于由序列点与易失性访问分隔开的另一个可见副作用,易失性访问无法优化或重新排序。
它还提供了一些用途:
挥发物的用途
1)静态易失性对象对内存映射的I / O端口进行建模,静态const易失性对象对内存映射的输入端口进行建模,例如实时时钟
2)sig_atomic_t类型的静态易失性对象用于与信号处理程序进行通信。
3) volatile variables that are local to a function that contains an invocation of the setjmp macro are the only local variables guaranteed to retain their values after longjmp returns.
4) In addition, volatile variables can be used to disable certain forms of optimization, e.g. to disable dead store elimination or constant folding for microbenchmarks.
And of course, it mentions that volatile is not useful for thread synchronization:
Note that volatile variables are not suitable for communication between threads; they do not offer atomicity, synchronization, or memory ordering. A read from a volatile variable that is modified by another thread without synchronization or concurrent modification from two unsynchronized threads is undefined behavior due to a data race.
首先,从历史上看,关于volatile访问和类似含义的不同解释存在各种问题。请参阅此研究:Volatiles 被错误编译,以及如何处理。
除了该研究中提到的各种问题之外, 的行为volatile是可移植的,除了其中一个方面:当它们充当 内存屏障时。内存屏障是某种机制,用于防止代码的并发无序执行。使用volatile内存屏障一样肯定是不可移植。
C 语言是否保证内存行为volatile显然是有争议的,尽管我个人认为该语言很清楚。首先我们有副作用的正式定义,C17 5.1.2.3:
访问
volatile对象、修改对象、修改文件或调用执行任何这些操作的函数都是副作用,它们是执行环境状态的变化。
该标准将术语排序定义为确定评估(执行)顺序的一种方式。定义既正式又繁琐:
Sequenced before是由单个线程执行的评估之间的不对称、可传递、成对关系,这会导致这些评估之间存在偏序。给定任意两个评估 A 和 B,如果 A 在 B 之前被排序,那么 A 的执行应该在 B 的执行之前。(相反,如果 A 在 B 之前被排序,那么 B在A之后被 排序。)如果 A 没有被排序在 B 之前或之后,则 A 和 B 是未 排序的。当 A 在 B 之前或之后排序时,评估 A 和 B 是不确定排序的,但未指定哪个。 13)序列点的存在 表达式 A 和 B 的计算之间意味着与 A 相关的每个值计算和副作用在与 B 相关的每个值计算和副作用之前排序。(序列点的摘要在附录 C 中给出。)
上面的 TL;DR 基本上是,如果我们有一个A包含副作用的表达式,它必须在另一个表达式之前执行B,以防B在 之后排序A。
通过这部分可以优化 C 代码:
在抽象机中,所有表达式都按照语义指定的方式进行评估。如果一个实际的实现可以推断出它的值未被使用并且没有产生所需的副作用(包括由调用函数或访问易失性对象引起的任何副作用),则它不需要计算表达式的一部分。
这意味着程序可以按照标准在其他地方规定的顺序(评估顺序等)评估(执行)表达式。但是如果它可以推断出它没有被使用,它就不需要评估(执行)一个值。例如,该操作0 * x不需要计算x,只需将表达式替换为0.
除非访问变量是副作用。这意味着在 case xis 时volatile,即使结果始终为 0 ,它也必须评估(执行)0 * x。不允许优化。
此外,该标准谈到了可观察的行为:
对一致实现的最低要求是:
- 对 volatile 对象的访问严格按照抽象机的规则进行评估。
/--/ 这是程序的可观察行为。
鉴于上述所有情况,符合标准的实现(编译器 + 底层系统)可能不会volatile以无序的顺序执行对象的访问,以防编写的 C 源代码的语义另有说明。
这意味着在这个例子中
volatile int x;
volatile int y;
z = x;
z = y;
Run Code Online (Sandbox Code Playgroud)
两个赋值表达式都必须求值并且z = x; 必须在 之前求值z = y;。将这两个操作外包给两个不同的未排序内核的多处理器实现不符合标准!
困境在于编译器不能做很多事情,比如预取缓存和指令流水线等,尤其是在操作系统之上运行时。所以编译器把这个问题交给程序员,告诉他们内存屏障现在是程序员的责任。而 C 标准明确指出该问题需要由编译器解决。
编译器不一定关心解决问题,因此volatile为了充当内存屏障是不可移植的。它已成为实施质量问题。
总结一下这个问题,(在阅读了很多内容之后)似乎“易失性”保证了类似的内容:该值不仅会从寄存器读取/写入,而且至少会读取到内核的 L1 缓存,其顺序与读/写出现在代码中。
不,绝对不是。这使得 volatile 对于 MT 安全代码来说几乎毫无用处。
如果确实如此,那么 volatile 对于多线程共享的变量来说将是非常好的,因为在能够协作的典型 CPU(即主板上的多核或多 CPU)中,对 L1 缓存中的事件进行排序就足够了以某种方式使 C/C++ 或 Java 多线程的正常实现成为可能,并且具有典型的预期成本(即,对于大多数原子或非满足互斥操作而言,成本并不高)。
但无论是在理论上还是在实践中,易失性都没有在缓存中提供任何有保证的排序(或“内存可见性”)。
(注:以下内容是基于对标准文档的合理解释、标准的意图、历史实践以及对编译器编写者期望的深刻理解。这种方法基于历史、实际实践以及真实的人们的期望和理解)现实世界,这比解析一个不被认为是一流规范编写并且已经被修改了很多次的文档的文字要强大和可靠得多。)
在实践中,易失性确实保证了 ptrace 能力,即在任何优化级别上为正在运行的程序使用调试信息的能力,并且事实上调试信息对于这些易失性对象是有意义的:
ptrace(类似 ptrace 的机制)在涉及易失性对象的操作之后在序列点处设置有意义的断点:您确实可以在这些点上中断(请注意,只有当您愿意将许多断点设置为任何断点时,这才有效C/C++ 语句可能会被编译到许多不同的汇编起点和终点,就像在大规模展开的循环中一样);在实践中,易失性保证比严格的 ptrace 解释要多一点:它还保证易失性自动变量在堆栈上有一个地址,因为它们没有分配给寄存器,寄存器分配将使 ptrace 操作更加微妙(编译器可以输出调试信息来解释变量如何分配给寄存器,但读取和更改寄存器状态比访问内存地址稍微复杂一些)。
请注意,完整的程序调试能力,即至少在序列点考虑所有变量的易失性,是由编译器的“零优化”模式提供的,该模式仍然执行诸如算术简化之类的琐碎优化(通常不能保证不所有模式下的优化)。但易失性比非优化更强:x-x可以对非易失性整数进行简化x,但不能对易失性对象进行简化。
因此,易失性意味着保证按原样进行编译,就像系统调用的编译器从源代码到二进制/程序集的转换一样,编译器不会以任何方式重新解释、更改或优化。请注意,库调用可能是也可能不是系统调用。许多官方系统函数实际上是库函数,它们提供了一层薄薄的插入,并且通常在最后遵循内核。(特别getpid是不需要进入内核,并且可以很好地读取操作系统提供的包含信息的内存位置。)
不稳定的交互是真实机器与外界的交互,必须遵循“抽象机器”。它们不是程序部分与其他程序部分的内部交互。编译器只能推理它所知道的内容,即内部程序部分。
易失性访问的代码生成应该遵循与该内存位置最自然的交互:这应该不足为奇。这意味着一些易失性访问预计是原子的:如果在架构上读取或写入 a 表示的自然方式long是原子的,那么预计 a 的读取或写入volatile long将是原子的,因为编译器不应生成例如,逐字节访问易失性对象的愚蠢低效代码。
您应该能够通过了解架构来确定这一点。您不必了解有关编译器的任何信息,因为volatile 意味着编译器应该是透明的。
但是,易失性只不过是强制发出预期的程序集,以便针对特定情况进行最不优化的操作来执行内存操作:易失性语义意味着一般情况语义。
一般情况是编译器在没有有关构造的任何信息时所做的操作:f.ex。通过动态分派在左值上调用虚函数是一般情况,在编译时确定表达式指定的对象的类型后直接调用重写器是一种特殊情况。编译器始终对所有构造进行一般情况处理,并且遵循 ABI。
易失性没有做任何特别的事情来同步线程或提供“内存可见性”:易失性仅提供从执行或停止的线程内部(即CPU核心内部)看到的抽象级别的保证:
只有第二点意味着 volatile 在大多数线程间通信问题中没有用处;对于任何不涉及与 CPU 外部但仍在内存总线上的硬件组件进行通信的编程问题,第一点本质上是无关的。
从运行线程的核心的角度来看, 易失性 提供有保证的行为意味着异步信号传递到该线程,这些信号是从该线程的执行顺序的角度运行的,请参阅源代码顺序中的操作。
除非您打算向线程发送信号(这是一种非常有用的方法,用于整合有关当前正在运行的线程的信息,而无需事先商定停止点),否则 volatile 不适合您。
| 归档时间: |
|
| 查看次数: |
309 次 |
| 最近记录: |