Everything that is statically initialised or lazily initialised (for instance, a block-scope static variable whose containing block was entered) gets de-initialised during normal program termination - either by main()
returning or a following a call to exit()
.
The issue isn't so much the sequencing of termination of threads, but rather that there is no effort to stop them at all. Instead, reaping of the (possibly still-running) threads is delegated to the operating system to sort out when the process terminates.
Practically speaking, it would be really difficult for an implementation to force termination of threads - detached or otherwise. Aside from anything else, it's a recipe for unpredictable behaviour as these threads are almost invariably blocked on synchronisation objects or system calls, and hold resources (hello deadlocks!). For another, Posix-Threads doesn't provide an API to do so. It's no surprise that threads need to return from their thread-function to exit.
There is finite period of time between main()
returning and the process terminating in which the run-time performs static de-initialisation (in strict reverse order to that of initialisation) as well as anything registered with atexit()
, during which any extant thread can still be running. In a large program, this time can be significant.
If any of those threads happen to accessing a statically initialised object, this is of course undefined behaviour.
I recently spent quite a bit of time tracking down a series of crashes in a large iOS app featuring much C++.
The crashing code looked much like this, with the crash deep in the bowels of std::set<T>::find(const T&)
bool checkWord(const std::string &w)
{
static std::set<std::string> tags{"foo", "bar"};
return (tags.find(w) != tags.end());
}
Meanwhile, on the main thread, there was a call to exit()
several functions down on the stack.
iOS and macOS apps are heavily multi-threaded using Grand Central Dispatch/libdispatch and it turns out that not only are threads still running after main()
exits, but jobs are being executed from background dispatch queues as well.
I suspect it will be a similar situation on many of other systems.
I found no terribly nice solution to the problem other than avoiding block-scope statics in favour for data that required no initialisation.