Follow up: Async methods continue to execute when you stop the game in the editor, potentially dange

Hi there.

Nearing 2 years ago now I raised since concerns regarding Async method usage in the editor. To recap:

An example of a dangerous scenario:

  • A behaviour is attached to an object. It’s Start method is an Async method which waits 10 seconds, then destroys that gameobject.
  • The user hits play in the editor.
  • The user hits stop before the 10 seconds are up
  • now, in edit mode, the 10 seconds are up and the gameobject destroys itself.
  • user saves the scene without realizing
  • losses work…
  1. I don’t think many people are aware of this. Every time I bring it up on Reddit, people thank me because I just helped them Dodge a bullet.
  2. I think the “expected” behavior is for the async methods to stop executing like coroutines when you stop the game in the editor.

I’ve also noticed that the Addressable package leaks gameobjects into the scene if you await a load operation and then stop the game in the editor. You’ll see a resource manager object leak into your scene and get saved into the scene.

@mkderoy wrote:

So I’m just here wondering if there is any movement on an overarching solution. It would be really nice to be able to use Async/await pattern in game code but as it stands it is too dangerous to use.

3 Likes

We haven’t had any movement on usage of async/await in game code. For this reason (and others) I would suggest that it is avoided.

This is a shame. It makes it an absolute joy to work with addressables and Rest APIs with most games these days being so connected to the cloud.

No returns values with coroutines and callback hell, and were in 2020. :frowning:

It’s so tantalisingly close to being usable. Everything already just works except for this one unsolved problem.

You allude to other issues, what are they? Are there possible user workarounds? Maybe it would be possible for us to create our own synchronisation context and just stop pumping events when exiting play mode… only need to do this hacky special handling in Editor? Though my experience is limited to none when it comes to all of this.

Do you ever see this being resolved?

2 Likes

The other issues are mostly performance related - async/await generates a good but of code behind the scenes, and that code applies a lot of GC pressure, so it is often not the best solution.

I’m not too experienced with async/await either, but I suspect that there is a way to work around this with a custom synchronization context.

I think that it will be resolved at some point, as async/await could be a very nice paradigm for APIs exposed by Unity and Unity packages. But I don’t know when this effort will happen.

2 Likes

Yes I recently started looking into addressables and it is interesting that the API is a strange mix of Async/await and callback driven. Clearly the author’s didn’t/couldn’t commit to one or the other. It’s in a strange spot where it “does work with Async/await but also you really shouldn’t use it!”.

Thanks for your time.

1 Like

this should be greatly reduced in the latest roslyn (or runtime), and devs can bring it close to 0 by using ValueTask<T> and IValueTaskSource

also, custom awaiters can be written for most async operations that can reduce or eliminate GC (beyond the op itself)

I have an internal library that provides:

  • alternatives to Task.Yield and Task.Delay that drops the continuation when exiting playmode
  • a CancellationToken that triggers on exit playmode
  • custom awaiters for UnityWebRequest and other async operations, all playmode-aware (with user opt-out)
    unfortunately you need to remember to use them everywhere instead of the system ones (no analyzer yet)

Hey M_R!

And that there is the crux of it for me. I’m testing game play code in the editor. Gameplay code won’t exhibit any of this problematic behaviour it run time, so I don’t really want to litter my code base with all these checks, just got the sake of the editor.

That asside, do you have any resources perhaps you are willing to share your work for others to learn from? Would be really curious to see!

For ValueTask you need to wait for .NET 5 in Unity, it’s only available in .NET core and successors.
I have experimented with async/await Job complete and i had lags, in my case Unitys synchrontationContext take up to 400ms (start awaiting for over 100 async methods in one frame)

There’s also GitHub - Cysharp/UniTask: Provides an efficient allocation free async/await integration for Unity.
(which i think addresses both issues)

1 Like

It seems like just taking a hammer to the problem - though thanks for the resource!

For me personally, I don’t intend async/await to be used in hot paths of my code base. I don’t intend for them to replace coroutines for lots of other small things (tweening, etc). I just want to be able to make web requests and load addressables without the results leaking out of play mode in the editor. I just want to use regular C# async/await.

So I’ve done some experimenting. All I’ve done is copied the UnitySynchronizationContext class so I can instance my own copy. I register that as the SyncContext on RuntimeInitLoad. I create an invisible DontDestroyOnLoad ticker object that just Exec’s my copy of the SyncContext every Update, and when that object gets destroyed (ie exiting Play mode) then none of my exec methods resolve. Problem solved for me.

This has the downside of interrupting editor async methods when transitioning back from PlayMode, but this already happens due to DomainReload on PlayMode enter… But I am not executing edit-time long running async methods that I expect to carry through play state change. I generally do my work, save up, and hit play. I don’t ever kick off some long running task in edit mode and decide to hit play while the operation is still in flight and expect it’ll just ‘work’. So I can accept that limitation.

Another limitation is that, my simple setup does not provide a way to return control to my async methods at different points (FixedUpdate, LateUpdate, etc)… but if I need that much control over the async method its probably a sign that it might be better to do it the ‘traditional Unity way’ (co-routines).

All of this code is editor only, and I just strip out all this functionality at runtime so there is no custom layer of ‘jank’ over everything. It is just pure C# async/await with Unity’s default sync context handling.

I keep seeing users mention performance implications, but (again, for me) this is not a concern. I am generally doing something heavy-weight (loading addressables, serialising/deserialising JSON request/responses)… the ‘cost’ of native async/await is dwarfed by the cost of the actual work, so meh, I am cool with it I think.

How about going for CancellationToken? Make your method be cancellable and on exiting play (or whatever the event name is) cancellationTokenSource.Cancel()? Just be sure, that before any significant elements you’re checking if cancel was not triggered, or use throwifcancellationrequested ← thou not sure how it will be parsed.
Maybe CTS could be put in Singleton, that you remember to check when using asyncs?
You can always later use CreateLinkedTokenSource if you find yourself needed second CTS.

It’s asyncs - that means multithreading - that means you won’t be safe, cause cancel can be triggered around same moment that async is removing the element. But y, I think only way would be to be to have fun with SynchronizationContext or lock elements, maybe some semaphore slim. Thou perf is going down

it’s basically what @Sarseth said. there is a static CTS and I register to EditorApplication.playModeStateChanged to cancel it and reset the token
Yield is basically copied from either https://github.com/microsoft/referencesource (or a blog post somewhere that explained how it works), simplified and with playmode checking inside.
Delay just does a normal Task.Delay and if we are not in play mode after that it awaits a forever-pending task
everything else is just linked to the playmode cancellation token

I also have a wrapper that logs uncaught exceptions, except cancellation.

// cancellation token (static constructor)
exitPlayModeSource = new CancellationTokenSource ();
                UnityEditor.EditorApplication.playModeStateChanged += pmsc => {
                    switch (pmsc) {
                        case UnityEditor.PlayModeStateChange.ExitingPlayMode:
                            exitPlayModeSource.Cancel ();
                            exitPlayModeSource = new CancellationTokenSource ();
                            exitPlayMode = exitPlayModeSource.Token;
                            break;
                    }
                };
                exitPlayMode = exitPlayModeSource.Token;
// Yield
#if UNITY_EDITOR
        public static PlayModeYieldAwaitable Yield () => new PlayModeYieldAwaitable ();

        public struct PlayModeYieldAwaitable
        {
            public YieldAwaiter GetAwaiter () => new YieldAwaiter ();
            public struct YieldAwaiter : INotifyCompletion
            {
                public bool IsCompleted => false;

                public void OnCompleted (Action continuation)
                {
                    var syncCtx = SynchronizationContext.Current;
                    if (syncCtx != null)
                        syncCtx.Post (s_CallIfPlaying, continuation);
                    else UnityEditor.EditorApplication.delayCall += () => CallIfPlaying (continuation);
                }

                public void GetResult () {}

                static readonly SendOrPostCallback s_CallIfPlaying = CallIfPlaying;
                static void CallIfPlaying (object continuation)
                {
                    if (Application.isPlaying)
                        ((Action)continuation) ();

                }
            }
        }
#else
        public static YieldAwaitable Yield () => Task.Yield ();
#endif

I wrote a blog post about this issue (and a solution) here: Safe Async Tasks in Unity - Marcos Pereira

2 Likes

For my use case, it worked to simply check if the application is still in play mode after each await call:

await Task.Yield();
if (Application.isPlaying == false)
{
   return; // stop execution of current async method. May have to handle calling methods properly.
}
1 Like