2
votes

My question is about thread synchronization. Please see the code below:

std::vector<int> v_int;

for (size_t i = 0; i < 5; ++i) {
    v_int.emplace_back(i);
}

auto f_async = std::async(std::launch::async,
    [](auto v_int) mutable {
        for (auto& el : v_int.get()) {
            el += 10;
        }
    }, std::ref(v_int));

//more instructions...

f_async.get();

My question is how the new thread spawned by std::async "sees" the modifications done by the (main) thread to the vector, given that there is no acquire-release (mutex, atomic bool, atomic flag...) to protect the vector?

Is there an implicit sequential consistency given that the new thread "happens" after the complete write to the vector?

A typical producer/ consumer would look like this:

std::vector<int> v_int_global;
std::atomic<bool> data_ready{ false };

void producer_int() {
    for (size_t i = 0; i < 5; ++i) {
        v_int_global.emplace_back(i);
    }
    data_ready.store(true, std::memory_order_release);
}

void transformer_int() {
    while (!data_ready.load(std::memory_order_acquire));
    for (auto& el : v_int_global) {
        el += 10;
    }
}

int main() {
    std::thread t1 (producer_int);
    std::thread t2 (transformer_int);

    t1.join();
    t2.join();
}

Thank you.

1
en.cppreference.com/w/cpp/thread/async - see the part about synchronizes-with. I don't have the standard to hand to verify it though.Mike Vine
"Is there an implicit sequential consistency given that the new thread "happens" after the complete write to the vector?" - in the example you've given, yes. Here you start the async task but then immediately call .get on the returned future, which effectively makes this whole example synchronous. If you were to move the code that populates v_int to be between the call to std::async and the call to f_async.get, you would have UB.0x5453

1 Answers

1
votes

std::async is specified as synchronizing with the invocation of the argument ([futures.async]/p5):

Synchronization: Regardless of the provided policy argument,

(5.1) the invocation of async synchronizes with the invocation of f. [ Note: This statement applies even when the corresponding future object is moved to another thread.  — end note ] ; and

(5.2) the completion of the function f is sequenced before ([intro.multithread]) the shared state is made ready. [ Note: f might not be called at all, so its completion might never happen.  — end note ]

The term synchronizes-with means that at least one "acquire-release" event will be taking place. Thus any work done before invoking std::async is guaranteed to be visible to the lambda, regardless of when it is executed.

Similarly, future::get() synchronizes with the call that stores the result in the shared state ([futures.state]/p9):

Calls to functions that successfully set the stored result of a shared state synchronize with calls to functions successfully detecting the ready state resulting from that setting. The storage of the result (whether normal or exceptional) into the shared state synchronizes with the successful return from a call to a waiting function on the shared state.