Awaitable class documentation doesn't add up with the current behavior

I noticed a bug regarding the Awaitable class.

According to a sample provided in the documentation:

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

Not only I noticed that wasn’t true, but the provided sample doesn’t even work as stated:

private async Awaitable<float> DoHeavyComputationInBackgroundAsync()
{
    await Awaitable.BackgroundThreadAsync();
    // here we are on a background thread
    // do some heavy math here
    return 42; // note: we don't need to explicitly get back to the main thread here, depending on the caller thread, DoHeavyComputationInBackgroundAsync will automatically complete on the correct one.
    // Added note: It will return on the same background thread
}

public async Awaitable Start()
{
    var computationResult = await DoHeavyComputationInBackgroundAsync();
    // Added note: The statement below is false, the execution continues on the same background thread as the method above.
    // although DoHeavyComputationInBackgroundAsync() internally switches to a background thread to avoid blocking,
    // because we await it from the main thread, we also resume execution on the main thread and can safely call "main thread only APIs" such as LoadSceneAsync()
    await SceneManager.LoadSceneAsync("my-scene"); // this will succeed as we resumed on main thread
}

Hopefully I didn’t misunderstand the documentation.

Matteo

1 Like

Yes, this is yet again a bad / wrong example in the documentation. Of course the execution would resume on the background thread. I guess they wanted to add a await Awaitable.MainThreadAsync(); in somewhere. Something like this generally would make the most sense

        private async Awaitable<float> DoHeavyComputationInBackgroundAsync()
        {
            Debug.Log("Main Thread: " + System.Environment.CurrentManagedThreadId);
            await Awaitable.BackgroundThreadAsync();
            // do background work here.
            Debug.Log("Background Thread: " + System.Environment.CurrentManagedThreadId);

            await Awaitable.MainThreadAsync();
            Debug.Log("Back on main Thread: " + System.Environment.CurrentManagedThreadId);
            return 42; 
        }

        public async Awaitable Start()
        {
            Debug.Log("Thread Before: " + System.Environment.CurrentManagedThreadId);
            var computationResult = await DoHeavyComputationInBackgroundAsync();
            Debug.Log("Thread After: " + System.Environment.CurrentManagedThreadId);
        }

If an async method does change to a background thread to do work, it should switch back before it returns. Though as mentioned in the documentation, calling await Awaitable.MainThreadAsync(); when you are already on the main thread is “almost” a “no-op” and would continue immediately. So when dealing with unknown async tasks it usually doesn’t hurt to put a await Awaitable.MainThreadAsync(); right after the call to ensure we’re back on the main thread. Common issues are when you use third party libraries where you don’t know if they switch to a background thread or not.

You could use a “wrapper” like this:

        public static async Awaitable<T> MT<T>(Awaitable<T> aTask)
        {
            T res = await aTask;
            await Awaitable.MainThreadAsync();
            return res;
        }

This would ensure you return back to the main thread after awaiting a tast. So you can do

var computationResult = await MT(DoHeavyComputationInBackgroundAsync());
// back on main thread thanks to MT()

Though an explicit await Awaitable.MainThreadAsync(); after the actual background task is probably more readable.

1 Like

The behavior of those methods has changed in latest patch release because it introduced lots of subtle bugs (with small changes within a method that could break some callers because of threading stickiness). The documentation is aligned with latest version.

Really? But how does Unity decide when to switch back to the main thread? What happens when you await another async method from within that task method and you want to execute that in the same thread? The “thread stickiness” actually made sense, now I’m more confused what the exact behaviour is :slight_smile:

Awaiting MainThreadAsync or BackgroundThreadAsync now has only effect in the method that directly calls it (let us name it InnerMethod). If an outer method calls InnerMethod from main thread, it will resume on MainThread. If another outer method calls it from a background thread, it will resume on a background thread :slight_smile: