Last week we explored the possibilities of using multi-threading to create animated – or even interactive – loading screens, and drastically decrease loading times.
While we went into a lot of details, using the loading screen of Roche Fusion as an example, there is one important topic we glanced over completely:
What if something goes wrong?
Or more technically: what if one of our threads throws an exception?
If we do not do anything to handle that case and one of our loading threads throws – be it because of a bug, or corrupted content files, which would cause that thread to abort execution without us ever knowing about it.
That is because exceptions trickle up the call stack until they are handled (typically using a
try catch statement), or until they reach the top of the stack. In the latter case they cause their thread to abort execution.
If the thread in question is the application’s main thread, it typically causes the user to see an error message. Other threads however fail silently, and if we do not handle them ourselves, our program might get stuck waiting for the thread to finish execution.
Note that this problem is not unique to loading screens, but can occur in any multi-threaded environment. The solutions below are similarly applicable whenever we are dealing with more than a single thread. The loading screen serves only as a convenient example.
There is a number of solutions to this problem of multi-threaded exception handling.
For example, our main thread can check regularly for whether our subordinate threads have stopped, and whether they have done so with an exception.
A problem with that approach is that we need to know all threads that are running, and make sure to check them all. If we do not do so, a thread may still fail without being noticed.
Alternatively, we can subscribe to application wide events informing us of any unhandled exceptions, so that we can respond to them appropriately.
This approach can not lose track of any threads. However, due to the asynchronous nature of events in a multi-threaded environment, we can not be sure when our event handler’s code is executed, and may run into other threading issues.
Using what we already have
Given the framework we have set up in last week’s post however, I want to introduce another simple solution which avoids both of these problems.
In the design outlined last week, any of our worker threads has the capability to schedule work to run on the application’s main thread. We can use this to have each of our threads catch their own exceptions, and then pass them on to be thrown on the main thread. There they can either cause the application to fail completely, or be handled by the main thread as appropriate.
A simple implementation of this could look as follows:
void startThread(Action threadAction)
() => tryRun(threadAction)
void tryRun(Action Action)
catch (Exception e)
void rethrow(Exception e)
() => throw e
void scheduleOnMainThread(Action action)
actionQueue user here is the an instance of the class developed in this post, that allows us to schedule work to run on our main thread from multiple other threads.
Note that when our given
threadAction throws an exception on our created thread, nothing concrete will happen right away. We simply schedule the exception to be re-thrown on our main thread.
When the main thread gets to executing this scheduled task, it will throw the exception and it can be handled appropriately.
However, while this seems to work great at first, there is one issue with this approach, which will become very clear when we try to use the information contained in the exception to debug what went wrong:
By re-throwing the same exception, the application automatically overrides the stack-trace contained within it.
Without a stack trace however, we do not even know what part of our code threw the original exception.
Luckily there is a very easy way of solving this problem.
Instead of throwing the same exception again, we can create a new one, containing the original one as inner exception – something that is supported by all C# exception.
We modify the code as follows:
void rethrow(Exception e)
() => throw new Exception("A thread threw an exception.", e)
And that is all we have to do to make sure we get all the information contained in the original exception.
Of course – if possible – we should design our application in such a way that such fatal exceptions become exceedingly unlikely.
For example, in Roche Fusion we have a
try-catch block around each script file that is loaded. In case anything goes wrong loading and parsing the file, we simple print a warning to the in-game console, and proceed to load further files.
In most cases this allows the game to run just fine, though some content may not appear as desired.
While it has happened (once), that we missed such a warning and accidentally released a version of the game that could crash under certain circumstances because a needed script file was never loaded successfully, it makes both our development process, and potential modding by our users a much more relaxed experience.
I hope this has given you some insight into how exceptions can be handled in a multi-threaded environment.
If you are interested in the other solutions that I mentioned but did not go into further, I encourage you to search for them online. The various resources on this topic are reasonably exhaustive.
Of course, let me know if you know of other ways to handle exceptions in a setting like this, especially if you think that way is better than the one I am using here!
Enjoy the pixels!