如果已知访问顺序是安全的,如何在没有互斥体的情况下同步 threads/CPUs?

How to synchronize threads/CPUs without mutexes if sequence of access is known to be safe?

考虑以下因素:

// these services are running on different threads that are started a long time ago
std::vector<io_service&> io_services;

struct A {
  std::unique_ptr<Object> u;
} a;

io_services[0].post([&io_services, &a] {
      std::unique_ptr<Object> o{new Object};

      a.u = std::move(o);

      io_services[1].post([&a] {
            // as far as I know changes to `u` isn't guaranteed to be seen in this thread 
            a.u->...;
          });
    });

实际代码将结构传递给一堆不同的 boost::asio::io_service 对象,并且结构的每个字段由不同的服务对象填充 (永远不会从不同的 io_service objects/threads 同时在服务之间通过引用传递,直到处理完成).

据我所知,当我在线程之间传递任何内容时,即使没有 read/write 竞争(如同时访问),我总是需要某种显式 synchronization/memory 刷新。这种情况下正确的做法是什么?

请注意,对象 不属于我,不可复制或移动。我可以使用 std::atomic<Object*>(如果我没记错的话),但我宁愿使用智能指针。有办法吗?

编辑: 看起来 std::atomic_thread_fence 是完成这项工作的工具,但我无法真正包装 'memory model' 概念以安全编码。 我的理解是此代码需要以下几行才能正常工作。真的是这样吗?

// these services are running on different threads that are started a long time ago
std::vector<io_service&> io_services;

struct A {
  std::unique_ptr<Object> u;
} a;

io_services[0].post([&io_services, &a] {
      std::unique_ptr<Object> o{new Object};

      a.u = std::move(o);

      std::atomic_thread_fence(std::memory_order_release);

      io_services[1].post([&a] {
            std::atomic_thread_fence(std::memory_order_acquire);

            a.u->...;
          });
    });

只有在没有同步的情况下才会出现数据竞争时才需要同步。数据竞争被定义为不同线程的无序访问。

您没有这种未排序的访问权限。 t.join() 保证后面的所有语句都严格排在 运行 作为 t 的一部分的所有语句之后。所以不需要同步。

详细说明:(解释为什么 thread::join 具有上述声明的属性)首先,来自标准 [thread.thread.member] 的 thread::join 的描述:

void join();

  1. Requires: joinable() is true.
  2. Effects: Blocks until the thread represented by *this has completed.
  3. Synchronization: The completion of the thread represented by *this synchronizes with (1.10) the corresponding successful join() return.

a).上面说明join()提供了同步(具体:*this代表的线程完成与调用join()的外线程同步)。下一个 [intro.multithread]:

  1. An evaluation A inter-thread happens before an evaluation B if

(13.1) — A synchronizes with B, or ...

这表明,由于 a),我们有 t 线程间的完成发生在 的 return 之前 join()打电话。

最后,[intro.multithread]:

  1. Two actions are potentially concurrent if

(23.1) — they are performed by different threads, or

(23.2) — they are unsequenced, and at least one is performed by a signal handler.

The execution of a program contains a data race if it contains two potentially concurrent conflicting actions, at least one of which is not atomic, and neither happens before the other ...

以上描述了数据竞争的必要条件。 t.join() 的情况不满足这些条件,因为如图所示,t 的完成实际上 发生在 的 return 之前=14=].

因此不存在数据竞争,所有数据访问都保证有明确定义的行为。

(我想说的是,自从@Smeeheey 回答问题以来,您似乎已经以某种重要的方式改变了您的问题;本质上,他回答了您最初措辞的问题,但不能因此得到认可因为你问了两个不同的问题。这是糟糕的形式 - 以后,请 post 一个新问题,这样原始回答者就可以得到应得的信用。)

如果多个线程read/write一个变量,即使知道该变量是按定义的顺序访问的,你必须 仍然通知编译器。执行此操作的正确方法必然涉及同步、原子或记录在案以执行先验之一的内容(例如 std::thread::join)。假定同步路由在实现中既明显又不受欢迎..:[=​​17=]

用原子学解决这个问题可能只包含std::atomic_thread_fence;但是,C++ 中的获取栅栏不能单独 同步 释放栅栏——必须修改实际的原子对象。因此,如果你想单独使用栅栏,你需要指定 std::memory_order_seq_cst;完成后,您的代码将按其他方式工作。

如果你想坚持使用 release/acquire 语义,幸运的是即使是最简单的原子也可以 – std::atomic_flag:

std::vector<io_service&> io_services;

struct A {
  std::unique_ptr<Object> u;
} a;
std::atomic_flag a_initialized = ATOMIC_FLAG_INIT;

io_services[0].post([&io_services, &a, &a_initialized] {
    std::unique_ptr<Object> o{new Object};

    a_initialized.clear(std::memory_order_release); // initiates release sequence (RS)
    a.u = std::move(o);
    a_initialized.test_and_set(std::memory_order_relaxed); // continues RS

    io_services[1].post([&a, &a_initialized] {
        while (!a_initialized.test_and_set(std::memory_order_acquire)) ; // completes RS

        a.u->...;
    });
});

有关发布顺序的信息,请参阅 here