Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Lifetime of promise and set_value_at_thread_exit

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

like image 871
Evg Avatar asked Sep 05 '25 07:09

Evg


2 Answers

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();
like image 125
bartop Avatar answered Sep 07 '25 20:09

bartop


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.

TL;DR

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.

Future-Promise model

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:

  • Neither set result, nor ready; std::promise may call set_value, set_exception, set_value_at_thread_exit and set_exception_at_thread_exit once.
  • Set result, but not ready; This is because 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 result, and ready; This is because 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.

Lifetime concern

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.

like image 24
o_oTurtle Avatar answered Sep 07 '25 21:09

o_oTurtle