quick recap: one example of this problem has been faced when TimerThread has been changed from being DeletedAtShutdown to being managed by a SharedResourcePointer, see here.
The core of the problem is triggered by a sequence of events like this:
- A VST3 plug-in is loaded in a DAW (e.g. REAPER, but in the other topic it was the VST3 helper)
- The plug-in instance creates a static Singleton object that has a
Threadobject. - The plug-in starts that
Threadobject, which in turns starts an underlying native thread. - At that point, while the thread is still running, the DAW is instructed to quit.
- As per exit sequence, all native threads are halted by Windows (but the
Threadobjects that they were underlying to, are still valid) - In the exit sequence, only after all native threads are halted, the static objects are cleaned up. It’s at this point that the destructor for the Singleton static object is called.
- The destructor of Singleton calls
stopThread(-1)on its memberThreadto stop it, before it gets destroyed. stopThread(-1)sees that itsthreadHandlevalue is still not null, thus it thinks the underlying thread is still running, but…- As the thread was halted “natively” by Windows at the very beginning of the exit sequence, the JUCE code hasn’t had a chance to set the
threadHandleto nullptr to reflect that it the thread has stopped. - As a result, the destructor of Singleton waits forever inside
stopThread(-1)
It turns out that, inside that Thread::stopThread(), it is actually possible to determine if the underlying native thread is still running or not, via the GetExitCodeThread() function: if it reports that the underlying native thread is STILL_ACTIVE, then it is still running as normal and the usual signaling to the Thread object that it has to exit should proceed, as usual.
But if GetExitCodeThread (threadHandle) reports that an actual exit code is available (it reports a value that differs from STILL_ALIVE), then it means that we are precisely in the condition described by the sequence above: we still have a valid threadHandle to the underlying native thread, but that thread has been halted by Windows.
Note that the threadHandle that our Thread has is still valid, even if Windows has halted the native thread: I have read in the docs (I’ll search for the exact URL, I cannot find it at the moment) that the underlying native object is only deleted when all the handles to it are closed, and our Thread has not yet closed its threadHandle.
With all the above being said, I have implemented a proof of concept fix directly in Thread::stopThread():
bool Thread::stopThread (const int timeOutMilliseconds)
{
// agh! You can't stop the thread that's calling this method! How on earth
// would that work??
jassert (getCurrentThreadId() != getThreadId());
const ScopedLock sl (startStopLock);
if (isThreadRunning())
{
// --------------- START PATCH - YFEDE -----------------------
#if JUCE_WINDOWS
/* Is the underlying native thread REALLY still running, or has it been halted by Windows,
for example because the DAW was terminated? */
DWORD threadExitCode = 1234; // some value just to see if the call below changes it
const auto [[maybe_unused]] success = GetExitCodeThread (threadHandle, &threadExitCode);
jassert (success != 0);
if (threadExitCode != STILL_ACTIVE)
{
DBG ("Windows has already halted the native thread for juce::Thread with name " + getThreadName ().quoted ());
closeThreadHandle (); // the thread is actually already terminated, so we clean up to reflect that state
return true;
}
#endif
// --------------- END PATCH - YFEDE -----------------------
signalThreadShouldExit();
notify();
if (timeOutMilliseconds != 0)
waitForThreadToExit (timeOutMilliseconds);
if (isThreadRunning())
{
// very bad karma if this point is reached, as there are bound to be
// locks and events left in silly states when a thread is killed by force..
jassertfalse;
Logger::writeToLog ("!! killing thread by force !!");
killThread();
threadHandle = nullptr;
threadId = {};
return false;
}
}
return true;
}
I also have a proof of concept project in place where it shows that this makes a difference and solves the problem, it works both with JUCE 7 and with the most recent JUCE 8 on develop.
If you need to have a look at it, I need to tidy it up a little.
With this patch in place, I think Threads are allowed to be owned by objects with static duration. And as a corollary, maybe that regains us the possibility to also have Timers in objects with static duration as well, which is a feature that has been lost when TimerThread was switched from DeletedAtShutdown to SharedResourcePointer.



