Unity: Callbacks and Multi-Threading

Even if you’re not explicitly working with multi-threading, Unity’s internal processes might be, and it’s crucial to be able to recognise a threading problem when you encounter it. Unity can execute your code on other threads and this might cause multi-threading-related issues in your code.


Unity employs multi-threading in the serialization, build process and audio playback. That’s the reason why a Unity game can freeze but the audio continues to play.  The game and the audio are running on separate threads.


When you are using Unity callbacks to implement custom importers or a build process your code will likely be executed on a different and/or multiple thread(s). Now you need to consider if your code is multi-threading safe.


One key aspect to be aware of is that the Unity API is not thread-safe, meaning it can’t be accessed outside the main thread. This limitation can lead to strange bugs where variables abruptly change or functions aren’t called.


Real-World Example

Consider a scenario where game objects are disabled as part of a build process to eliminate debug features. The function responsible for this task uses Application.isPlaying.


Surprisingly, the value of isPlayMode, which was false during the call of ShouldBeAlive(), changed to true within the function. This unexpected behaviour is an indication of a threading issue.



Why did this happen?

In this case, Unity executed the script on a worker thread as part of the build process. Outside the main thread Application.isPlaying (Unity API) can’t be accessed. Attempting to access it triggers a warning from Unity, but this notification will not reach you because the logger is also part of the API and inaccessible outside the main thread. It will silently fail or exhibit strange behaviour.


How to fix it?

Fortunately, all threading problems we encountered in our custom importers and build scripts could be solved by capturing the critical values in variables. In the case of the example, assigning the value of isPlayMode to a variable before passing it to the function effectively solved the issue.