【问题标题】:C++20: How is the returning from atomic::wait() guaranteed by the standard?C++20:标准如何保证 atomic::wait() 的返回?
【发布时间】:2022-01-10 15:39:57
【问题描述】:

这是一个语言律师问题。

首先,下面代码中的a.wait() 是否总能返回?

std::atomic_int a{ 0 };

void f()
{
    a.store(1, std::memory_order_relaxed);
    a.notify_one();
}
int main()
{
    std::thread thread(f);

    a.wait(0, std::memory_order_relaxed);//always return?

    thread.join();
}

我相信标准的意图是a.wait() 总是可以返回。 (否则atomic::wait/notify就没有用了,不是吗?)但我认为目前的标准文本不能保证这一点。

标准的相关部分在 §31.6 [atomics.wait] 第 4 段:

如果存在副作用 X 和Y 上 M 这样:

  • (4.1)——原子等待操作在观察X的结果后阻塞,
  • (4.2) — 在M 的修改顺序中,X 在 Y 之前,并且
  • (4.3) — Y 发生在调用原子通知操作之前。

和§31.8.2 [atomics.types.operations] 第 29~33 段:

void wait(T old, memory_order order = memory_order::seq_cst) const volatile noexcept;

void wait(T old, memory_order order = memory_order::seq_cst) const noexcept;

效果:按顺序重复执行以下步骤:

  • (30.1) - 评估 load(order) 并将其值表示与 old 的值表示进行比较。
  • (30.2) — 如果它们比较不相等,则返回。
  • (30.3) — 阻塞,直到它被原子通知操作解除阻塞或被虚假解除阻塞。

void notify_one() volatile noexcept;

void notify_one() noexcept;

效果:如果存在任何此类原子等待操作,则取消阻止至少一个有资格通过此调用解除阻塞 (31.6) 的原子等待操作的执行。

通过上述措辞,我看到了两个问题:

  1. 如果wait()线程在步骤(30.1)中看到了值,则将其与步骤(30.2)中的old进行比较,并被调度出去;然后在另一个线程notify_one() 介入,没有看到阻塞线程,什么都不做;步骤 (30.3) 中的后续阻塞将永远不会被解除阻塞。这里不是需要标准说“wait() function atomically执行评估-比较-块操作”,类似于condition_variable::wait()的说法吗?
  2. notify_*() 和解除阻止 wait() 之间没有同步。如果在步骤 (30.3) 中,线程被原子通知操作解除阻塞,它将重复步骤 (30.1) 以评估 load(order)。这里没有什么可以阻止它获得旧值。 (或者有吗?)然后它会再次阻塞。现在没有人会唤醒它。

以上问题只是吹毛求疵,还是标准的缺陷?

【问题讨论】:

  • @NateEldredge,我认为f 中的store(1) 对应于标准中的Y。它在notify 之前排序,因此发生在notify 之前。
  • 哦,完全正确。我看错了。
  • 那么我看不出有什么问题。 wait 是一个原子等待操作,如果它加载 0,那么它就有资格被通知解除阻塞。因此,通知实际上必须取消阻止它。这是对wait 的整个调用,这是一个原子等待操作,而不仅仅是它的第 3 步。
  • @NateEldredge 问题 №2 怎么样?
  • 2 号比较棘手,我同意...

标签: c++ multithreading language-lawyer atomic c++20


【解决方案1】:

#1 几乎是由C++20 thread possibly waiting on std::atomic forever 解决的。 wait() 操作显然可以被notify() 解锁,并且是唯一这样的操作,所以notify() 必须解锁它。符合条件的“等待操作”是整个调用,而不仅仅是步骤 30.3。

如果实现以非原子方式执行步骤 30.1-3,以便通知可以发生在步骤 1 和步骤 3“之间”,那么它必须以某种方式确保步骤 3 无论如何都会解除阻塞。


#2 更粘。在这一点上我认为你是对的:标准不保证第二次加载的值为 1;如果没有,那么它可能会再次阻塞并且永远不会被唤醒。

relaxed 内存排序的使用在此示例中非常清楚。如果我们想证明第二次加载必须看到 1,我能看到的唯一方法是调用读写一致性 (intro.races p18),这要求我们证明存储 发生在加载之前,在 intro.races p10 的意义上。这反过来又要求在此过程中的某个地方,我们在一个线程中进行一些操作,与在另一个线程中进行一些操作(你不能让 线程间发生之前没有 与 同步,除非有消费操作,这里不是这种情况)。通常,您会从获取负载与发布存储(atomics.order p2)的配对中获得同步,而这里我们没有这样的东西;据我所知,也没有其他任何可以同步的东西。所以我们没有证据。

事实上,我认为即使我们升级到seq_cst 操作,问题仍然存在。然后,我们可以在存储之前 coherence-ordered 加载,并且 atomics.order p4 的总顺序 S 将变为“第一次加载,第二次加载,存储”。我不认为这有任何矛盾。我们仍然需要显示一个 synchronizes with 来排除这种情况,但我们又不能这样做。似乎有比relaxed 情况更好的机会,因为seq_cst 加载和存储分别是获取和释放。但是使用它的唯一方法是如果其中一个负载从存储中获取其值,即如果其中一个负载返回 1,我们假设情况并非如此。因此,这种不受欢迎的行为似乎符合所有规则。

这确实让您想知道标准作者是否打算要求通知与解锁同步。这将解决问题,我猜现实生活中的实现已经包含了必要的障碍。

但事实上,我没有在任何地方看到这一点。

我能看到的唯一可能的出路是“有资格被解除阻塞”适用于整个等待操作,而不仅仅是它的单个迭代。但似乎很明显,其意图是如果您被通知解除阻塞并且值没有更改,那么您会再次阻塞,直到发生 second 通知(或虚假唤醒)。

在我看来,它开始像一个缺陷。

【讨论】:

    猜你喜欢
    • 2017-03-15
    • 2022-09-24
    • 1970-01-01
    • 2020-08-03
    • 2021-07-09
    • 2017-01-12
    • 1970-01-01
    • 2014-06-02
    • 2023-03-30
    相关资源
    最近更新 更多