我啥时候真的需要使用 atomic<bool> 而不是 bool? [复制]
Posted
技术标签:
【中文标题】我啥时候真的需要使用 atomic<bool> 而不是 bool? [复制]【英文标题】:When do I really need to use atomic<bool> instead of bool? [duplicate]我什么时候真的需要使用 atomic<bool> 而不是 bool? [复制] 【发布时间】:2013-04-25 14:38:02 【问题描述】:atomic<bool>
不是多余的,因为bool
本质上是原子的吗?我认为不可能有部分修改的 bool 值。我什么时候真的需要使用atomic<bool>
而不是bool
?
【问题讨论】:
你需要atomic<bool>
来避免竞争条件。如果两个线程访问相同的内存位置,并且其中至少一个是写操作,则会发生竞争条件。如果您的程序包含竞争条件,则行为未定义。
@nosid:是的,但是 OP 的意思是他不相信你可以像你一样对 bool 进行 partial 写操作,比如说int
值,您将分别复制该值的每个字节或字。因此,如果写入已经是原子的,则不应存在任何竞争条件。
相关:***.com/questions/5067492/…
如果没有 atomic,则无法保证您将在另一个线程中看到更新,或者您将看到变量更新的顺序与您在不同线程中创建它们的顺序相同.
相关:Do I have to use atomic<bool> for “exit” bool variable?
【参考方案1】:
No C++ 中的类型是“本质上是原子的”,除非它是 std::atomic*
-something。那是因为标准是这样说的。
在实践中,为操纵std::atomic<bool>
而发出的实际硬件指令可能(或可能不)与普通bool
相同,但原子性是一个更大的概念,具有更广泛的影响(例如限制关于编译器重新排序)。此外,一些操作(如求反)在原子操作上被重载,以在硬件上创建与非原子变量的本机、非原子读-修改-写序列截然不同的指令。
【讨论】:
小修正,std::atomic_flag
是唯一的例外,虽然它的名字也是以 atomic 开头的。
@yngccc:我认为这就是为什么 Kerrek SB 写了std::atomic*
而不是std::atomic<*>
。
这个 std::atomic* 包括 std::atomic ?【参考方案2】:
原子操作不仅仅是关于撕裂值,所以虽然我同意你和其他发帖者的观点,我不知道撕裂bool
的环境是可能的,但风险更大。
Herb Sutter 对此发表了精彩的演讲,您可以在线查看。请注意,这是一个漫长而复杂的谈话。 Herb Sutter, Atomic Weapons。问题归结为避免数据竞争,因为它让您产生顺序一致性的错觉。
【讨论】:
【参考方案3】:某些类型的原子性完全取决于底层硬件。每个处理器架构对某些操作的原子性都有不同的保证。例如:
Intel486 处理器(以及之后的更新处理器)保证以下基本内存操作将始终以原子方式执行:
读取或写入一个字节 读取或写入在 16 位边界上对齐的字 读取或写入在 32 位边界上对齐的双字
其他架构对原子操作有不同的规范。
C++ 是一种高级编程语言,致力于将您从底层硬件中抽象出来。出于这个原因,标准根本不允许人们依赖这种低级假设,否则您的应用程序将无法移植。因此,C++ 中的所有原始类型都由符合 C++11 标准的开箱即用标准库提供atomic
对应物。
【讨论】:
另一个关键部分是 C++ 编译器通常被允许将变量保存在寄存器中或优化访问,因为它们可以假设没有其他线程正在更改该值。 (因为数据竞争 UB)。atomic
有点包含volatile
的这个属性,所以while(!var)
不能优化成if(!var) infinite_loop();
。见MCU programming - C++ O2 optimization breaks while loop【参考方案4】:
记住memory barriers。尽管可能无法部分更改bool
,但多处理器系统可能在多个副本中拥有此变量,并且即使在另一个线程将其更改为新值后,一个线程仍可以看到旧值。 Atomic 引入了内存屏障,因此变得不可能。
【讨论】:
关键字volatile
可以修复多处理器问题吗?
没有。易失性与内存栅栏无关。
只是为了清楚起见。 @Vincent 的评论可能源于对 Java 中的关键字 volatile
的理解。 Java 中的 volatile
关键字确实控制内存栅栏,但与 C 中的 volatile
关键字有非常不同的行为,后者不控制。 This question 进一步解释了差异。
为什么原子性与内存排序有关? std::atomic考虑比较和交换操作:
bool a = ...;
bool b = ...;
if (a)
swap(a,b);
在我们读取 a 之后,我们得到了 true,另一个线程可能会出现并设置 a false,然后我们交换 (a,b),所以在退出 b 之后是 false,即使交换已经完成。
使用std::atomic::compare_exchange
,我们可以原子地执行整个 if/swap 逻辑,这样其他线程就不能在 if 和交换之间将 a 设置为 false(没有锁定)。在这种情况下,如果进行了交换,则 b 在退出时必须为 false。
这只是适用于 bool 等二值类型的原子操作的一个示例。
【讨论】:
为什么这是评分最低的答案?这(或 std::atomic_flag 中的 test_and_set)是使用原子 bool 类型的主要原因。【参考方案6】:C++ 的原子类型处理三个 潜在问题。首先,如果操作需要多个总线操作(这可能发生在bool
,取决于它的实现方式),任务切换可能会破坏读取或写入。其次,读取或写入可能仅影响与正在执行操作的处理器关联的缓存,而其他处理器的缓存中可能具有不同的值。第三,如果不影响结果,编译器可以重新排列操作的顺序(约束有点复杂,但现在就足够了)。
您可以自行处理这三个问题中的每一个,方法是假设您正在使用的类型是如何实现的、显式刷新缓存以及使用特定于编译器的选项来防止重新排序(不,@987654323 @ 不会这样做,除非你的编译器文档说这样做)。
但是为什么要经历这一切呢? atomic
会为您解决问题,并且可能比您自己做的更好。
【讨论】:
任务切换不会导致撕裂,除非它需要多个指令来存储变量。整个指令是原子的。单个内核上的中断(它们要么在中断之前完全完成,要么丢弃任何部分工作。这是存储缓冲区的一部分。)在实际同时运行的不同内核上的线程之间更有可能发生撕裂,因为那时是的,您可以通过一条指令完成商店的各个部分之间的撕裂,例如一个未对齐的商店或一个对公共汽车来说太宽的商店。 不,核心不能写入缓存行,除非它拥有该行的独占所有权。 MESI 缓存一致性协议确保了这一点。 (见Can num++ be atomic for 'int num'?)。 C++ 的真正问题是允许编译器假设非原子变量不会被其他线程更改,因此它可以将负载提升到循环之外并将它们保存在 寄存器 中或优化掉。例如将while(!var)
变成if(!var) infloop();
。 atomic
的这一部分类似于 volatile
所做的:总是从内存中重新读取(缓存但一致)。
@PeterCordes — 我没有智慧断言可以运行 C++ 代码的每个可能的硬件架构的行为。也许你会这样做,但这并不意味着你应该复活一个有六年历史的线程。
在需要显式一致性的机器上实现高效的 C++ 实现听起来不太可能,因此当通过确实存在的机制将值保存在寄存器中产生与您正在谈论的相同问题时,弥补这一点很奇怪在所有真正的 CPU 上。这个答案让我感到困扰的是,它无助于消除我们使用的实际系统中关于缓存一致性的常见误解。许多人认为在 x86 或 ARM 上需要某种形式的显式刷新,并且可以从缓存中读取陈旧数据。
如果 C++ 标准完全关心运行多个线程的非连贯共享内存的效率,就会有像释放存储这样的机制,它只使某个数组或其他对象全局可见,而不是 在该点之前的所有其他操作(包括所有非原子操作)。在一致的系统上,发布存储只需等待之前的动态加载/存储完成并提交,而不是写回任何私有缓存的全部内容。其他内核对脏私有缓存的访问是按需进行的。以上是关于我啥时候真的需要使用 atomic<bool> 而不是 bool? [复制]的主要内容,如果未能解决你的问题,请参考以下文章
我需要 std::atomic<bool> 还是 POD bool 足够好?