@SimonSchroeder
Hmm, further complications. I went back to ChatGPT and carefully phrased what we are trying to do/asking about, where we think this case of a boolean and the way it is set/read means we do not need atomicity and can just use, say, a volatile shared variable. The gist of its answer is:
In this situation std::atomic<bool> is required if you want the program to be correct according to the C++ memory model.
The important distinction is between what the hardware happens to do and what C++ guarantees.
In C++, a non-atomic object cannot safely be accessed concurrently this way. That's a data race, and a data race means undefined behaviour.
It doesn't matter that:
[...]
volatile does not fix it. volatile is about observable memory accesses, primarily for things such as memory-mapped hardware; it isn't a thread-synchronisation mechanism.
and concludes:
But atomicity is still required for the read/write relationship.
So the short answer is:
Yes, atomic<bool> is required for a correct C++ program here. No, volatile bool is not a valid replacement. But you absolutely don't need to pay for an atomic load on every iteration — checking it periodically is a very reasonable optimisation for your algorithm.
(It having suggested only calling done.load(std::memory_order_relaxed) once every so many iterations round the loop, just as you & I talked about. I have implemented that for every 256 iterations in each thread and that gives me acceptably similar timing now.)
So I take that it while my non-atomic implementation may appear to work, or may work fine on my particular machine/architecture, it is at least theoretically not allowed ("Undefined Behaviour") under C++ at least.
In a certain sense this is "reassuring"/"simple": instead of having to ponder whether a given "shared" variable and what we do with it (flag, counter, whatever) determines whether I need atomic or not, it seems I just need to use atomic whenever I have cross-thread read/writes.