Consider this
Manager.cpp
struct MyThread : Thread {
MyThread() : Thread ("TroubleMaker") { }
void run () { /* do stuff */ }
};
struct Manager {
Manager () { thread.startThread (); }
~Manager () { thread.stopThread (-1); }
MyThread thread;
};
static Manager manager;
juce_Thread.cpp
...
static RunningThreadsList runningThreads;
...
On application exit, if runningThreads is destroyed before manager, then we get asserts and potential undefined behavior. Some Thread member function implementations depend on the thread existing in the RunningThreadsList, such as Thread::getCurrentThread().
The only robust solution I can think of, is to change RunningThreadsList into a ReferenceCountedObject, created on first use. When a Thread is added or removed from the list, the reference count on the RunningThreadsList object is incremented or decremented respectively.
Furthermore, to keep this object around for RunningThreadsList::getInstance(), we would replace
static RunningThreadsList runningThreads;
with
static ReferenceCountedObjectPtr <RunningThreadsList> runningThreads;
This solves the problem. If there are still threads around when the runningThreads destructor is called (which now just decrements the reference count), then the RunningThreadsList object will survive. Logically, instead of destroying the RunningThreadsList when the static variable at function scope has its destructor called, we will destroy the RunningThreadsList only when all references from running threads are gone AND a possible reference from the ReferenceCountedObjectPtr static variable at function scope gets destroyed.
There are a few other places in Juce where this issue exists. The Juce leak checker is another example. So is DeletedAtShutdown. Now I admit, I’m a pretty hardcore user so the average Juce weenie is unlikely to encounter this issue. Still, we should break this habit of implementing singletons using objects at function scope with static storage duration, it greatly complicates writing library code.