Just Software Solutions

May 2009 C++ Standards Committee Mailing - Object Lifetimes and Threads

Monday, 18 May 2009

The May 2009 mailing for the C++ Standards Committee was published a couple of weeks ago. This is a minimal mailing between meetings, and only has a small number of papers.

The primary reason I'm mentioning it is because one of the papers is concurrency related: N2880: C++ object lifetime interactions with the threads API by Hans-J. Boehm and Lawrence Crowl. Hans and Lawrence are concerned about the implications of thread_local objects with destructors, and how you can safely clean up threads if you don't call join(). The issue arose during discussion of the proposed async() function, but is generally applicable.

thread_local variables and detached threads

Suppose you run a function on a thread for which you want the return value. You might be tempted to use std::packaged_task<> and std::unique_future<> for this; after all it's almost exactly what they're designed for:

#include <thread>
#include <future>
#include <iostream>

int find_the_answer_to_LtUaE();

std::unique_future<int> start_deep_thought()
{
    std::packaged_task<int()> task(find_the_answer_to_LtUaE);
    std::unique_future<int> future=task.get_future();
    std::thread t(std::move(task));
    t.detach();
    return future;
}

int main()
{
    std::unique_future<int> the_answer=start_deep_thought();
    do_stuff();
    std::cout<<"The answer="<<the_answer.get()<<std::endl;
}

The call to get() will wait for the task to finish, but not the thread. If there are no thread_local variable this is not a problem — the thread is detached so the library will clean up the resources assigned to it automatically. If there are thread_local variables (used in find_the_answer_to_LtUaE() for example), then this does cause a problem, because their destructors are not guaranteed to complete when get() returns. Consequently, the program may exit before the destructors of the thread_local variables have completed, and we have a race condition.

This race condition is particularly problematic if the thread accesses any objects of static storage duration, such as global variables. The program is exiting, so these are being destroyed; if the thread_local destructors in the still-running thread access global variables that have already been destroyed then your program has undefined behaviour.

This isn't the only problem that Hans and Lawrence discuss — they also discuss problems with thread_local and threads that are reused for multiple tasks — but I think it's the most important issue.

Solutions?

None of the solutions proposed in the paper are ideal. I particularly dislike the proposed removal of the detach() member function from std::thread. If you can't detach a thread directly then it makes functions like start_deep_thought() much harder to write, and people will find ways to simulate detached threads another way. Of the options presented, my preferred choice is to allow registration of a thread termination handler which is run after all thread_local objects have been destroyed. This handler can then be used to set the value on a future or notify a condition variable. However, it would make start_deep_thought() more complicated, as std::packaged_task<> wouldn't automatically make use of this mechanism unless it was extended to do so — if it did this every time then it would make it unusable in other contexts.

If anyone has any suggestions on how to handle the issue, please leave them in the comments below and I'll pass them on to the rest of the committee.

Posted by Anthony Williams
[/ cplusplus /] permanent link
Tags: , , ,
Stumble It! stumbleupon logo | Submit to Reddit reddit logo | Submit to DZone dzone logo

Comment on this post

If you liked this post, why not subscribe to the RSS feed RSS feed or Follow me on Twitter? You can also subscribe to this blog by email using the form on the left.

12 Comments

Hopefully I understand your problem description properly...

Why not make it illegal for a thread to be running when the program exits?

I can't think of a case where I would want the thread to run while a program was exiting.

Speaking of detach, is detach still called in the destructor? IMO, this is going to open up a new class of bugs.
by Sohail at 15:00:33 on Monday, 21 January 2019

No, <code>thread::detach</code> is <a href="http://www.justsoftwaresolutions.co.uk/cplusplus/cplusplus-standards-committee-mailing-march-2009.html">not called</a> in <code>thread::~thread</code> anymore.

by fr3@K at 15:00:33 on Monday, 21 January 2019

Removing detach seems a bit heavy handed and I think the repurcussions will surface in additional APIs / objects that folks write.

My personal preference would be to manage the lifetime of a destructible threadpool object that exposed initialization and cleanup callbacks as opposed to forcing folks to always manage the lifetime of individual threads. Others may differ on this of course.

by Rick at 15:00:33 on Monday, 21 January 2019

Good news about ~thread(). Also good to know that people smarter than me agreed with me :-)

by sohail at 15:00:33 on Monday, 21 January 2019

Hi Sohail,

Making it illegal (which implies undefined behaviour) to have a thread running during program exit doesn't help things. In the absence of thread_local variables with destructors, the example code above is fine as the thread exits as soon as the task finishes. The problem is that destructors for thread_local variables prolong the thread, with no way of identifying when the thread has truly finished other than by calling join(). This makes some potentially useful patterns of usage open to undefined behaviour in the presence of thread_local objects with destructors.
by Anthony Williams at 15:00:33 on Monday, 21 January 2019

Hi Rick,

Managing threads with a thread pool would be nice, but the C++ committee have declared thread pools out of scope for C++0x, so we have to deal with the issues of managing threads manually for now.

by Anthony Williams at 15:00:33 on Monday, 21 January 2019

Just a try. What about adding explicit function which will destroy all thread local objects?

by Dmitriy V'jukov at 15:00:33 on Monday, 21 January 2019

Hi Dmitriy,

An explicit function that destroys thread_local objects is one of the proposed solutions in N2880. It would still require careful coding where would you put the call in the example above? find_the_answer_to_LtUaE() might not be run on its own thread, so we can't put the call in there. Once packaged_task::operator() has returned we're no better off than now --- the future is ready, so the main thread can resume and start global destruction.

That leaves packaged_task::operator(). In general, this can be run on any thread at any time, so we can't just blindly put the "destroy thread locals" call in there. Therefore, we must pass in a flag either through the constructor or directly to operator() saying "destroy thread locals please". This is untidy in my opinion, but may be better than other options.

by Anthony Williams at 15:00:33 on Monday, 21 January 2019

But the whole point you were making as I understand it is: * We have a detached thread * Said thread may have thread local variables that have yet to be destructed * We might exit before this happens

"If there are thread_local variables (used in find_the_answer_to_LtUaE() for example), then this does cause a problem, because their destructors are not guaranteed to complete when get() returns. Consequently, the program may exit before the destructors of the thread_local variables have completed, and we have a race condition."

So there are some options: * Before starting destruction of global variables, the library should join() all detached threads * Manually register some de-register functions (no thanks) * Make it a diagnosed error to have a thread running when the program is exiting (my favourite)

I don't see how the third option above does not handle the specific case you were discussing.

"This makes some potentially useful patterns of usage open to undefined behaviour in the presence of thread_local objects with destructors."

What patterns of usage?
by Sohail at 15:00:33 on Monday, 21 January 2019

I agree with Sohail that without a requirement to join all the threads one hardly can find some beautiful and bullet-prof solution. It's a kind of unsolvable problem: you want to not care about thread lifetime, and you want to care about thread lifetime at the same time. And when join is conducted manually, I would prefer: void my_formally_detached_thread() { some_useful_work_here(); std::destroy_thread_local_objects(); notify_my_app_that_I_am_finished(); // substitution for join() }

over: void my_formally_detached_thread() { some_useful_work_here(); std::register_callback_after_destruction_of_thread_local_objects(my_callback); }

void my_callback() { notify_my_app_that_I_am_finished(); // substitution for join() }

by Dmitriy V'jukov at 15:00:33 on Monday, 21 January 2019

Could this be attacked on the future's front? Could the packaged task, for example, register a thread with the future such that the thread that reads the future will automatically join with the task that's responsible for the future? One major disadvantage to this is that I don't believe the current specification for futures have any kind of thread ownership. This approach would also reduce parallelism because the getter would block on the destructors of the setter thread.

Sohail, the problematic usage example for having a diagnosed error if threads are running on exit is exactly the one illustrated in this blog post: the find_the_answer_to_LtUaE() thread is *trying* to shut down, but once it sets the future, it has no way of preventing from exiting first. Similarly, in the spirit of modularity, the main() program has no direct knowledge of that thread and cannot join with it. The committee considered automatically joining all outstanding threads on exit, but decided that the chance of a program hanging indefinitely on exit is near 100%. For example, if a thread is waiting on an event queue, it may be harmlessly left running at the time of exit. To require otherwise would require some kind of cooperative thread termination, which many think is a good idea, but which was nixed for this version of the standard.

by Pablo Halpern at 15:00:33 on Monday, 21 January 2019

I agree that the straight-forward std::destroy_thread_local_objects() call makes Dmitriyy's example simpler than the callback. I can see that in some cases the reverse might be true. We don't have to supply just one solution, but it would be good to have something that worked.

The big issue is that the part of the code that does "notify_my_app_that_I_am_finished()" may be buried inside a class or function that can be called from other places, so you cannot inject code to destroy thread-locals immediately before it, or extract it to a callback. std::packaged_task is actually a case in point --- you cannot easily have it destroy thread-local variables, or pass the thread handle into the returned future as per Pablo's suggestion, since it can be used in cases where it is not being used directly as the thread function.

One option here is to provide parameters to std::packaged_task that indicate this is the desired behaviour, or to provide a std::async function that packages the thread handle into the returned future. Neither option strikes me as ideal, since it limits the options for users, and they can't do the same. We could provide all the options: std::async(), an instrumented std::packaged_task, std::destroy_thread_locals() and std::register_final_callback(), but I'd rather somehow come up with a better alternative.

by Anthony Williams at 15:00:33 on Monday, 21 January 2019

Add your comment

Your name:

Email address:

Your comment:

Design and Content Copyright © 2005-2024 Just Software Solutions Ltd. All rights reserved. | Privacy Policy