如果已知访问顺序是安全的,如何在没有互斥锁的情况下同步线程/CPU

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

本文关键字:情况下 CPU 线程 同步 顺序 访问 安全 如果      更新时间:2023-10-16

请考虑以下事项:

// 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对象/线程访问,它通过引用在服务之间传递,直到该过程完成)。

据我所知,当我在线程之间传递任何东西时,即使没有读/写竞争(如同时访问),我也总是需要某种明确的同步/内存刷新。在这种情况下,正确执行此操作的方法是什么?

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

编辑:似乎 std::atomic_thread_fence 是这项工作的工具,但我无法真正包装"内存模型"概念来安全地编码。我的理解是,此代码需要以下行才能正常工作。真的是这样吗?

// 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描述:

无效连接();

  1. 要求:joinable() 为真。
  2. 效果:阻止直到 由 *this 表示的线程已完成。
  3. 同步: 完成由 *这与 (1.10) 同步的线程 相应的成功 join() 返回

以上显示join()提供了同步(具体来说:由*this表示的线程的完成与调用join()的外线程同步)。下一页 [介绍.多线程]:

  1. 一个评估 A 线程间发生在一个评估 B 之前,如果

(13.1) — A 与 B 同步,或 ...

这表明,由于 a),我们有线程间的完成发生在t join()调用返回之前。

最后,[介绍多线程]:

  1. 如果出现以下情况,则两个操作可能并发

(23.1) — 它们被执行 通过不同的线程,或

(23.2) — 它们是未排序的,至少 一个由信号处理程序执行。

程序的执行 包含数据争用(如果它包含两个潜在的并发) 冲突操作,其中至少一个不是原子的,也不是 发生在另一个之前...

以上描述了数据竞争所需的条件。t.join()的情况不符合这些条件,因为如图所示,t的完成实际上确实发生在join()返回之前。

因此,没有数据竞争,并且所有数据访问都保证了明确定义的行为。

(我想说的是,自从@Smeeheey回答问题以来,你似乎已经以某种重要的方式改变了你的问题;基本上,他回答了你原来措辞的问题,但由于你问了两个不同的问题,所以无法获得信任。这是糟糕的形式 - 将来,请发布一个新问题,以便原始答案者可以按时获得学分。

如果多个线程读取/写入一个变量,即使您知道该变量是按定义的顺序访问的,您仍然必须通知编译器。执行此操作的正确方法必然涉及同步、原子学或记录以执行先验本身之一(例如std::thread::join)。假设同步路由在实现中既明显又不可取..:

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

如果你想坚持发布/获取语义,幸运的是,即使是最简单的原子也可以 - 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->...;
    });
});

有关发布顺序的信息,请参阅此处。