多个线程可以安全地同时将相同的值写入同一个变量吗?
Posted
技术标签:
【中文标题】多个线程可以安全地同时将相同的值写入同一个变量吗?【英文标题】:Can multiple threads write the same value to the same variable at the same time safely? 【发布时间】:2020-01-09 01:11:34 【问题描述】:多个线程能否安全地将相同的值同时写入同一个变量?
举个具体的例子——C++ 标准是否保证以下代码在每个符合标准的系统上都可以编译、运行而没有未定义的行为并打印“true”?
#include <cstdio>
#include <thread>
int main()
bool x = false;
std::thread one[&] x = true; ;
std::thread two[&] x = true; ;
one.join();
two.join();
std::printf(x ? "true" : "false");
这是一个理论问题;我想知道它是否总是有效,而不是在实践中是否有效(或者像这样编写代码是否是个好主意:))。如果有人能指出标准的相关部分,我将不胜感激。根据我的经验,它在实践中总是有效的,但不知道它是否能保证有效,我总是使用 std::atomic
代替 - 我想知道这对于这种特定情况是否是绝对必要的。
【问题讨论】:
std 无法定义 MT 程序。故事结束。 @curiousguy 你在说什么?它强烈而严格地做到了for almost a decade。 @LightnessRacesBY-SA3.0 错误。没有解释如何将非线程程序的语义扩展到 MT。所以不仅没有定义MT程序,也没有定义单线程程序。 再次评论警察。 @curiousguy 这毫无意义。 有点奇怪的例子:bool
变量只有两个可能的值。更有趣的情况是使用double
变量,并让两个线程中的每一个都存储不同的值。然后你可以询问最终结果是否保证是两个线程存储的两个值之一,或者它是否可能是初始值,或者它是否可能完全是其他值。
【参考方案1】:
没有。
您需要同步对这些变量的访问,方法是使用互斥锁或使它们原子化。
写入 same 值时没有例外。您不知道编写该值涉及哪些步骤(这是潜在的实际问题),标准也不知道这就是代码具有未定义行为的原因……这意味着您的编译器可以对您的程序造成绝对的混乱(这就是真正你需要避免的问题)。
有人会过来告诉你,这样那样的架构可以保证对这些大小的变量进行原子写入。但这不会改变 UB 方面。
您要查找的段落是:
[intro.races/2]
:如果其中一个修改了内存位置 ([intro.memory]) 而另一个读取或修改了相同的内存位置,则两个表达式求值冲突。
[intro.races/21]
: […] 如果程序的执行包含两个潜在的并发冲突操作 […],则该程序的执行包含 数据竞争。任何此类数据竞争都会导致未定义的行为。
... 以及周围的措辞。该部分实际上非常深奥,但您实际上不需要解析它,因为这是经典的教科书数据竞赛,您可以在任何有关编程的书中阅读。
【讨论】:
为了清楚起见(?),线程一和线程二本身的分配是冲突的。 main 中的赋值和 main 末尾的 read 和线程一和线程二的赋值没有冲突吧? @JeffGarrett Right 不仅仅是“步骤”,这是一个低级问题。存在更高级别的问题,编译器会假设只有在没有其他线程可以使用该对象时才会发生写入。 这就是字面意思,@curiousguy。但是,等一下,我想the standarddidn't specify anything about multi-threaded programs...? @LightnessRacesBY-SA3.0 std 说了很多关于不允许进行数据竞赛的内容。 std 有子句。但是没有任何东西可以定义一个 C++ 程序。因为没有任何东西被定义为连续的或不连续的。声称我们有 MT 语义是一个骗局(您对此深有体会)。没有什么可以让任何人对程序进行推理,因为有可靠的基础。最多您可以声称定义了具有从未创建或销毁的原子和互斥锁的程序……这令人毛骨悚然。但你可以这么说。【参考方案2】:从标准的角度来看,亮度是正确且准确的。
但我会从另一个角度告诉你为什么从硬件架构的角度来看这不是一个好主意。
如果没有内存屏障(原子、互斥体等),您可能会遇到所谓的缓存一致性问题。在多核或多处理器机器上,您的两个线程都可以将x
设置为true
,但您的主线程可能会打印false
,即使您的编译器没有将x
存储到寄存器中。那是因为主线程使用的硬件缓存尚未更新为使x
从它所在的任何缓存行中失效。 C++ 提供的原子类型和锁守卫(以及无数的操作系统原语)就是为了解决这个问题而实现的。
无论如何,谷歌搜索Cache Coherence Problem 和Cache Coherence Multicore。对于如何实现原子事务的特定架构实现,请查看Intel LOCK prefix。
【讨论】:
为了清楚起见,我的回答不仅仅是关于标准,而是关于这些规则的实际效果。 UB 不仅仅是一个理论上的问题:由此产生的症状是真实的,因此,任何其他/低于此的症状通常都可以被认为是没有实际意义的。但是很高兴听到架构在这些情况下会绊倒你的其他方式,如果你设法通过了 UB :) 我认为这个答案不正确。这个答案似乎表明,即使您摆脱了写入x
的线程之一(从而消除了UB),主线程仍然可能无法看到写入的结果。但我不认为这是真的:一个线程的完成同步来自std::thread::join
[link]的相应返回,这意味着在join
调用返回后,主线程应该观察作者线程的所有写入。
不是 fail 看到写入本身的结果。只是在工作线程设置它之后,x
的值的变化可能不会立即在 main 中看到。它可能需要额外的时钟周期。但你可能是对的,join 调用是有效同步主线程的内存屏障。
@ruakh "主线程应该观察来自写入线程的所有写入" 所有不能被另一个线程破坏的最新写入
@curiousguy:问题包括完整的程序; 没有没有“又一个线程”。以上是关于多个线程可以安全地同时将相同的值写入同一个变量吗?的主要内容,如果未能解决你的问题,请参考以下文章
使用volatile修饰int型变量i,多个线程同时进行i++操作,这样可以实现线程安全吗