Art*_*bev 0 c++ multithreading undefined-behavior c++11
让\xe2\x80\x99s 说我有一个硬件,在该硬件上,所有对内存的访问都小于或等于大小bool
都是线程安全的,并且由于硬件或代码而避免了与缓存有关的一致性问题。
我是否应该期望从多个线程对同一对象的非原子访问将仅编译 \xe2\x80\x9cas coded\xe2\x80\x9d ,以便我获得该平台的线程安全程序?
\n在 C++11 之前,语言标准根本不关心多线程,并且不可能创建可移植的(符合语言标准)多线程 C++ 程序。必须使用第三方库,而代码级别的程序线程安全性只能由这些库的内部提供,而这些库又使用相应的平台功能,编译器将代码编译为单个代码- 螺纹。
\n从C++11开始,根据标准:
\nconflict
如果其中一个修改内存位置,而另一个则读取或修改同一内存位置。potentially concurrent
if\n-- 它们由不同的线程执行,或者 \n-- 它们是无序的,至少有一个由信号处理程序执行,并且它们不是都由同一个信号处理程序调用执行;data race
包含两个潜在并发冲突的操作,则程序的执行包含 a,至少其中一个不是原子的,而happens before
另一个也不是,标准中描述的信号处理程序的特殊情况除外([intro.races] 第22节) C++20 点:https://timsong-cpp.github.io/cppwp/n4868/intro.races#22)。data race
结果undefined behavior
。一个atomic
操作与涉及同一对象的任何其他原子操作是不可分割的。\nhappens before
另一个操作意味着第一个操作的内存写入对第二个操作的读取有效。
根据语言的标准,undefined behaviour
只是标准没有提出要求的。
有些人错误地认为undefined behaviour
这只是运行时发生的事情,与编译无关,但标准的作用undefined behaviour
是规范编译,因此在undefined behaviour
.
语言标准并不禁止诊断undefined behaviour
编译器进行诊断。
该标准明确指出,在 的情况下undefined behaviour
,除了忽略不可预测的结果之外,还允许在翻译期间和在执行过程中以环境记录(包括编译器的文档)方式(字面上尽一切可能,尽管有记录)进行行为。执行,并终止翻译或执行(https://timsong-cpp.github.io/cppwp/n4868/intro.defs#defns.undefined)。
因此,编译器甚至可以为undefined behaviour
.
data race
不是对对象的冲突访问实际上同时发生时的状态,而是正在执行对对象甚至具有潜在(取决于环境)冲突访问的代码时的状态(在语言级别上考虑相反的是不可能的,因为由操作引起的硬件对内存的写入可能会在并发代码的范围内延迟未指定的时间(并且请注意,除此之外,操作可能会受到编译器分散在并发代码上的某些限制的限制)和硬件))。
至于undefined behaviour
仅导致某些输入的代码(对于执行可能会发生或不会发生),
as-if
规则(https://en.cppreference.com/w/cpp/language/as_if)允许编译器生成仅对于不会导致的输入正确工作的代码undefined behaviour
(例如,因此问题当输入原因发生时发出诊断消息;发出诊断消息被明确标记为标准中undefined behaviour
允许的一部分);undefined behaviour
请注意,与“潜在”相反(我potential
在这里使用这个词是因为*
下面标记的注释中的内容)data races
,链接中的示例的情况在编译时很容易检测到。
如果编译器可以轻松检测到 a data race
,那么合理的编译器只会终止编译而不是编译任何内容,但是:
一方面,实际上[*]
不可能断定数据竞争一定会在运行时发生,因为在运行时可能会发生单个实例上的所有并发代码实例由于环境原因而无法启动,这使得任何多线程代码先验都可能是单线程的,因此有可能完全避免data races
(尽管在许多情况下它会破坏程序的语义,但这不是编译器关心的问题)。
另一方面,允许编译器注入一些代码,以便data race
在运行时处理 a (注意,不仅对于发出诊断消息等明智的事情,而且以任何(尽管有记录)甚至有害的方式) ,但除此之外,这样的注入将是一个有争议的(即使是合理的)开销:
data races
由于翻译单元的单独编译,某些潜力可能根本无法检测到;data races
根据运行时输入数据,特定执行中可能存在或不存在某些潜力,这将使注入的正确性变得巨大;data races
由于程序代码和逻辑的复杂构造,即使可能,它也可能足够复杂并且成本太高而无法检测。因此,目前编译器甚至不尝试检测data races
.
除了data races
它们本身之外,对于可能发生数据竞争并且以单线程方式编译的代码,还存在以下问题:
as-if
规则(https://en.cppreference.com/w/cpp/language/as_if),如果编译器查找没有差异,则可以消除变量,因为编译器不\xe2\x80\x99t考虑多线程,除非使用该语言及其标准库的特定多线程方法;as-if
和硬件在执行时从 \xe2\x80\x9c 编码的 \xe2\x80\x9d 重新排序,如果看起来没有差异,除非特定的多线程方法使用语言及其标准库,并且硬件可以实现各种不同的方法来限制重新排序,包括对代码中显式对应命令的要求;问题中指出,以下几点并非如此,但为了完成可能问题的设置,理论上在某些硬件上可能存在以下情况:
\n请注意,如果可以使用类型修饰符,则适当使用合理实现的变量修饰符(**
详细信息请参阅下面标记的注释 )可以解决编译器的消除和重新排序问题,但不能通过硬件重新排序,而不是 \xe2 \x80\x9c在缓存中卡住\xe2\x80\x9d。volatile
volatile
[**]
遗憾的是,实际上,该语言的标准说 \xe2\x80\x9c 通过易失性左值进行访问的语义是实现定义的 \xe2\x80\x9d (https://timsong-cpp.github.io/cppwp/ n4868/dcl.type.cv#5 )。\n尽管语言标准指出 \xe2\x80\x9cvolatile
是对实现的提示,以避免涉及对象的激进优化,因为对象的值可能会通过以下方式更改无法被实现检测到。\xe2\x80\x9d ( https://timsong-cpp.github.io/cppwp/n4868/dcl.type.cv#note-5 ),这将有助于避免编译器的消除和重新排序如果volatile
按照其预期目的来实现,即对于代码的环境(例如硬件、操作系统、其他应用程序)可能访问的值是正确的,则正式编译器没有义务按照其预期目的来volatile
实现\n但是,与此同时,标准的现代版本注意到\xe2\x80\x9c此外,对于某些实现,volatile
可能表明需要特殊的硬件指令来访问该对象。\xe2\x80\x9d ( https://timsong-cpp.github.io/cppwp/n4868/dcl.type.cv#note-5),这意味着某些实现还可能实现防止硬件重新排序并防止 \xe2\x80\x9c 卡住\xe2 \x80\x9d 在缓存中,尽管这不是volatile
预期的目的。
可以保证(只要实现符合标准),所有这三个问题以及data races
问题都只能通过使用特定的多线程手段来解决,包括C++标准库中的多线程部分,因为C++ 11.
因此,为了可移植,确认语言的标准,C++
程序必须保护其执行免受任何data races
.
如果编译器编译时就好像代码是单线程的(即忽略data race
),并且合理地实现了(如上面标记为 的注释中所述**
)volatile
修饰符被正确使用,并且不存在由硬件问题引起的缓存和重新排序,则将得到线程安全的机器代码,不使用数据竞争保护(来自环境依赖,不确认从C++11开始的标准,C++代码)。
至于在多个线程的特定环境中使用非原子标志的潜在安全性示例,请在 https://en.cppreference.com/w/cpp/language/storage_duration#Static_local_variables上阅读(自 C++11 起)通常使用 的变体,这将已初始化的本地静态变量的运行时开销减少到单个比较。bool
static local variables
double-checked locking pattern
non-atomic boolean
但请注意,这些解决方案是依赖于环境的,并且由于它们是编译器本身实现的一部分,而不是使用编译器的程序,因此无需担心是否符合那里的标准。
\n为了使您的程序符合语言的标准并受到保护(只要编译器符合标准)免受编译器实现细节自由的影响,您必须保护 a 的标志double-check lock
免受数据竞争,以及最合理的方法,将使用std::atomic
或std::atomic_bool
。
请参阅我的回答帖子/sf/answers/4828210131/关于 C++ 中实现的问题中有关C ++实现的详细信息double-checked locking pattern
(包括在数据竞争中使用非原子标志) C++ 双重检查锁的潜在问题?(请记住,那里的代码在线程中包含多线程操作,这会影响线程中的所有访问操作,触发内存一致性并防止重新排序,因此整个代码先验不会像单线程一样被编译 -螺纹)。double-check lock
归档时间: |
|
查看次数: |
580 次 |
最近记录: |