关于C++内存顺序:如何确保其他线程安全地访问共享资源?

huangapple go评论73阅读模式
英文:

About c++ memory order: how to keep other threads to access common resources safely?

问题

This is my code: Godbolt.

#include <atomic>
#include <iostream>
#include <thread>
#include <vector>

int main(int, char **) {
  volatile bool resource = true;

  std::atomic_bool stop{false};
  std::atomic_int working{0};

  std::thread controller([&]() {
    // Inform all workers resource is going to be destroyed.
    stop.store(true, std::memory_order_relaxed); // #1

    // Wait for all existing workers to finish.
    while (working.load(std::memory_order_relaxed)) { // #2
      std::this_thread::yield();
    }

    // Destroy resource.
    resource = false; // #3
  });

  std::vector<std::thread> workers;
  for (int i = 0; i < 64; ++i) {
    workers.emplace_back([&]() {
      working.fetch_add(1, std::memory_order_relaxed); // #4
      if (stop.load(std::memory_order_relaxed)) { // #5
        working.fetch_sub(1, std::memory_order_relaxed); // #6
        return;
      }
      
      // Access resource
      if (!resource) { // #7
        std::cerr << "Data race detected: resource is not available."
                  << std::endl;
        abort();
      }
      working.fetch_sub(1, std::memory_order_relaxed); // #8
    });
  }

  for (auto &worker : workers) worker.join();
  controller.join();

  std::cout << "no data race detected." << std::endl;
  return 0;
}

The case is like this:

  1. Multiple worker threads accessing a common resource(read-only).
  2. One controller thread will destroy the common resource after informing the workers and wait for the existing workers to finish.

This case describes a common scene: Some workers born at any time, but a controller(resource manager) might intend to destroy the resource at any time. To avoid data race, the controller needs to wait a bit while for current workers to finish and prevent any new workers.

I used several C++ atomics to get it to work. But I have no confidence about the memory ordering, although it seems to work well on my x86 server.

  1. In the above code, I just use the relaxed ordering, which is apparently not enough, but I don't know which ordering is right here.
  2. By the way, are there any websites or tools to test the memory reordering issues among different platforms?
英文:

This is my code: Godbolt.

#include &lt;atomic&gt;
#include &lt;iostream&gt;
#include &lt;thread&gt;
#include &lt;vector&gt;

int main(int, char **) {
  volatile bool resource = true;

  std::atomic_bool stop{false};
  std::atomic_int working{0};

  std::thread controller([&amp;]() {
    // Inform all workers resource is going to be destroyed.
    stop.store(true, std::memory_order_relaxed); // #1

    // Wait for all existing workers to finish.
    while (working.load(std::memory_order_relaxed)) { // #2
      std::this_thread::yield();
    }

    // Destroy resource.
    resource = false; // #3
  });

  std::vector&lt;std::thread&gt; workers;
  for (int i = 0; i &lt; 64; ++i) {
    workers.emplace_back([&amp;]() {
      working.fetch_add(1, std::memory_order_relaxed); // #4
      if (stop.load(std::memory_order_relaxed)) { // #5
        working.fetch_sub(1, std::memory_order_relaxed); // #6
        return;
      }
      
      // Access resource
      if (!resource) { // #7
        std::cerr &lt;&lt; &quot;Data race detected: resource is not available.&quot;
                  &lt;&lt; std::endl;
        abort();
      }
      working.fetch_sub(1, std::memory_order_relaxed); // #8
    });
  }

  for (auto &amp;worker : workers) worker.join();
  controller.join();

  std::cout &lt;&lt; &quot;no data race detected.&quot; &lt;&lt; std::endl;
  return 0;
}

The case is like this:

  1. Multiple worker threads accessing a common resource(read-only).
  2. One controller thread will destroy the common resource after informing the workers and wait for the existing workers to finish.

This case describes a common scene: Some workers born at any time, but a controller(resource manager) might intend to destory the resource at any time. To avoid data race, the controller need to wait a bit while for current workers to finish and prevent any new workers.

I used several c++ atomics to get it work. But i have no confidence about the memory ordering although it seems to work well on my x86 server.

  1. In the above code, i just use the relaxed ordering which is apparently not enough, but i don't know which ordering is right here.
  2. By the way, are there any websites or tools to test the memory reordering issues among different platforms?

答案1

得分: 1

#1, #2, #4, #5 需要使用 seq_cst。这是唯一可以防止 StoreLoad 重排序(将加载操作移到存储操作之前)的排序方式。

如果 #2 在 #1 之前重新排序,可能会发生数据竞争。在这种情况下,working.load() 可能会返回 0(所有现有的工作线程都已完成),但然后另一个工作线程启动并立即检查停止标志 (#5),获取值为 false。然后它将继续访问资源,与即将销毁资源的控制线程竞争。

同样,重新排序 #4 和 #5 也会导致潜在的数据竞争。然后,工作线程可能会将停止标志视为 false,但尚未递增 working 以指示它已启动。如果此时控制线程运行,它将继续销毁资源。

另外,#8 需要使用 release,因为它不能在 #7 之前重新排序。我们同样认为 #2 需要使用 acquire,但实际上它已经必须使用 seq_cst,如上所示,这包括了 acquire 的所有属性。

#6 可以保持为 relaxed。任何达到 #6 的线程都不会访问资源,因此它不能参与数据竞争。

如果我以后有时间,我会添加一个正式的证明,证明这些排序方式将消除数据竞争。

英文:

Briefly: #1, #2, #4, #5 need to be seq_cst. This is the only ordering which prevents StoreLoad reordering (moving a load before a store).

We can see that a data race would potentially occur if #2 were reordered before #1. In that case, it could happen that working.load() returns 0 (all existing workers have finished), but then another worker starts and immediately checks the stop flag (#5), getting the value false. Then it will proceed to access the resource, racing with the controller thread which is now about to destroy it.

Likewise, reordering #4 and #5 also results in a potential data race. Then the worker could see the stop flag as false, but not yet have incremented working to indicate that it has started. If the controller runs at this point, it would proceed to destroy the resource.

Also, #8 needs to be release, since it is essential that it not be reordered before #7. We would similarly argue that #2 needs to be acquire, but in fact it already has to be seq_cst as shown above, which includes all the properties of acquire.

#6 can stay as relaxed. Any thread which reaches #6 is not going to access the resource at all, and so it cannot be involved in a data race.

If I get some time later, I will add a formal proof that these orderings would eliminate the data race.

huangapple
  • 本文由 发表于 2023年6月29日 17:20:22
  • 转载请务必保留本文链接:https://go.coder-hub.com/76579736.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定