Safely using async and await in Unity

Clément Couture
Homa Engineering
Published in
3 min readJun 6, 2024

Introduction

This article is for developers that know a little async and await, and want to implement it in Unity. If you are not familiar with it, here is a quick article on how to use them.

Async/await, or the Task Asynchronous Programming (TAP) model, is a really elegant asynchronous programming solution. However, a lot of the complexity is hidden away, and that can lead to unexpected behaviors, especially when working in Unity.

For example, this piece of code would work fine:

HttpClient client = new HttpClient();
var initContent = await client.GetAsync(initUri);

// Do some initialisation
SceneManager.LoadScene(nextSceneIndex);

While this one will most certainly throw an exception:

HttpClient client = new HttpClient();
await client.GetAsync(initUri).ContinueWith(initContentTask =>
{
var initContent = initContentTask.Result;

// Do some initialisation

SceneManager.LoadScene(nextSceneIndex);
});

But to an untrained eye, those two pieces of code can look equivalent. So let’s dive deeper into what is happening behind the scenes.

How it works in C#

Tasks and TaskSchedulers

Tasks were initially not made for the TAP model. Meaning they were designed outside the async/await ecosystem. They were created before, and work as wrappers for generic asynchronous work.

Tasks also support chaining with other tasks using continuations, like we saw earlier with Task.ContinueWith. By default, this method can run the continuation you pass in on a different thread. This is the heart of the issue, as most of Unity’s API requires to be called from the main thread.

This is because most of Task’s methods use a TaskScheduler to handle async work. And the default TaskScheduler they will use (TaskScheduler.Current) may run work on different threads.

Await and SynchronizationContext

Await statements are syntactic sugar for “put the rest of my method in an Action, and give this Action to the task”. But Await does not use Task.ContinueWith. Instead, the “unwrapped” code would look like this:

HttpClient client = new HttpClient();
var __awaiter = client.GetAsync(initUri).GetAwaiter();
__awaiter.OnCompleted(() =>
{
var initContent = __awaiter.GetResult();

// Do some initialisation

SceneManager.LoadScene(nextSceneIndex);
});

It is a bit more complicated than that, because more context is carried to OnCompleted’s delegate. Blocks like using or try/catch still apply inside the delegate. This is just for illustration purposes.

So await is using Task.GetAwaiter()**.**OnCompleted(Action) instead. The main difference is that the “Awaiter” uses SynchronizationContext.Current to dispatch their async work, and not TaskScheduler.Current. They are both static properties, but SynchronizationContext.Current has a great advantage: it has a setter. Meaning that other parts of the code can “override” the default behavior of SynchronizationContext as it was designed in vanilla C#.

And this is exactly what happens in Unity. Unity (like other frameworks) creates their own SynchronizationContext, which executes actions in the main thread and put them in SynchronizationContext.Current. This will make code after awaits run on the main thread.

To summarize

The Task Asynchronous Programming (TAP) system is a powerful asynchronous programming paradigm. However, working with it causes thread issues in Unity.

  • In Unity, async methods called from the main thread will run entirely on the main thread. Having async methods does not make your code multithreaded!
  • Be careful when working with methods using TaskSchedulers. They will not use Unity’s synchronization context by default and do not offer the same guarantees (you will probably end up executing code on other threads!).

At Homa, we pride ourselves on pushing the boundaries of technology and data within the gaming industry, and we’re always on the lookout for individuals who share our passion for innovation. If you’re eager to work with an experienced team of Developers and Engineers and contribute to groundbreaking projects, we invite you to take a look at our open roles.

Visit our website to learn more about our exciting tech opportunities and apply to become part of the team!

--

--