When Should I Use Std::Thread::Detach

When should I use std::thread::detach?

In the destructor of std::thread, std::terminate is called if:

  • the thread was not joined (with t.join())
  • and was not detached either (with t.detach())

Thus, you should always either join or detach a thread before the flows of execution reaches the destructor.


When a program terminates (ie, main returns) the remaining detached threads executing in the background are not waited upon; instead their execution is suspended and their thread-local objects destructed.

Crucially, this means that the stack of those threads is not unwound and thus some destructors are not executed. Depending on the actions those destructors were supposed to undertake, this might be as bad a situation as if the program had crashed or had been killed. Hopefully the OS will release the locks on files, etc... but you could have corrupted shared memory, half-written files, and the like.


So, should you use join or detach ?

  • Use join
  • Unless you need to have more flexibility AND are willing to provide a synchronization mechanism to wait for the thread completion on your own, in which case you may use detach

Why must one call join() or detach() before thread destruction?

Technically the answer is "because the spec says so" but that is an obtuse answer. We can't read the designers' minds, but here are some issues that may have contributed:

With POSIX pthreads, child threads must be joined after they have exited, or else they continue to occupy system resources (like a process table entry in the kernel). This is done via pthread_join().
Windows has a somewhat analogous issue if the process holds a HANDLE to the child thread; although Windows doesn't require a full join, the process must still call CloseHandle() to release its refcount on the thread.

Since std::thread is a cross-platform abstraction, it's constrained by the POSIX requirement which requires the join.

In theory the std::thread destructor could have called pthread_join() instead of throwing an exception, but that (subjectively) that may increase the risk of deadlock. Whereas a properly written program would know when to insert the join at a safe time.

See also:

  • https://en.wikipedia.org/wiki/Zombie_process
  • https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessa
  • https://docs.microsoft.com/en-us/windows/win32/procthread/terminating-a-process

What happens to a detached thread when main() exits?

The answer to the original question "what happens to a detached thread when main() exits" is:

It continues running (because the standard doesn't say it is stopped), and that's well-defined, as long as it touches neither (automatic|thread_local) variables of other threads nor static objects.

This appears to be allowed to allow thread managers as static objects (note in [basic.start.term]/4 says as much, thanks to @dyp for the pointer).

Problems arise when the destruction of static objects has finished, because then execution enters a regime where only code allowed in signal handlers may execute ([basic.start.term]/1, 1st sentence). Of the C++ standard library, that is only the <atomic> library ([support.runtime]/9, 2nd sentence). In particular, that—in general—excludes condition_variable (it's implementation-defined whether that is save to use in a signal handler, because it's not part of <atomic>).

Unless you've unwound your stack at this point, it's hard to see how to avoid undefined behaviour.

The answer to the second question "can detached threads ever be joined again" is:

Yes, with the *_at_thread_exit family of functions (notify_all_at_thread_exit(), std::promise::set_value_at_thread_exit(), ...).

As noted in footnote [2] of the question, signalling a condition variable or a semaphore or an atomic counter is not sufficient to join a detached thread (in the sense of ensuring that the end of its execution has-happened-before the receiving of said signalling by a waiting thread), because, in general, there will be more code executed after e.g. a notify_all() of a condition variable, in particular the destructors of automatic and thread-local objects.

Running the signalling as the last thing the thread does (after destructors of automatic and thread-local objects has-happened) is what the _at_thread_exit family of functions was designed for.

So, in order to avoid undefined behaviour in the absence of any implementation guarantees above what the standard requires, you need to (manually) join a detached thread with an _at_thread_exit function doing the signalling or make the detached thread execute only code that would be safe for a signal handler, too.

Std thread detach

[basic.start.main]/5 A return statement in main has the effect of leaving the main function (destroying any objects with automatic storage duration) and calling std::exit with the return value as the argument. If control flows off the end of the compound-statement of main, the effect is equivalent to a return with operand 0.

[support.start.term]/9

[[noreturn]] void exit(int status);

Effects:

...

  • Finally, control is returned to the host environment.

You seem to expect that when main returns, the program waits for all threads to finish - in effect, implicitly joins all detached threads. That's not what happens - instead, the program terminates, and the operating system cleans up resources allocated to the process (including any threads).

Why program is terminated if neither calls std::thread::detach nor std::thread::join?

This was the subject of much debate pre-C++11.

Your question makes a bold assumption: that detachment is obviously the correct behavior. But you never substantiate it. Indeed, there are many arguments against this idea, and the committee considered them.

I'll take this example from the paper outlining the argument against it:

int fib(int n) {
if (n <= 1) return n;
int fib1, fib2;

std::thread t([=, &fib1]{fib1 = fib(n-1);});
fib2 = fib(n-2);
if (fib2 < 0) throw ...
t.join();
return fib1 + fib2;
}

Once you start throwing exceptions around, the default detachment behavior stops being so useful. Indeed, you can imagine a more complex case, where the exception comes from something non-local to the thread creation routine. Consider this example from a later paper:

      std::vector<std::pair<unsigned int, unsigned int>> partitions =
utils::partition_indexes(0, size-1, num_threads);
std::vector<std::thread> threads;

LOG(LOG_DEBUG, "controller::reload_all: starting reload threads...");
for (unsigned int i=0; i<num_threads-1; i++) {
threads.push_back(std::thread(reloadrangethread(this,
partitions[i].first, partitions[i].second, size, unattended)));
}

LOG(LOG_DEBUG, "controller::reload_all: starting my own reload...");
this->reload_range(partitions[num_threads-1].first,
partitions[num_threads-1].second, size, unattended);

LOG(LOG_DEBUG, "controller::reload_all: joining other threads...");
for (size_t i=0; i<threads.size(); i++) {
threads[i].join();
}

push_back can fail due to lack of memory for reallocating the array. If that happens, you lose access to all of those threads, and your program is broken.

Both of these scenarios lead to a broken program, whether you default to detach or default to terminate. But if the program is going to be broken, it's best that it be broken immediately when the issue occurs, rather than at some later location in the code.

Now, the safer solution is to join in the destructor. But that didn't happen for various other reasons. The (unfortunate) consensus was that, if you didn't say what you want to do, then your code is broken and should blow up.

Fortunately, C++20 gave us std::jthread, which joins by default in its destructor.

The general idea of std::thread's destructor behavior is very simple:

  1. The user didn't say what to do (ie: didn't call join or detach).
  2. Neither answer is obviously the right one.

You claim that simply doing a detach is the right solution. But why is it right? Detachment is a very unsafe thing to do since you lose the ability to join with the thread ever again.

There's also the issue of RAII. An exception could cause some thread object to be destroyed unintentionally. If that happens, and the default behavior is to detach, is your program still in a functional state? What if the rest of your program was expecting to join those threads, and now that's impossible?

How to reattach thread or wait for thread completion before exiting

Assuming that the IO thread is coded by you, you can handle this with a combination of std::promise and std::future, something like this:

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

using namespace std::chrono_literals;

void demo_thread (std::promise <bool> *p)
{
std::cout << "demo thread waiting...\n";
std::this_thread::sleep_for (1000ms);
std::cout << "demo thread terminating\n";
p->set_value (true);
}

int main ()
{
std::promise <bool> p;
std::thread t = std::thread (demo_thread, &p);
t.detach ();

// ...

std::cout << "main thread waiting...\n";
std::future <bool> f = p.get_future();
f.wait ();

std::cout << "main thread terminating\n";
}

Live demo

Why can detached thread in C++11 execute even if the destructor has been called

In C++, std::thread does not manage the thread of execution itself. C++ does not have controls for managing the thread of execution at all.

std::thread manages the thread handle - the identifier of a thread (thread_t in Posix world, which was largely a model for std::thread). Such identifier is used to communicate (as in control) with the thread, but in C++, the only standard way of communication would be to join the thread (which is simply waiting for thread's completion) or detaching from it.

When std::thread destructor is called, the thread handle is also destructed, and no further controlling of the thread is possible. But the thread of execution itself remains and continues being managed by implementation (or, more precisely, operation system).

Please note, for non-detached threads std::threads destructors throws an exception if the thread has not been joined. This is simply a safeguard against developers accidentally loosing the thread handle when they didn't intend to.



Related Topics



Leave a reply



Submit