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

标签 c++ thread-safety atomic race-condition stdatomic

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

value 48
value 49
value 50
value 54
value 51
value 52
value 53

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

我曾经认为我可以使用不带互斥量的 std::atomic 来解决多线程计数器自增问题,但事实并非如此。

我可能误解了什么是原子对象,谁能解释一下?

void
inc(std::atomic<int>& a)
{
  while (true) {
    a = a + 1;
    printf("value %d\n", a.load());
    std::this_thread::sleep_for(std::chrono::milliseconds(1000));
  }
}

int
main()
{
  std::atomic<int> a(0);
  std::thread t1(inc, std::ref(a));
  std::thread t2(inc, std::ref(a));
  std::thread t3(inc, std::ref(a));
  std::thread t4(inc, std::ref(a));
  std::thread t5(inc, std::ref(a));
  std::thread t6(inc, std::ref(a));

  t1.join();
  t2.join();
  t3.join();
  t4.join();
  t5.join();
  t6.join();
  return 0;
}

最佳答案

I used to think I could use std::atomic without a mutex to solve the multi-threading counter increment problem, and it didn't look like the case.

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

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

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

正如@Sopel 和@Shawn 在评论中建议的那样,您可以使用适当的函数之一(如 fetch_add)或运算符重载(如 operator++operator +=。有关详细信息,请参阅 std::atomic documentation

更新

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

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

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

  1. 线程 X 执行上面的步骤 1 到 5,将 a 从 50 递增到 51 并从 a.load() 返回值 51>
  2. 线程 Y 执行上面的步骤 1 到 5,将 a 从 51 递增到 52 并从 a.load() 返回值 52>
  3. 线程 Y 执行 printf() 向控制台发送 52
  4. 线程 X 执行 printf() 向控制台发送 51

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

最后,还有一个问题潜伏在第 6 步。因为 printf() 没有对如果两个线程同时调用 printf() 会发生什么做出任何 promise 时间(至少我认为不会)。

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

注意 documentation for printf提到了 C++17 中引入的锁“……用于在多个线程读取、写入、定位或查询流的位置时防止数据竞争。”在两个线程同时争用该锁的情况下,我们仍然无法判断哪个线程会获胜。

关于c++ - 为什么我的 std::atomic<int> 变量不是线程安全的?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58477740/

相关文章:

java - RestTemplate 线程安全吗?

将指针转换为 _Atomic 指针和 _Atomic 大小

C++ 模板变量赋值困惑

C++ 排序数组 => vector 迭代器不兼容

c++ - vector.resize() 方法在调整大小时是否调用默认元素构造函数?

Qt 使用 QueuedConnection 将两个信号连接在一起

c++ - 对原子类 : memory_order_relaxed 感到困惑

c++ - 多个原始输入窗口接收器

c++ - 发布获取语义以计算平均值

rest - 使用 REST 避免重复的 POST