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…
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.
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.
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.
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.
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.
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.
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!”.
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)
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)
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.
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.
}