Introducing Asynchronous Programming in Unity

Hi! I’m Jemin Lee, a Partner Engineer at Unity. Our team works with game studios to help them streamline development in their Unity projects.

In this article, we’ll explore the fundamentals of asynchronous programming in Unity - from core concepts to practical applications.

You’ll learn:

  • Why asynchronous programming is important
  • The differences between synchronous and asynchronous
  • How Unity’s Coroutines work
  • How async & await simplify asynchronous code
  • What’s new with Awaitable in Unity 6

Instead of diving into every syntax detail, this guide focuses on helping you build a clear, practical understanding so you can start writing asynchronous code with confidence.

What is Asynchronous Programming

Asynchronous programming allows a program to start operations and continue execution without blocking the current thread while waiting for tasks to complete. This keeps the program responsive, even during long-running operations such as asset loading, networking, or file I/O.

Synchronous Programming

In synchronous programming, code executes sequentially, line by line.

Each operation must be completed before the next one begins. This means that while waiting for the completion of the operation, the thread is blocked and cannot perform other tasks.

This sequential approach works fine for quick operations, but can cause serious problems in game development when operations take time. Consider a common scenario: a menu scene that downloads all game assets before displaying the UI:

private void LoadMainMenu()
{
    DownloadPlayerAssets();    // takes 5 seconds - game frozen!
    DownloadWorldAssets();     // takes 8 seconds - still frozen!
    DownloadAudioAssets();     // takes 3 seconds - still frozen!
    
    ShowMainUI(); // After 16 seconds of frozen game
}

In this example, the main thread runs each download one after another, waiting for each to complete before moving on. The UI and gameplay are completely frozen for 16 seconds.

Even if each operation could technically be divided into smaller steps or executed on another thread,
the current structure forces the main thread to stay blocked until everything is finished.

Asynchronous Programming - Move forward without blocking

In the synchronous example above, the main thread waits for every operation to finish before continuing.

But what if we separate starting a task from waiting for its completion? In other words, what if we wrote the code so that the start of each task doesn’t need to be synchronized with the completion of the previous one?

private void LoadMainMenu()
{
    StartDownloadPlayerAssets();   // takes time, but we don't have to wait
    StartDownloadWorldAssets();    // starts immediately after
    StartDownloadAudioAssets();    // starts right away

    ShowMainUI(); // The UI can appear immediately
}

Now, we’re not waiting for each operation to complete before moving on.

Instead, each StartDownloadXXX() method creates a task that begins its work independently, and the program continues executing other logic (such as rendering the UI or processing input) while those tasks are still running.

Each task can behave differently depending on its implementation:

  • It might run on the same thread, spread across multiple frames (frame-sliced).

  • Or it could run concurrently on another thread (background execution).

Either way, the key idea remains the same:

  • Execute tasks without blocking. Let the program keep moving while work is still in progress.

You can also pause a method’s execution and wait for another operation to finish. This allows the method to temporarily suspend its work, while the thread itself remains free to handle other operations.

The key point is that waiting in asynchronous code doesn’t mean blocking - the thread itself remains free to perform other work while the method is paused, so the program keeps running smoothly without halting.

There are two main ways to implement asynchronous code in Unity: Coroutines and Awaitable.

Coroutines

Before Unity 6’s Awaitable, you would primarily depend on using Coroutines to achieve asynchronous behavior. A coroutine is a programming pattern that implements asynchronous behaviour by suspending and resuming execution.

Think of coroutines like cleaning your house in a smart way. In synchronous programming, you clean the entire house in one exhausting day.

private void CleaningHouse()
{
    Debug.Log("Start Cleaning House");
    
    CleanLivingRoom();
    CleanBedRoom();
    CleanBathRoom();
    
    Debug.Log("Finish Cleaning house");
}

But instead of cleaning the entire house in one exhausting day, you can split it.

  • Day 1: Clean the living room and kitchen
  • Day 2: Clean the bedrooms
  • Day 3: Clean the bathrooms and finish up

This way, you’re not exhausted on any single day, and you can still do other important things like exercise, spend time with family, and play video games.

If we rewrite the CleaningHouse() as a coroutine, it would look like this:

private IEnumerator CleaningHouseRoutine()
{
    Debug.Log("Start Cleaning House");
    
    CleanLivingRoom();
    yield return new WaitForSeconds(10); // wait for 10 seconds
    CleanBedRoom();
    yield return new WaitForSeconds(10); // wait for 10 seconds
    CleanBathRoom();
    
    Debug.Log("Finish Cleaning house");
}

When CleaningHouseRoutine() coroutine is executed, here’s exactly what happens:

Step 1:

  • Debug.Log(“Start Cleaning House”) prints immediately
  • CleanLivingRoom() executes and completes

Step 2:

  • The coroutine hits yield return new WaitForSeconds(10)
  • The coroutine pauses here and exits - giving control back to Unity
  • Unity continues do other things(rendering, handling input, running other systems, etc…)
  • The coroutine waits for 10 seconds in the background

Step 3 (After 10 seconds):

  • Unity resumes the coroutine from where it left off
  • CleanBedRoom() executes and completes

Step 4:

  • The coroutine hits the second yield return new WaitForSeconds(10)
  • The coroutine pauses again and exits - control returns to Unity
  • Another 10-second wait begins

Step 5 (After another 10 seconds):

  • Unity resumes the coroutine again
  • CleanBathRoom() executes and completes
  • Debug.Log(“Finish Cleaning house”) prints
  • The coroutine reaches the end and completes

The key to understanding coroutines is the yield statement. When a coroutine hits a yield statement, it pauses execution at that exact point and returns control back to Unity.

For example, when the code hits `yield return SomeTask()`, the program executes `SomeTask()` and the coroutine pauses until `SomeTask()` completes.

Similarly, when it encounters `yield return new WaitForSeconds(n);`, the coroutine pauses for n seconds. And while the coroutine is suspended, other codes can run.

And when the time or task being waited on via the yield statement completes, code execution resumes at the last point within the coroutine.

Now we can rewrite the synchronous code that paused the game for 16 seconds as a coroutine.

// Coroutine approach: pause and resume
private IEnumerator LoadMainMenuRoutine()
{
    // Day 1 : Start downloading player assets and 
    // Take a break while waiting for the completion
    yield return DownloadPlayerAssetsRoutine();

    // Day 2: Start downloading world assets and
    // Take a break while waiting for the completion
    yield return DownloadWorldAssetsRoutine();
    
    // Day 3: Start downloading audio assets and
    // Take a break while waiting for the completion
    yield return DownloadAudioAssetsRoutine();
    
    ShowMainUI(); // Game stayed responsive throughout!
}

For a detailed explanation of coroutines, refer to the official Unity documentation:

Note: Asynchronous Single Thread

Asynchronous programming is related to multithreading, but it doesn’t necessarily require it. The core idea is to start a task without waiting for it to finish - meaning an asynchronous operation can still run on the same thread at different points in time.

In Unity, coroutines run on the main thread - the same thread that calls the StartCoroutine() method. However, because coroutines can pause and resume across multiple frames using the yield statement, the program remains responsive.

For example, this diagram shows how a coroutine method like SomeCoroutineMethod() splits its work across multiple frames using the yield statement. Each small task runs on the main thread, but by pausing and resuming between frames, the game stays responsive instead of freezing during long operations.

The Problems with Traditional Coroutine Approaches

While Coroutines have served us well, they often lead to unnecessarily complicated code. Let’s consider an example of loading an addressable asset using a coroutine.

// A coroutine that loads an addressable asset asynchronously
public IEnumerator GetAssetRoutine(string assetName, Action<GameObject> onAssetLoaded)
{
    // Start loading the asset and keep the handle so we can wait for it to finish
    var asyncOperationHandle = Addressables.LoadAssetAsync<GameObject>(assetName);    // Wait until the loading is finished
    yield return asyncOperationHandle;

    // Get the loaded asset from the handle
    var loadedAsset = asyncOperationHandle.Result;

    // Pass the result back through a callback    
    onAssetLoaded(loadedAsset);
}


public void Start()
{
    // You might expect to write:
    // GameObject loadedAsset = StartCoroutine(GetAssetRoutine("boat"));
    // Debug.Log(loadedAsset);
    // but Coroutines can't directly return values.

    // Instead, you must use a callback to receive the loaded asset
    StartCoroutine(GetAssetRoutine("boat",(loadedAsset) => { Debug.Log(loadedAsset); }));
}

Note: Addressables is a package that lets you load assets easily by referencing them with their address (key) names. It simplifies your code by handling asset locations internally, so you don’t need to manage file paths or bundle structures yourself.

The code above is not intuitive because you cannot directly get loaded assets via ‘=’.

The fundamental issue is, Unity’s Coroutines return IEnumerator which is only an iterator used to control the execution flow, not to return actual data.

There’s no mechanism to pass a return value back to the caller. Therefore, to get results from a coroutine, you must use a callback or store the data in a shared variable. And this could fall us into complex callback patterns that are unnecessarily complex and non-intuitive.

public IEnumerator GetAssetRoutine(string assetName, Action<GameObject> onAssetLoaded)
{
    var asyncOperationHandle = Addressables.LoadAssetAsync<GameObject>(assetName);
    yield return asyncOperationHandle;
    var loadedAsset = asyncOperationHandle.Result;
    
    onAssetLoaded(loadedAsset);
}

public void Start()
{
    StartCoroutine(GetAssetRoutine("boat", (loadedAsset) => {
        DoSomething(loadedAsset, (result1) => {
            DoSomething2(result1, (result2) => {
                DoSomething3(result2, (result3) => {
                    // Welcome to callback hell!
                });
            });
        });
    }));
}

Additional Limitations of Traditional Approaches

Beyond readability issues, coroutines have several technical limitations:

  • Memory Allocation Overhead: Each coroutine creates an IEnumerator object, causing GC pressure
  • Main Thread Only: Coroutines execute exclusively on the main thread via Unity’s player loop

C# async/await: A Better Way

C#'s native async/await syntax offers a more elegant solution:

public async Task<GameObject> GetAssetAsync(string assetName)
{
   // Start loading the asset and keep the handle so we can wait for it to finish
    var asyncOperationHandle = Addressables.LoadAssetAsync<GameObject>(assetName);

    // .Task exposes the async operation as a Task, so it can be awaited
    // Wait until the loading task is finished (without blocking)
    await asyncOperationHandle.Task; 
    // Return the loaded asset
    return asyncOperationHandle.Result;
}

public async void Start()
{
    // Wait for the asset to be loaded
    GameObject loadedAsset = await GetAssetAsync("boat");

    // Then run more async operations in sequence
    await DoSomethingAsync(loadedAsset);
    await DoSomething2Async(loadedAsset);
    await DoSomething3Async(loadedAsset);
}

Much cleaner!

The async keyword marks a method as asynchronous, while the await keyword pauses the method’s execution and creates a checkpoint. Once the awaited task completes, the execution resumes from that checkpoint, continuing the rest of the method seamlessly.

The async method returns a Task or Task<T>, where T is the type of the result.

When you await a Task<T>, the await expression completes only after the asynchronous operation finishes, and then automatically provides the result of type T.

This means you can write code like:

GameObject asset = await LoadAssetAsync("boat"); 

The await keyword unwraps the Task and gives you the actual result, so you can use it directly. This makes your code much cleaner since you can handle asynchronous results without relying on callbacks or shared variables.

However, Unity developers have historically avoided async/await due to several limitations.
Let’s check the following example code.

private async void Start()
{
    var task = SomeAsync(); // Task object is created each time
    await task;
}

private async Task SomeAsync()
{
    Time.timeScale = 2f;
    Debug.Log($"Time: {Time.time}");
    // Even with the game running at double speed
    // it still waits for 100ms of real-time.
    await Task.Delay(100);
    Debug.Log($"Time: {Time.time}");
}

You can see that the above code has the following issues.

  • Memory Allocations

Every time you call an async method, it creates a Task object which causes GC pressure.

  • No Unity Lifecycle Integration

Task method can’t await Unity’s Playerloop specific events like frame timing or FixedUpdate. Also you can’t wait for time differently depending on the time scale.

Introduction to Awaitable

By writing the async method using the Awaitable introduced in Unity 6, you can resolve the above problem while still using the async and await keywords. This is the recommended approach for asynchronous programming in Unity 6 and later.

The syntax looks like this:

public async Awaitable<GameObject> GetAssetAsync(string assetName)
{
    var asyncOperationHandle = Addressables.LoadAssetAsync<GameObject>(assetName);
    // .Task exposes the async operation as a Task, so it can be awaited
    // Wait until the loading task is finished (without blocking)
    await asyncOperationHandle.Task;
    // Once loading is done, return the loaded asset.
    return asyncOperationHandle.Result;
}

public async void Start()
{
    var loadedAsset = await GetAssetAsync("boat");
}

Awaitable is a type used in Unity to represent asynchronous operations, replacing the Task type.

Awaitable Pooling

One of Awaitable’s advantages over Task is its object pooling system. This eliminates the GC pressure caused by creating new Task objects for every async operation.

However, this comes with a critical limitation: you must never await the same Awaitable instance twice.

// INCORRECT
async Awaitable AwaitableExample()
{
    var awaitableInstance = SomeAwaitableMethod();
    await awaitableInstance; // First await - completes normally
    // At this point, awaitableInstance returns to the pool!
    await awaitableInstance; // Second await - UNDEFINED BEHAVIOR!
}

Why this happens

After the first await completes, the Awaitable instance returns to Unity’s object pool and may immediately be reused by another async method.

Your second await might actually be waiting for a completely different operation!

Here’s a concrete example that illustrates this:

async void Start()
{
    Awaitable awaitable1 = TestAsyncMethod();
    await awaitable1;
    Awaitable awaitable2 = TestAsyncMethod();
   
    // This will likely be true due to pooling!
    if(awaitable1 == awaitable2)
    {
        Debug.LogError("Same Awaitable instance reused from pool!");
    }
   
    // awaitable1 might now represent awaitable2's operation
    // Potential deadlock or unexpected behavior
    await awaitable1; 
}

async Awaitable TestAsyncMethod()
{
    await Awaitable.WaitForSecondsAsync(1f);
}

Because Awaitable is recycled via a pool, awaitable1 and awaitable2 are the same instance. This means that awaiting awaitable1 will effectively be the same as awaiting awaitable2. This potentially leads to unintended behaviour.

  • Best Practice: Always await Awaitable instances immediately and never store them for later reuse.
async void Start()
{
    // Don't : Don't store Awaitable instance and await later
    Awaitable awaitable = TestAsyncMethod();
    await awaitable;

    // Do : await immediately
    await TestAsyncMethod();
}

Unity Player Loop Integration

Unlike Task.Delay(), Awaitable methods respect Unity’s time management:

public class TimingComparison : MonoBehaviour
{
    async void Start()
    {
        Debug.Log($"Starting at Time: {Time.time}");
        Time.timeScale = 2f; // Double game speed
        
        // Real-world time delay (unaffected by timeScale)
        await Task.Delay(2000); // Always 2 real seconds
        Debug.Log($"Task.Delay completed at Time: {Time.time}");
        
        // Game time delay (affected by timeScale) 
        await Awaitable.WaitForSecondsAsync(2f); // 1 real second at 2x speed
        Debug.Log($"Awaitable completed at Time: {Time.time}");
        
        Time.timeScale = 1f;
    }
}

Additionally, Awaitable provides Playerloop-specific Await methods.

// Time-based waits
await Awaitable.WaitForSecondsAsync(1.5f);        // Respects Time.timeScale

// Frame-based waits  
await Awaitable.NextFrameAsync();                  // Wait one frame
await Awaitable.EndOfFrameAsync();                 // End of current frame
await Awaitable.FixedUpdateAsync();                // Next FixedUpdate

// Background processing
await Awaitable.BackgroundThreadAsync();           // Switch to background thread
await Awaitable.MainThreadAsync();                 // Switch back to main thread

Thread Switching

Most Unity Object APIs (such as creating GameObjects, modifying Transforms, or accessing scene data) must be called on the main thread. Still, running heavy computations directly on the main thread can cause frame drops or stuttering, so it’s often beneficial to offload such work to a background thread using Awaitable.

Awaitable provides built-in methods to switch between threads:

await Awaitable.BackgroundThreadAsync(); // Switch to background thread
await Awaitable.MainThreadAsync();       // Switch back to main thread

Here’s an example that performs heavy mathematical calculations on a background thread and then continues on the main thread automatically:

public class ThreadSwitchExample : MonoBehaviour
{
    async void Start()
    {
        var results = await DoHeavyComputationAsync();
        
        // Automatically resumed on the main thread - safe to use Unity APIs
        foreach (var position in results)
        {
            var go = new GameObject("ComputedObject");
            go.transform.position = position;
        }
    }
    
    async Awaitable<Vector3[]> DoHeavyComputationAsync()
    {
        // Switch to background thread to avoid blocking the main thread
        await Awaitable.BackgroundThreadAsync();
        
        // CPU-intensive calculations
        var results = new Vector3[10000];
        for (int i = 0; i < results.Length; i++)
        {
            results[i] = new Vector3(
                Mathf.Sin(i * 0.1f),
                Mathf.Cos(i * 0.1f),
                Mathf.Tan(i * 0.1f)
            );
        }

        // No need to call Awaitable.MainThreadAsync()
        // continuation automatically resumes on the Main Thread
        return results;
    }
}

Note that within that DoHeavyComputationAsync, you must not call any Unity Object APIs while on the background thread, as they are not thread-safe.

The effects of Awaitable.MainThreadAsync and Awaitable.BackgroundThreadAsync are local to the current method only. Because Start() runs on the main thread, the continuation after await DoHeavyComputationAsync() also resumes on the main thread.

One-frame delay rule

Switching between threads introduces a one-frame delay because Unity queues the continuation for the next update cycle.

  • Switching to the same thread does not incur a delay.
  • Switching too frequently can cause unnecessary overhead.
async Awaitable PerformanceExample()
{
    await Awaitable.MainThreadAsync();      // No delay (already on main thread)
    await Awaitable.BackgroundThreadAsync(); // One frame delay for next line
    
    DoBackgroundWork();
    
    await Awaitable.MainThreadAsync();       // Another one frame delay
    
    DoMainThreadWork();
}

So avoid frequent thread switching in performance-critical code.

// Don't do this : Excessive thread switching  
for (int i = 0; i < 100; i++)
{
    await Awaitable.BackgroundThreadAsync(); // Too much overhead!
    DoSmallWork();
    await Awaitable.MainThreadAsync();
}

Summary

Asynchronous programming in Unity allows you to run operations without blocking the main thread, keeping your game responsive even during long-running tasks.

Traditionally, Unity developers relied on Coroutines to implement asynchronous behavior. While effective, coroutines can be verbose, cannot directly return values, and often lead to complex callback chains.

The C# async/await pattern offers cleaner, more intuitive asynchronous code with direct return values, but standard Task objects introduce garbage collection overhead and lack integration with Unity’s player loop and time scaling.

Unity 6’s Awaitable type bridges this gap by:

  • Eliminating GC pressure via object pooling
  • Integrating with Unity’s lifecycle and time scaling
  • Providing Unity-specific await methods for frames, updates, and threads
  • Allowing seamless thread switching between main and background threads

Best Practices:

  • Always await Awaitable instances immediately; never reuse them after completion
  • Minimize thread switching in performance-critical code
  • Use coroutines for simple sequential delays, async/await with Awaitable for complex asynchronous workflows
  • Switch back to the main thread before interacting with Unity objects

By understanding coroutines, async/await, and Awaitable, you can choose the right tool for each scenario, write cleaner asynchronous code, and keep your Unity games running smoothly without unnecessary blocking.

What’s Next?

In this article, we focused on the mechanics of asynchronous programming in Unity - using Coroutines, async/await, and Unity 6’s Awaitable.

However, we did not cover one important underlying concept: SynchronizationContext.

Key points to keep in mind:

  • Asynchronous programming is closely related to multithreading, but it is not the same thing.
  • In asynchronous programming, async/await handles pausing and resuming code execution.
  • SynchronizationContext represents where (on which thread or environment) an asynchronous task will continue after resumption.
  • Unity uses a special UnitySynchronizationContext to ensure that most async continuations run on the main thread, making it safe to interact with Unity APIs.

If you want to dive deeper into this topic, I’ve written a follow-up article that focuses specifically on SynchronizationContext and Unity’s implementation:

27 Likes

The chapters on Coroutines and async/await are good, but the way that What is Asynchronous Programming focuses so heavily on multi-threading feels misleading to me. It felt initially like this thread was about concurrent programming instead of asynchronous programming.

Except when using async/await, you typically do wait for the completion of a previous task before moving on to the next one (you even point out in the section about Awaitables that you should await them immediately instead of assigning them to a variable).

The benefit with writing async code is more about avoiding the whole thread being blocked while waiting for a task to complete.

Asynchronous literally means “not occurring at the same time”.

While you can execute multiple asynchronous tasks in parallel using Awaitable.BackgroundThreadAsync and Task.Run etc., this doesn’t happen by default when you’re just writing async methods or coroutines in Unity.

I think a better mental model about asynchronous programming would be that it allows you to suspend the execution of your code until some event occurs, without the thread being blocked while you’re waiting for said event to occur.

Edit: It might also be good to mention that it’s unsafe to use Task.Delay on WebGL platforms, because it uses the ThreadPool internally, or Task.ContinueWith without passing in TaskScheduler.FromCurrentSynchronizationContext() to it, because it uses the ThreadPoolTaskScheduler by default.

4 Likes

UnitySynchronizationContext is sealed class which so inconvenient when we just want to use it for Task or Reactive system

That’s a good introduction to async/await for beginners who are used to tutorials that rely heavily on coroutines. It’s useful to have this post available so we can guide them toward replacing coroutines with async/await, since they’re often not inclined to read documentation or blog posts on other sites.

For deeper details or nuances of async/await, they’ll eventually need to consult additional resources, but this post should make the transition from coroutine-based asynchronous programming to async/await relatively easy for them.

Good Article to understand the status of Asynchronous Programming in Unity.

As you say that’s indeed typically what you do, but it’s good for people to know that it doesn’t need to be the case, you can start a task and then do other stuff before awaiting it, start several tasks and later wait for all of them, etc. But you need to be careful with how you handle them.

This is some really helpful concise information. Unity has been on the Coroutine train for so long it’s been a struggle to get away from it and the nuances of doing async work in Unity is foggier than it should be - so this write up is super helpful because you’re addressing the entire problem as a whole, including coroutines, highlighting some solutions and illustrating the main gotchas.

Really nice, thanks.

Definitely a good idea to teach about that possibility as well! It’s just that it’s so common for people to mix up concurrent and asynchronous programming, and to think that async/await is inherently about multi-threading, that I think it’s important to not add to the confusion when teaching new programmers about what asynchronous programming means :slightly_smiling_face:

1 Like

There’s only Awaitable.WaitForSecondsAsync(). As of Unity 6.4a, there are no other wait methods on Awaitable, not for unscaled or real-time.

1 Like

Good catch. This needs to be fixed as soon as possible, since it will likely confuse many readers who don’t check the manual page.

Thanks for pointing that out. Awaitable.WaitForSecondsRealtime() isn’t implemented in Awaitable yet.
I mixed it up with the WaitForSecondsRealtime type used in traditional coroutines.

Thanks for the clarification. I agree that asynchronous programming shouldn’t be conflated with multithreading or parallel execution. I’ll revise the intro to reflect that distinction more clearly.

That said, I’ve found it tricky to explain asynchronous programming to beginners without first referencing threads at all. For beginners, using the concept of threads as a mental anchor helps them understand what’s actually being freed when the program doesn’t block.

Topics like SynchronizationContext, thread behavior in async workflows, and why Task.Delay isn’t safe on WebGL require a deeper dive, so I plan to cover them in a separate article aimed at more intermediate readers.

1 Like

I’ve always found it easier to explain it like this: that specific piece of code will stop executing and resume once something has happened, because terms like “asynchronous” and “threads” can be confusing for beginner programmers who aren’t fluent in English.

1 Like

I like to think of async/await as basically inherently just being syntactic sugar for subscribing to events.

So we can take multiple functions that otherwise would have been stitched together using callbacks:

void OnTriggerEnter(Collider other)
{
    var loadOperation = SceneManager.LoadSceneAsync(“MyScene”, LoadSceneMode.Additive);
    loadOperation.completed += OnLoadOperationCompleted;
}

void OnLoadOperationCompleted(AsyncOperation op)
{
    Debug.Log("MyScene was loaded successfully.");
}

And squash them together into a single one that can be easily read from top-to-bottom, line-by-line, without any jumping around:

async void OnTriggerEnter(Collider other)
{
    await MySceneManager.LoadSceneAsync(“MyScene”, LoadSceneMode.Additive);
    Debug.Log(“MyScene was loaded successfully.”);
}

That’s what asynchronous programming is inherently all about: just the execution of pieces of code separated by time - without the thread being blocked while waiting for the signal to move from one piece of code to the next.

I’m quite worried that if new programmers mistakenly start thinking that async/await is inherently related to multi-threading, they could avoid using it for a long time because of being worried that they can’t freely make use of Unity’s APIs that aren’t thread-safe inside their async methods.

I liked the approach that the section on Awaitables had: first talk about how to execute code asynchronously on the main thread (which probably covers over ~98% of use cases), and only later move on to talking about how one can go about executing code on a background thread.

Also, another huge benefit that async/await has over coroutines that could be pointed out is that it fully supports using statements and try/catch/finally:

using var someService = new SomeService(); // ← disposed reliably
var something = await someService.GetSomethingAsync();
await someService.SendSomethingAsync(something);

It’s pretty scary how if you accidentally combine coroutines with try/finally, everything compiles just fine, yet the body of the finally statement might never get executed if a coroutine gets stopped mid-execution:

using var someService = new SomeService(); // ← not disposed reliably!
Something something = null;
yield return someService.GetSomethingAsync(x => something = x);
yield return someService.SendSomethingAsync(something);

I remember it took us multiple days to figure out how this seemingly impossible thing was happening when we first ran into this limitation.

1 Like

Did you forget the instructions on how to use cancellation tokens to integrate with the Monobehaviour lifecycle?

4 Likes

Sorry for the late reply, but I don’t believe your example, where you do calculation on a background thread then hop back to the main thread before returning, needs the explicit switch back to Main if you’re using Awaitables. I’m not going to quote the whole example, but line 32 can actually be removed, which is important. You don’t need to await Awaitable.MainThreadAsync() inside DoHeavyComputationAsync(), just before you return.

Per Unity - Manual: Awaitable completion and continuation

The effect of Awaitable.MainThreadAsync and Awaitable.BackgroundThreadAsync are local to the current method only.

I actually really like this about using Awaitable and BackgroundThreadAsync(). It took me a while to get used to this idea, but it’s actually fantastic for kicking stuff onto background threads whilst being confident you won’t accidentally leave yourself on a background thread when your code resumes.

3 Likes

Thank you everyone, for your great feedback.

Thanks for catching that you’re absolutely right.

Since the method is awaited from the main thread, the continuation automatically resumes there once the background work finishes, so calling Awaitable.MainThreadAsync() before returning isn’t necessary in this case.

Before the last edit, I recalled an article mentioning that continuations didn’t automatically return to the original thread in some cases, but it seems that was either outdated information or my own misunderstanding. I should’ve double-checked against the latest documentation. I updated the example accordingly.

I intentionally left out cancellation tokens for this article to keep the focus on helping beginners get comfortable with asynchronous programming first. I’ll see if I can add a brief mention without making it overly complex.

Thanks for the great points. I’ll be revising some part of article based on what you mentioned. Updating that section will take a bit of time since I’ll need to redraw the diagrams and rewrite parts of the explanation to make it clearer.

2 Likes

Asynchronous programming hard only on pure C# and Tasks because there is no build in concept of PlayerLoop

Unity have Player Loop so explaining async/await way simpler and dont need any new concepts
For beginner what you do is just ask to continue you method execution later in player loop or on next frame of after few frames and thats it. Very simple and powerful thing.

Question

Why Awaitables implements in a way so they slower than UniTask and why they so limited when comparing to UniTask

Do you have plans to make Awaitables on par with UniTask by what can be awaited, awaiting different parts of player loop like last Update or PreLateUpdate and EarlyUpdate etc.

1 Like