Asynchronous Operations, Unity, and Threading
Asynchronous Operations (theory)
I’ve been quite interested in asynchronous operations and Unity recently. From my understanding, we typically call these when there’s an operation to be completed, such as a web request or a download, that isn’t going to be completed instantaneously. If we execute something synchronously, we’re waiting for the operation to finish before moving on to another task, whereas executing something asynchronously allows us to move on to another task before it finishes (i.e. it doesn’t bring the program to a complete halt).
I’m drawing very heavily from a Stack Overflow answer with this one (as a lot of this is still fairly new to me), but I believe this is a form of ‘threading’, where a ‘thread’ in programming is a series of commands that exists as a unit of work. An operating system can manage multiple threads or processes by assigning a slice of processor time before switching to another thread to give it time to do some of its work as well. So an operating system running on a single processor can simulate running multiple things at the same time by allocating slices of time to different threads. The main exception to this is when you introduce multiple cores/processors, which can allow the operating system to allocate time to one thread on the first processor and allocate the same block of time to another thread on a different processor. As the answer I’m drawing from says, ‘[a]ll of this is about allowing the operating system to manage the completion of your task while you can go on in your code and do other things.’
Asynchronous Operations (in Unity)
Looking up AsyncOperation in Unity’s Documentation, it appears calling an asynchronous method is about calling a coroutine — a coroutine which we can then, most likely within a coroutine of our own, yield until the operation continues, or manually check whether it’s done, or check its progress. Let’s take a look at some of these in action:
While Loops
In this example, I’m calling the Resources.LoadAsync method, creating and storing a reference to it, and using a while loop which will run for as long as the operation isn’t finished (i.e. while ‘isDone’ is false). I’m also printing some messages to the console to report back on the progress of this operation (specifically, ‘operation.progress’ will return a value between 0 and 1 to tell us where the operation’s currently at). As soon as we get to that final Debug.Log message, I know the operation is complete. I’m not sure a handle to an AsyncOperation lets us do this, but I’m pretty sure there are some asynchronous operations which let us check whether the operation has been successful (i.e. has returned what we wanted) or unsuccessful (e.g. the asset couldn’t be found at that path). This sort of logic can be particularly useful when we need to, say, display the progress of the operation on a loading bar.
Yield Return
We can also typically use a yield statement to essentially wait (within a coroutine) for an asynchronous operation to finish. This can be a quick and easy way of doing just that, if it’s just a matter of waiting for the moment you can perform some further logic.
Event Listener
In this example, I’m adding another method as a listener to the asynchronous operation (via the ‘operation.completed += ObjectLoaded;’ statement). The AsyncOperation will call this ‘completed’ event when the operation’s finished (as long as it has some listeners), so in this case my custom method will then be called. The event expects this method to have an AsyncOperation variable as an argument, so that’s why I’ve added that in the parameters (and we could indeed use this variable to find out more about the operation after it’s completed).
While we could just call an asynchronous operation without any of these options, at the very least it’d make it hard for us to tell when a process has actually been completed. For instance, our scene might depend on a particular ScriptableObject and set of resources to have been successfully loaded in before actually loading and displaying the scene to the player. We could use something like Resources.LoadAsync, AssetBundle.LoadAsync, or (more recently) Addressables.LoadAssetAsync to call these load processes and wait until we’re certain they’ve been completed. In the meantime, the project can be free to show the progress of these to the player or even allow the player to continue as they were while this process happens in the background.
These processes could take a couple of seconds, they could take a couple of minutes, or they could even happen instantaneously; whatever the case, we have the logic in place to patiently wait and respond when ready.