为什么我的std :: atomic <int>变量不是线程安全的?

Shu*_*eng 1 c++ atomic thread-safety race-condition stdatomic

我不知道为什么我的代码不是线程安全的,因为它输出了一些不一致的结果。

value 48
value 49
value 50
value 54
value 51
value 52
value 53
Run Code Online (Sandbox Code Playgroud)

我对原子对象的理解是,它防止其中间状态暴露出来,因此当一个线程正在读取它而另一线程正在写入它时,它应该解决该问题。

我曾经认为我可以使用没有互斥量的std :: atomic来解决多线程计数器增量问题,但情况并非如此。

我可能误解了原子对象是什么,有人可以解释吗?

value 48
value 49
value 50
value 54
value 51
value 52
value 53
Run Code Online (Sandbox Code Playgroud)

Fra*_*yne 6

我曾经认为我可以使用没有互斥量的std :: atomic来解决多线程计数器增量问题,但情况并非如此。

您可以,但不是您编码的方式。您必须考虑原子访问发生的位置。考虑这一行代码……

a = a + 1;
Run Code Online (Sandbox Code Playgroud)
  1. 首先,a原子获取值。假设获取的值为50。
  2. 我们将该值加1,得到51。
  3. 最后,我们原子地将该值存储到a使用=运算符中
  4. a 最终是51
  5. 我们a通过调用自动加载的值a.load()
  6. 我们通过调用printf()打印刚刚加载的值

到目前为止,一切都很好。 但是在第1步和第3步之间,其他一些线程可能已将a- 的值更改为例如值54。因此,当第3步在其中存储51时,a它将覆盖值54,从而为您提供输出。

正如@Sopel和@Shawn在注释中建议的那样,您可以a使用适当的函数之一(如fetch_add)或运算符重载(如operator ++或)原子地增加值operator +=。有关详细信息,请参见std :: atomic文档

更新资料

我在上面添加了步骤5和6。这些步骤也可能导致看起来不正确的结果。

在步骤3的存储和a.load()步骤5 的调用tp 之间。其他线程可以修改的内容a。在我们的线程a在步骤3中存储51之后,它可能会a.load()在步骤5中发现返回了一些不同的数字。因此,设置a为值51 的线程可能不会将值51传递给printf()

问题的另一个来源是,没有任何东西协调两个线程之间的步骤5.和6.的执行。因此,例如,假设在单个处理器上运行两个线程X和Y。一个可能的执行顺序可能是…

  1. 线程X执行上述步骤1至5,a从50 递增到51,并从中获取值51a.load()
  2. 线程Y执行上述步骤1到5,a从51 递增到52,并从中获取值52a.load()
  3. 线程Y执行printf()向控制台发送52
  4. 线程X执行printf()向控制台发送51

现在,我们在控制台上打印了52,然后是51。

最后,在步骤6中还存在另一个问题。因为printf()如果两个线程同时调用printf()(至少我不认为会发生),则不会做出任何承诺。

在多处理器系统上,上面的线程X和Y可能printf()在两个不同的处理器上恰好在同一时刻(或恰好在同一时刻的几个滴答内)调用。我们无法预测哪个printf()输出将首先出现在控制台上。

注意printf文档中提到了C ++ 17中引入的一个锁“…,当多个线程读取,写入,定位或查询流的位置时,该锁用于防止数据争用。” 对于两个线程同时争夺该锁的情况,我们仍然无法确定哪个线程将获胜。


120*_*arm 5

除了非原子地完成增量之外a,增量后显示的值的获取相对于增量也是非原子的。其他线程之一可能a在当前线程递增之后但在获取要显示的值之前递增。这可能会导致相同的值显示两次,并跳过前一个值。

这里的另一个问题是线程不一定按照它们创建的顺序运行。线程 7 可以在线程 4、5 和 6 之前执行其输出,但在所有四个线程都已递增之后a。由于执行最后一个增量的线程较早地显示其输出,因此最终的输出不是连续的。在可供运行的硬件线程少于六个的系统上更有可能发生这种情况。

在各个线程创建之间添加一个小的睡眠(例如,sleep_for(10))将使这种情况不太可能发生,但仍然不能消除这种可能性。保持输出有序的唯一可靠方法是使用某种排除(如互斥锁)来确保只有一个线程可以访问增量和输出代码,并将增量和输出代码视为必须运行的单个事务在另一个线程尝试进行增量之前一起进行。