4

In the following code, is using atomics necessary to guarantee race-free semantics on all platforms or does the use the use of a promise.set_value/future.wait imply some kind of implicit memory barrier that would allow me to rely on the flag write having become visible to the outer thread?

std::atomic_bool flag{false}; // <- does this need to be atomic?
runInThreadPoolBlocking([&]() {
    // do something
    flag.store(true);
});
if (flag.load()) // do something


// simplified runInThreadPoolBlocking implementation
template <typename Callable>
void runInThreadPoolBlocking(Callable func)
{
    std::promise<void> prom;
    auto fut = prom.get_future();

    enqueueToThreadPool([&]() {
        func();
        prom.set_value();
    });

    fut.get();
}

In general, are there any "implicit" memory barriers guaranteed by the standard for things like thread.join() or futures?

Milan
  • 3,342
  • 3
  • 31
  • 40
  • See [this question](https://stackoverflow.com/questions/55932983/will-calling-thread-see-modifications-to-local-variables-after-thread-join#55932983) for an answer with regard to thread.join() (The answer is yes for thread.join()) – Alecto Irene Perez May 01 '19 at 08:10
  • I asked a [similar question about constructors](https://stackoverflow.com/questions/47459833/does-the-c-memory-model-provide-guarantees-about-the-operations-of-constructor). – Raedwald May 01 '19 at 11:27
  • As is also already answered; the join() causes a "normal" sequenced-before relationship - the same as you have within a single thread when one instruction is put before another; so in that sense you can see this a "the same" thread and therefore an atomic is not needed. – Carlo Wood May 05 '19 at 08:17

2 Answers2

3

thread.join() and promise.set_value()/future.wait() guarantee to imply memory barriers.

Using atomic_bool is necessary if you don't want the compiler to reorder boolean check or assignment with other code. But in that particular case you can use not atomic bool. That flag will be guaranteed to be true in the moment of check if you don't use it in any other place, as the assignment and check are on the opposite sides of synchronisation point (fut.get()) (forcing the compiler to load real flag value) and the function runInThreadPoolBlocking() is guaranteed to finish only after the lambda is executed.

Quoting from cplusplus.com for future::get(), for example:

Data races


The future object is modified. The shared state is accessed as an atomic operation (causing no data races).

The same is for promise::set_value(). Besides other stuff

... atomic operation (causing no data races) ...

means no one of the conflicting evaluations happens before another (strict memory ordering).

So do all of std:: multithreading synchronization primitives and tools, where you expect some operations to occur only before or after the synchronization point (like std::mutex::lock() or unlock(), thread::join(), etc.).

Note that any operations on the thread object itself are not synchronized with thread::join() (unlike the operations within the thread it represents).

Oliort UA
  • 1,568
  • 1
  • 14
  • 31
-2
std::atomic_bool flag{false}; // <- does this need to be atomic?

Yes.

The call:

prom.get_future()

returns a std::future<void> object.

For the future, the reference says the following:

The class template std::future provides a mechanism to access the result of asynchronous operations:

An asynchronous operation (created via std::async, std::packaged_task, or std::promise) can provide a std::future object to the creator of that asynchronous operation.

The creator of the asynchronous operation can then use a variety of methods to query, wait for, or extract a value from the std::future. These methods may block if the asynchronous operation has not yet provided a value.

When the asynchronous operation is ready to send a result to the creator, it can do so by modifying shared state (e.g. std::promise::set_value) that is linked to the creator's std::future.

Note that std::future references shared state that is not shared with any other asynchronous return objects (as opposed to std::shared_future).

You don't store a 'return' value here so the point is kind of mute, and since there is no other guarentees (and the whole idea is that threads may run in parallel anyway ! ) you need to keep the bool atomic if it's shared !

darune
  • 10,480
  • 2
  • 24
  • 62
  • What do you mean by shared? I think the author of the question asks if the "other" code in his example will be enough to put just `bool` instead of atomic in that particular case. – Oliort UA May 01 '19 at 09:55