Document Number:N3070=10-0060
Date:2010-03-11
Author:Anthony Williams
Just Software Solutions Ltd

N3070 - Handling Detached Threads and thread_local Variables

This paper replaces my earlier paper N3038. It provides a simpler and more focused solution to the problem of the interactions of thread_local variables and detached threads.

As Hans Boehm and Lawrence Crowl pointed out in N2880, detached threads pose a problem for objects with thread storage duration. If we use a mechanism other than thread::join to wait for a thread to complete its work — such as waiting for a future to be ready — then N2880 correctly highlights that under the current working paper the destructors of thread_local variables will still be running after the waiting thread has resumed. This paper proposes a mechanism to make such synchronization safe by ensuring that the objects with thread storage duration are destroyed prior to the future being made ready. e.g.

int find_the_answer(); // uses thread_local objects
void thread_func(std::promise<int> * p)
{
    p->set_value_at_thread_exit(find_the_answer());
}

int main()
{
    std::promise<int> p;
    std::thread t(thread_func,&p);
    t.detach(); // we're going to wait on the future
    std::cout<<p.get_future().get()<<std::endl;
}

When the call to get() returns, we know that not only is the future value ready, but the thread_local variables on the other thread have also been destroyed.

Such mechanisms are provided for std::condition_variable, std::promise and std::packaged_task. e.g.

void task_executor(std::packaged_task<void(int)> task,int param)
{
    task.make_ready_at_thread_exit(param); // execute stored task
} // destroy thread_locals and wake threads waiting on futures from task

Other threads can wait on a future obtained from the task without having to worry about races due to the execution of destructors of the thread_local objects from the task's thread.

std::condition_variable cv;
std::mutex m;
complex_type the_data;
bool data_ready;

void thread_func()
{
    std::unique_lock<std::mutex> lk(m);
    the_data=find_the_answer();
    data_ready=true;
    std::notify_all_at_thread_exit(cv,std::move(lk));
} // destroy thread_locals, notify cv, unlock mutex

void waiting_thread()
{
    std::unique_lock<std::mutex> lk(m);
    while(!data_ready)
    {
        cv.wait(lk);
    }
    process(the_data);
}

The waiting thread is guaranteed that the thread_local objects used by thread_func() have been destroyed by the time process(the_data) is called. If the lock on m is released and reacquired after setting data_ready and before calling std::notify_all_at_thread_exit() then this does NOT hold, since the thread may return from the wait due to a spurious wakeup.

Proposed Wording

Modifications to std::condition_variable

Add the following non-member function following the class definition of condition_variable in 30.5.1:


    void notify_all_at_thread_exit(std::condition_variable& cond,std::unique_lock<std::mutex> lk);

Insert a new paragraph following 30.5.1p37:

    void notify_all_at_thread_exit(std::condition_variable& cond,std::unique_lock<std::mutex> lk);
Requires:
lk is locked by the calling thread and either
  • no other thread is waiting on cond or
  • lk.mutex() returns the same value for each of the lock arguments supplied by all concurrently waiting (via wait, wait_for or wait_until) threads.
Effects:

Transfer ownership of the lock associated with lk into internal storage and schedule cond to be notified when the current thread exits, after all objects of thread storage duration associated with the current thread have been destroyed. This notification shall be as-if

lk.unlock();
cond.notify_all();

[Note: The supplied lock will be held until the thread exits, and care must be taken to ensure that this does not cause deadlock due to lock ordering issues. After calling notify_all_at_thread_exit it is recommended that the thread should be exited as soon as possible, and that no blocking or time-consuming tasks are run on that thread. — End Note]

[Note: It is the user's responsibility to ensure that waiting threads do not erroneously assume that the thread has finished if they experience spurious wake-ups. This typically requires that the condition being waited for is satisfied whilst holding the lock on lk, and that this lock is not released and reacquired prior to calling notify_all_at_thread_exit. — End Note]

Modifications to std::promise and std::packaged_task

Add the following to the class definition of std::promise in section 30.6.5 [futures.promise]:

void set_value_at_thread_exit(const R& r);
void set_value_at_thread_exit(see below);
void set_exception_at_thread_exit(exception_ptr p);

Modify the error conditions for set_value in 30.6.5 [futures.promise] p19:

Error conditions:
  • promise_already_satisfied if the associated asynchronous state is already readyalready has a stored value or exception.
  • no_state if *this has no associated asynchronous state.

Modify the error conditions for set_exception in 30.6.5 [futures.promise] p23:

Error conditions:
  • promise_already_satisfied if the associated asynchronous state is already readyalready has a stored value or exception.
  • no_state if *this has no associated asynchronous state.

Add the following to the end of section 30.6.5 [futures.promise]:

void promise::set_value_at_thread_exit(const R& r);
void promise::set_value_at_thread_exit(R&& r);
void promise<R&>::set_value_at_thread_exit(R& r);
void promise<void>::set_value_at_thread_exit();
Effects:
Stores r in the associated asynchronous state without making the associated asynchronous state ready immediately. Schedules the associated asynchronous state to be made ready when the current thread exits, after all objects of thread storage duration associated with the current thread have been destroyed.
Throws:
future_error if an error condition occurs.
Error conditions:
  • promise_already_satisfied if its associated asynchronous state already has a stored value or exception.
  • no_state if *this has no associated asynchronous state.
void set_exception_at_thread_exit(exception_ptr p);
Effects:
Stores p in the associated asynchronous state without making the associated asynchronous state ready immediately. Schedules the associated asynchronous state to be made ready when the current thread exits, after all objects of thread storage duration associated with the current thread have been destroyed.
Throws:
future_error if an error condition occurs.
Error conditions:
  • promise_already_satisfied if its associated asynchronous state already has a stored value or exception.
  • no_state if *this has no associated asynchronous state.

Added the following member function to the class definition for std::packaged_task in 30.6.10 [futures.task]:

void make_ready_at_thread_exit(ArgTypes...);

Modify the error conditions for operator() in 30.6.10 [futures.task] p25:

Error conditions:
  • no_state if *this has no associated asynchronous state.
  • promise_already_satisfied if the associated asynchronous state is already readyalready has a stored value or exception.

Add the following to 30.6.10 [futures.task] following paragraph 26:

void make_ready_at_thread_exit(ArgTypes... args);
Effects:
INVOKE (f, t1, t2, ..., tN, R), where f is the associated task of *this and t1, t2, ..., tN are the values in args.... If the task returns normally, the return value is stored in the associated asynchronous state, otherwise the exception thrown by the task is stored. In either case, this shall be done without making the state ready immediately. Schedules the associated asynchronous state to be made ready when the current thread exits, after all objects of thread storage duration associated with the current thread have been destroyed.
Throws:
future_error if an error condition occurs.
Error conditions:
  • promise_already_satisfied if its associated asynchronous state already has a stored value or exception.
  • no_state if *this has no associated asynchronous state.