【问题标题】:Lock-free programming: how fresh is atomic value?无锁编程:原子值有多新鲜?
【发布时间】:2016-11-26 14:17:44
【问题描述】:

一个原子变量在多个并发运行的线程之间共享。据我所知,线程可以读取它执行宽松负载的陈旧值:

x.load(std::memory_order_relaxed)

如果我们使用发布-获取同步会怎样?据我从文档中得知,执行获取的线程保证可以看到释放线程写入变量的内容。

// Thread 1:
x.store(33, std::memory_order_release);

// Thread 2:
x.load(std::memory_order_acquire)

在这种情况下线程 2 是否总是看到新的值?

现在,如果我们将执行宽松存储的第三个线程添加到前面的示例中,线程 2 可能看不到更新,因为仅在线程 1 和 2 之间建立了同步。我说的对吗?

最后,据说读-修改-写操作总是对新值进行操作。这是否意味着它们强制线程“看到”其他线程所做的更新,所以如果我们在读-修改-写操作之后加载,我们将看到至少与该操作一样新鲜的值?

// Thread 1:
x.fetch_add(1, std::memory_order_relaxed); // get the fresh value and add 1
if (x.load(std::memory_order_relaxed) == 3) // not older than fetch_add
    fire();

// Thread 2:
x.fetch_add(2, std::memory_order_relaxed); // will see thread 1 modification
if (x.load(std::memory_order_relaxed) == 3) // will see what fetch_add has put in x or newer
    fire();

对于x 最初为零,我可以确定fire 在这种情况下至少会被调用一次吗?我所有的测试都证明它可以工作,但也许这只是我的编译器或硬件的问题。

我也对订购感到好奇。 x 修改和加载之间存在明显的依赖关系,因此我认为这些指令不会被重新排序,尽管指定了宽松的顺序。还是我错了?

【问题讨论】:

  • 你不知道不同线程的时序。如果线程 1 写入一个变量而线程 2 读取该变量,那么您不知道线程 2 是读取 before 还是 after 写入。 Atomic 可能有助于确保线程 2 在读取期间不会读取并获得新旧值的混合;就是这样。
  • 你指的是问题的哪一部分?
  • re:最后一段:运行时和编译时的内存重新排序,以及乱序执行,都保留了单个线程的行为。所有这些东西的黄金法则(包括编译器的“as-if 规则”)是“不要破坏单线程代码”。但是,x.fetch_add 的存储部分可以在x.load 之后对其他 线程全局可见。它不会在 x86 上,因为 x86 不会对稍后从同一地址加载的存储重新排序(但允许对其他地址进行 StoreLoad 重新排序)。
  • @PeterCordes 所以你的意思是 1) 不会重新排序 2) 来自线程 1 的 x.fetch_add 不会对来自线程 2 的 x.load 可见?但是fetch_add 的负载部分呢?据我所知,它应该看到新鲜的价值。只要我们在同一线程的fetch_add 之后调用loadload 也应该看到fetch_add 放在那里的内容。另一个线程可能会在x.fetch_addx.load 之间再次启动并修改x,但在这种情况下似乎并不重要,因为该线程也会加载和检查。
  • 这是个好问题。我知道答案(fire() 至少会运行一次),但我花了很长时间才写出来,因为我花了很长时间才让自己满意,我对为什么有正确的解释>(在单个线程中排序,不适用于外部观察者)。还要确保它适用于 C++,而不仅仅是像 x86 这样的强排序架构。

标签: multithreading c++11 concurrency atomic lock-free


【解决方案1】:

关键是memory_order_relaxed 意味着fetch_add 不会将加载/存储命令到任何其他 位置。它会做任何必要的原子操作,仅此而已。但是,单线程内的依赖排序仍然适用。


为了使fetch_add 是原子的,它必须防止任何其他尝试同时执行的fetch_add 对其正在读取的相同输入值进行操作。几乎所有东西都使用MESI cache-coherence protocol 的派生词,因此在实践中,原子的 fetch_add 是通过从读取到写入将高速缓存行“锁定”在 M 状态来完成的。 (或者使用 LL/SC,检测到这没有发生并重试。)另请参阅 Can num++ be atomic for 'int num'? 以获取有关 x86 的更详细说明。

C++ 没有具体说明它的实现方式,但了解 C++ 试图公开的硬件操作类型可能会很有用。


对于x 最初为零,我可以确定在这种情况下fire 至少会被调用一次吗?

是的,它至少会运行一次。它可以运行两次,因为两次加载都可以看到第二个fetch_add 的结果。

负载总是看到x 的值,该值至少已由 fetch_add 在其自己的线程中更新。 memory_order_relaxed 不允许线程观察自己的操作发生乱序, 并且 fetch_adds 确实以某种顺序发生。所以至少“排在第二位”的线程会看到x == 3

无法预测订单将是什么。您可以通过查看fetch_add 的返回值来观察它,而不是使用单独的负载。 (此代码不遵守fetch_adds 建立的顺序,因为多个线程可以获得相同的值。为此,您需要将值捕获为单个原子操作的一部分。)


我也对订购感到好奇。 x 修改和加载之间存在明显的依赖关系,因此我认为尽管指定了放宽顺序,但这些指令不会重新排序。

运行时和编译时的内存重新排序,以及乱序执行,都保留了单个线程的行为。所有这些东西的黄金法则(包括编译器的“as-if 规则”)是“不要破坏单线程代码”。

但不能保证这些操作对其他线程全局可见的顺序。 x.fetch_add 的存储部分可以在x.load 之后对其他 线程全局可见。它不会在 x86 上,因为 x86 不会对从同一地址稍后加载的存储重新排序(但允许对其他地址进行 StoreLoad 重新排序,even when a store and load partially overlap。)

第三个线程可能会看到 T1 和 T2 的操作以不同于 T1 看到 T2 的操作的顺序发生。 (即不必有一个所有线程都同意的总顺序,除非您使用顺序一致性。)

请注意,“观察”负载变为全局可见只能间接地进行:通过查看线程完成的其他存储,以确定必须加载的值。但是负载是订购的一个真实而重要的部分。

因此,如果fire() 将某些内容写入内存,则第 3 个线程可能会看到这种情况发生,而它仍会看到 x==0只有当它(对线程 3)看起来像 T1 的负载发生时才有可能在任一线程中的 fetch_add 之前。即使线程 3 使用 x.fetch_add(mo_relaxed) 来观察 x 中的值,C++ 也允许这样做。但就像我说的,你不会在 x86 上看到它。


请注意,“依赖排序”是一个具有另一种含义的短语。即使在弱排序的 CPU 上,加载指针然后取消引用它也可以保证 LoadLoad 重新排序不会在两者之间发生。只有一个架构没有这样做:Alpha 需要一个屏障才能工作。这是memory_order_consumememory_order_relaxed 分开的原因之一。

您可以使用memory_order_consume 而不是memory_order_acquire 以更便宜地与mo_release 生产者同步,如果它正在发布一个指针。 (编译器通常只是加强消费来获取,因为它很难实现。)

依赖排序的mo_consume 含义适用于两个不同的内存位置:指针和缓冲区。这与线程总是按顺序查看自己的操作这一事实完全不同。


当只有一个对象时,线程将始终看到至少与其最近的存储一样新的数据。 (按照它观察操作的顺序是新的,而不是绝对时间中的新!这只是意味着一个线程不能存储一个值,然后让它的下一次读取发现该值从已经存储的存储中恢复为一个在 fetch_add 之前看到。)

很难对这些东西进行推理(例如,其他线程看到的全局顺序,与相关线程之一看到的顺序)。列举所有可能或不可能的令人惊讶的重新排序也很棘手。

标签 wiki 中有一些链接。我特别推荐Jeff Preshing's blog。他解释得很好,并且发表了许多好文章。

【讨论】:

  • 感谢您如此详尽的解释!尽管我仍然觉得有些地方难以理解(例如,加载和取消引用指针,因为我记得有人告诉我指出的数据可能不是最新的,尽管指针是 - 请参阅 #2 here),但总的来说,你已经清除了我的事情。我会从你的链接中阅读更多内容。
  • @mentalmushroom:要让 load+deref 工作,您必须在生产者中使用发布存储。我忘了提这个!但是你只需要在消费者中mo_consume,而不是mo_acquire
猜你喜欢
  • 1970-01-01
  • 2015-09-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-12-10
  • 2018-12-29
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多