C++ Logo

std-discussion

Advanced search

Synchronization of atomic notify and wait

From: Nate Eldredge <nate_at_[hidden]>
Date: Mon, 18 Jul 2022 07:48:06 -0700 (PDT)
A question came up on StackOverflow regarding the Standard's language on
atomic wait and notify operations
(https://stackoverflow.com/questions/70228390/c20-how-is-the-returning-from-atomicwait-guaranteed-by-the-standard/70230293#70230293,
asked by user zwhconst).

Consider the following simple test case:

std::atomic<bool> b{false};

void thr1() {
     b.store(true, std::memory_order_relaxed);
     b.notify_one();
}

int main() {
     std::thread t(thr1);
     b.wait(false);
     t.join();
     return 0;
}

Presumably the standard's intention is that this program is guaranteed to
terminate. But as currently worded, I cannot seem to prove it.

Suppose that `b.wait(false)` starts while the value of `b` is still
`false`, and so it blocks. When `b.notify_one()` executes, the main
thread will unblock and test the value of `b`. However, I cannot find
anything to clearly imply that the store of `true` to `b` will be visible
by then. If the main thread loads the old value `false`, it will block
again, and potentially never wake up (unless there is a spurious unblock).

Intuitively, I would think that `notify_one()` ought to synchronize with
the corresponding unblock in `wait()`, but the standard doesn't seem to
actually say so. Without something like that, I can't find any way to
ensure that the store of `true` happens-before the relevant load. Is this
an accidental omission from the standard, or is there some more subtle
argument to ensure it?

We could imagine a perverse implementation of notify/wait with a global
`std::atomic<long> __counter`, in which `b.notify_one()` does
`__counter.fetch_add(1, std::memory_order_relaxed)`. Then `b.wait()` does
a loop of relaxed loads of `__counter` until its value changes, at which
point it reloads and tests the value of `b`. It would then be possible
that the store of `__counter` is reordered before the store of `b`, and
the load of `__counter` is reordered after the load of `b`. As far as I
can tell, this conforms to the letter of the standard, although reality
would tell you that the increment of `__counter` must be release, and the
load of `__counter` must be acquire.

An alternative explanation was offered by user Broothy on SO, which would
have it that `b.notify_one()` is considered a modification of `b` itself,
and therefore participates in the modification order of `b`. If "check
whether to unblock" is likewise a read of `b`, then the actual load of b's
value is sequenced later, and so it must read the earlier value `true`.
But this seems counterintuitive to me, since `b.notify_one()` isn't a
store and doesn't modify the value of `b`, and a typical implementation
would probably not actually touch `b` in any way. If that's really the
logic that was intended, it seems to me that the standard ought to include
some explanation.

-- 
Nate Eldredge
nate_at_[hidden]

Received on 2022-07-18 14:48:10