Me being an idiot wasn't referring to the sanity check, it was that, knowing how recursion works, this eventuality never even crossed my mind.
But the 12-second tween indeed wasn't a deadlock. However, deadlocks most certainly can (and DO) happen with this setup.
Here's the implementation of
join():
mini.Threads.join = function(threadIDs)
{
threadIDs = threadIDs instanceof Array ? threadIDs : [ threadIDs ];
if (!this.isInitialized)
Abort("mini.Threads.join(): must call mini.initialize() first", -1);
var isFinished = false;
while (!isFinished) {
this.doFrame();
isFinished = true;
for (var i = 0; i < threadIDs.length; ++i) {
isFinished = isFinished && !this.isRunning(threadIDs[i]);
}
}
};
Note that it loops until the requested thread(s) finish. This works well enough--if there is only one outstanding join. Trouble is, Scenario is often waiting on many things at once. At this point, the recursive system breaks down.
Let's simplify things a bit and say we have four running threads, call them A, B, C and D.
* A joins B, which prevents A from being updated until B terminates. So far so good.
* .join() is running its own loop, so B, C, and D keep updating.
* At some point (A is still waiting on B to finish), C joins D.
* B terminates. A's join now can't be satisfied because, by definition of recursion, it is itself blocked by C's join. D may not terminate until some way down the line, which means it's unintentionally holding up the works.
And yes, this system can give rise to a deadlock very easily. In the simplest case, this will do it:
* A joins B.
* B joins A.
In practice, such cycles will be (and were!) much more roundabout and therefore difficult to debug.
Note that, due to the way the system is designed, the effects of this may not be immediately obvious as other threads will continue to update (just like preemptive threading!). However, if what A is trying to accomplish is necessary for the game to continue (hence why B wanted to wait on it in the first place), this will put the game in an unrecoverable state.