Calling `std::shared_future<T>::wait` from multiple threads
Asked Answered
B

1

12

cppreference is explicit about calling std::shared_future<T>::wait from multiple threads:

Calling wait on the same std::shared_future from multiple threads is not safe; the intended use is for each thread that waits on the same shared state to have a copy of a std::shared_future.

But I can't find a basis for this assertion. There is nothing in the standard marking wait as some kind of special case. While the standard says that the methods on a single shared_future instance are not synchronized, you don't need synchronization as long only const methods are being called:

[17.6.5.9/3] A C++ standard library function shall not directly or indirectly modify objects (1.10) accessible by threads other than the current thread unless the objects are accessed directly or indirectly via the function’s non-const arguments, including this.

There are contradicting answers to be found on SO. Does anyone have an authoritative source on this which explains how calling a const method on a from stdlib could lead to a race condition?

Blotter answered 23/3, 2021 at 14:27 Comment(15)
I would like to add to this question: What about the usual std::future::wait? It is also const, so it should be thread-safe. But std::future is apparently less thread safe than std::shared_future, so the answer for std::shared_future will also have ramifications on std::future.Lefty
Relevant/possible dupe and futures.shared.future/32.9.8.2 "Member functions of shared_­future do not synchronize with themselves, but they synchronize with the shared state"Collenecollet
@LanguageLawyer shared_future doesn't have a set method, only promise does.Lefty
@Lefty indeed :/. Ignoring special member functions, shared_future only has const methods.Vittorio
Exactly! This is why the cited sentence doesn't seem to make much sense. An object with only const methods should always behave thread-safely.Lefty
@Lefty logical constness and bitwise one are two different thing, const is not a guarantee #3830867Marlowe
const is a guarantee for the standard library, see the quoted passage (17.6.5.9/3). const operations are expected not to cause race conditions with other const operations there.Blotter
@AlessandroTeruzzi timsong-cpp.github.io/cppwp/n4861/res.on.data.races#3Vittorio
Looking at gcc-4.6.2 implementation, shared_future wait is calling _M_state->wait() where _M_state is shared_ptr<_State_base>. _State_base wait is a non const function that aquire a mutex. gcc.gnu.org/onlinedocs/gcc-4.6.2/libstdc++/api/…Marlowe
@AlessandroTeruzzi The methods of the shared state are explicitly defined as synchronized by the standard.Blotter
@DimitriVorona which is precisely the issue here, you have multiple threads waiting till a pointer is not null, the method is syncronized, so they are executed one after another. Let's assume the first thread to complete owns the only copy of the shared_future (the others have a pointer to it). What happen if the std::shared_future goes out of scope before the other methods complete?Marlowe
@AlessandroTeruzzi if the only shared_future goes out of scope, then on which objects would the other threads call those methods in the first place?Lefty
@Lefty the method was called before the object went out of scope and the other threads are wait for the mutex to be released.Marlowe
Obviously, the usual lifetime rules apply, but this has nothing to do with thread-safety. Just assume that the lifetime of the threads is shorter than the lifetime of the shared shared_future.Blotter
to nitpick here cppreference doesn't says it is not "thread-safe" but just is not "safe" and using a copy per thread prevent exactly the scenario I have described above.Marlowe
I
0

Calling wait on a shared_future instance from multiple threads should be thread-safe since it is a const method. As your quote from the standard suggests, the standard library guarantees that const means thread-safe. However, what is not safe is calling non-const methods such as operator=, similar to shared_ptr. This may be one reason to always hold a copy of shared_future in each thread to prevent accidentally introducing data races.

For additional context, there once existed an atomic_future, which was essentially an atomic version of shared_future that could be safely shared among multiple threads. According to N2997:

The need for an atomic_future arose from the fact that the move constructor and both assigments made the shared_future much more error prone to use a single instance from multiple threads. But such use is required when more than one thread scans a global list of futures for results provided by worker threads.

However, atomic_future was removed from C++11 in N3194 due to its unsatisfactory interface.

Invaluable answered 29/8, 2024 at 9:15 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.