Suppose we have the following code:
std::promise<int> promise;
auto future = promise.get_future();
const auto task = [](auto promise) {
try {
promise.set_value_at_thread_exit(int_generator_that_can_throw());
} catch (...) {
promise.set_exception_at_thread_exit(std::current_exception());
}
};
std::thread thread(task, std::move(promise));
// use future
thread.join();
I wonder if this code is correct and safe, and if no, why.
It appears to work fine when compiled with GCC, but crashes (no message is printed) when compiled with MSVC (2017). My guess is that a crash happens because promise
local variable inside task
goes out of scope and is destroyed too early. If I remove _at_thread_exit
suffixes, this code works as expected (or appears to work). It also works correctly when the promise is captured:
const auto task = [p = std::move(promise)]() mutable {
/*...*/
};
Complete compilable example
Why does your code generate problems? Let's start with ansewer to 'when _at_thread_exit
writes to shared state of std::future
and std::promise
?'. It happens after destruction of all thread local variables. Your lambda is called within the thread and after its scope is left, the promise is already destroyed. But what happens when thread calling your lambda has some thread-local variables? Well, the writing will occur after destruction of the std::promise
object. Actually, the rest is really undefined in standard. It seems that passing data to shared state could be done after destruction of std::promise
but information is not really there.
Simplest solution is of course this:
std::promise<int> promise;
auto future = promise.get_future();
const auto task = [](std::promise<int>& promise) {
try {
promise.set_value_at_thread_exit(int_generator_that_can_throw());
} catch (...) {
promise.set_exception_at_thread_exit(std::current_exception());
}
};
std::thread thread(task, std::ref(promise));
// use future
thread.join();
I recently endeavor to understand the mechanism of future-promise and encounter the same problem. I read the source code and C++ standard and finally figure out what happens. The problem doesn't lie in "promise is destructed so garbage memory is accessed", but in its inconsistency.
If xx_at_thread_exit
is called, you shouldn't make std::promise
destruct before the thread exits (i.e. end of passed functor), because it will try to set both value and exception. It seems that MS-STL does what is regulated in the standard while libc++/libstdc++ doesn't.
In C++, a promise represents a worker who "promises to give some result", while a future represents a collector who "wants the future result". They share a shared state, where the result (value or exception) is stored. Also, such a connection is a nonce channel, meaning that each end can only set/get the result once, unless you turn a std::future
into a std::shared_future
to make it able to get multiple times.
There are three cases for the shared state:
std::promise
may call set_value
, set_exception
, set_value_at_thread_exit
and set_exception_at_thread_exit
once.std::promise
calls xx_at_thread_exit
, so it'll be ready when the functor passed to the thread ends (and after all local variables are destructed).set_value
or set_exception
, or xx_at_thread_exit
when the functor completely ends.Particularly, when the promise is destructed before ready, the shared state will be abandoned in dtor, meaning that an exception std::future_error
(broken promise) will be set.
A shared state is just like a std::shared_ptr
held by both std::future
and std::promise
; whoever destructs will make reference count -1, and when the reference count goes to 0, the shared state is deleted.
So now, we know that after calling xx_at_thread_exit
, the result is stored. Making it ready
happens totally in the shared state (the usual implementation will store a condition variable and register such an event on it), which will not be interfered with the destruction of std::promise
. That's why I say the problem doesn't lie in "promise is destructed so garbage memory is accessed".
Then why MS-STL will terminate the program? That's because C++ standard regulates that abandon doesn't check whether it has set the result, but checks whether it's ready. But set_value_at_thread_exit
doesn't make it ready yet, so the exception std::future_error
(broken_promise) will be set.
However, a shared state should have either a value or an exception! MS-STL basically just calls set_exception
, so the stored value makes it throw std::future_error
(promise_already_satisfied). But it's in a noexcept
dtor! So std::terminate
is called and thus the program is aborted.
For libc++/libstdc++, std::promise
will cut off the connection with the stored result once it's set. Thus, the exception won't be set in dtor and the program runs normally. This slightly violates the standard, since it should have both the value and the exception; but then it will violate the future-promise model. So, unless the standard is revised, the implementation of MS-STL is more reasonable for me to both fulfill the standard and the abstract model.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With