Document Number: | N3070=10-0060 |
Date: | 2010-03-11 |
Author: | Anthony
Williams Just Software Solutions Ltd |
thread_local
VariablesThis 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.
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);
lk
is locked by the calling thread and either
cond
orlk.mutex()
returns the same value for each of the
lock arguments supplied by all concurrently waiting
(via wait
, wait_for
or wait_until
) threads.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]
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:
promise_already_satisfied
if the
associated asynchronous state no_state
if *this
has no
associated asynchronous state.Modify the error conditions for set_exception
in 30.6.5 [futures.promise] p23:
promise_already_satisfied
if the
associated asynchronous state 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();
future_error
if an error condition occurs.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);
future_error
if an error condition occurs.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:
no_state
if *this
has no
associated asynchronous state.promise_already_satisfied
if the
associated asynchronous state Add the following to 30.6.10 [futures.task] following paragraph 26:
void make_ready_at_thread_exit(ArgTypes... args);
*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.future_error
if an error condition occurs.promise_already_satisfied
if its associated
asynchronous state already has a stored value or
exception.no_state
if *this
has no
associated asynchronous state.