Hi, pretty much just the title. All static variables become null, coroutines stop, but async methods continue executing. Is this intended?
I wouldnât say intended necessarily.
But yes, threads youâve spun up yourself donât necessarily stop when exiting play mode in the editor. Since exiting play mode doesnât stop the program, it just exits execution of scripts and resets the scene.
But then again, static variables shouldnât become null. That is unless they are unity objects, in which case theyâre destroyed and theyâll equate to null due to the == operator overload the unity uses.
Yeah, the âstatic variables shouldnât become nullâ part is why I thought this was a bit strange.
The only workaround I can think of seems to be to add if(Application.IsPlaying) checks after every await which really doesnât seem like the way I should be handling this issue.
Imagine having to micromanage static variables if they didnât reset.
I think the âexpectationâ is that we have a âclean slateâ every time we hit play/stop.
Well, it is in line with how programs generally work. A program is running as long as it has at least one active thread. So outside the editor it should behave the same. Stopping the main thread generally doesnât automatically stop all the other threads, which is intended.
Edit: Iâm under the impression here that stopping the game also resets the state of static variables âsomehowâ, while Async methods continue executing. Iâll have to test tomorrow but maybe the static variables donât get reset until you re-play? Does re-playing trigger an assembly reload which is what actually causes static variables to reset to their default values? Thatâd mean it would kill any lingering async tasks, too. Will have to test tomorrow.
And again, by that logic, static variables shouldnât reset either. But they do. And of course they do, right? Clearly the intention here by Unity was for a âclean slateâ when you hit the stop button. Otherwise it would be a mess. I understand why the asynchronous methods continue executing. The question is whether they should continue.
It makes writing async game logic really painful. You need to perform checks after each await call to ensure the application is still playing. And these checks needs to be in our runtime scripts only because of this editor âquirkâ. For code that executes in a build, this is never an issue, since there is no âpause and replayâ button. So we end up having to write code that needs to have termination points after every await statement just incase we are executing in the editor, when it wonât even be an issue in a real build of the game.
Not to mention other quirks, like consider the following code:
async void Test()
{
string result = await LongRunningTask();//completes in 10 seconds
gameobject.name = result;
}
Imagine this. You invoke Test. And before LongRunningTask completes, you stop and start the game. Because Async methods donât get cancelled, LongRunningTask eventually completes, and attempts to set the name of the GameObject to result. There is code being executed from the previous play session in the new play session.
Not to mention that you wouldnât even be able to work around the issue in this example with a simple ''if(Application.isPlaying)" above line 4 because the application is playing. Itâs not the same âplay sessionâ as the one that invoked the asynchronous method, though.
Thanks for this thread. I just encountered this in Unity 2018.2.2f1.
It feels very much like a bug to me, especially since async methods in fact run in the playerâs Main Thread, and Unityâs SynchronizationContext should really be able to terminate the tasks since, in my understanding, it is already able to collect them to force them to run into the main thread.
Still occurring in Unity 2019.2.1f1. This is an important issue, Unity should look into this ASAP.
Still happening in Unity 2019.3.0f3
Still happening in Unity 2019.3.1f1. This is EMERGENCY!
lol anyone know how to stop this dang threads. holy cow i just spawned like 1000 toasters after exiting play mode
EDIT: this link helped me. you need to make a cancellation token source then pass the sourceâs cancellation token as a parameter in you Task method.
in my case i am saying:
var tokenSource = new System.Threading.CancellationTokenSource();
tokenSource.Token.ThrowIfCancellationRequested();
await Task.Delay(time, tokenSource.token);
then when i want to cancel it, which i do OnApplicationQuit():
tokenSource.Cancel();
This should be done by default in Unityâs SynchronizationContext, but alas it is not, as evidenced by their latest documentation (see Limitations of async and await tasks).
Maybe someone can report it in the editor as a bug so they can track it appropriately?
In the meantime, Iâm using a wrapper method that I call instead of Task.Run()
, which ignores the result of a task if play mode was exited while it was running. Iâm using CancellationToken for that.
@jwlondon98 I wouldnât use OnApplicationQuit()
, playModeStateChanged
should work better as you donât need to place it in a MonoBehavior
, and it runs in the editor only.
The code I came up with in case it may help someone: Cancel async tasks in Unity upon exiting play mode ¡ GitHub
I never liked the coroutines, when I saw that async/await is available in unity I decided to try it.
I created some Debug.Log(âhello worldâ) in async method.
Pressed play and awesome it works !!
Pressed stop and sucks it still works !!
Now It is hard to imagine that someone made this feature and did not think it is a problem that needs official solution.
But to not add only rant to this thread.
What do you think about this ugly solution of mine.
Also instead of breaking we could also set the cancellation token for internal Task
async ValueTask Start() {
ValueTask task = UpdateLoopAsync(this);
await task;
}
async ValueTask UpdateLoopAsync(MonoBehaviour behaviour) {
while (true) {
if(behaviour == null) {
break;
}
Debug.Log(Time.time);
await Task.Yield();
}
}
Suppose itâs time for my scheduled recommendation of UniTask which does all the proper integration of asynchronous tasks in Unity so you donât have to.
Yes I know this one, but I like to stick with official stuff.
Same here, Iâm very reluctant on adopting heavy handed packages when a simpler solution that I can fully understand will do
Do you know how task sheduler is âtickingâ (when/how often for example) in Unity async ?
Thereâs âLimitations of async and await tasksâ here and this. Also from the profiler, it seems like pending tasks execute in the âplayer loopâ, on update.
Thanks for the heads up. Since I donât see an easy way to cancel my complex nested tasks when the editor leaves play mode, I think Iâll try this instead.
Why on earth does Unity fail so utterly completely in this department? It makes NO sense to keep the tasks running after leaving play mode. I spawn a bunch of objects in tasks, so even if I detect the play mode change, I still have to re-destroy the spawned objects, which feels like a very dirty and possibly-in-corner-cases failing solution, which means I might end up with spawned objects in my scene that accidentally get saved and committed.
I looked into this and came up with a way of spinning up tasks in separate threads (using Task.Run()) through a dedicated class that ensures tasks are terminated upon leaving play mode. I wrote about it here
Here is an easy solution for looping tasks (like periodically syncing Lobby player data).
Just check if the editor is in Play Mode before running the sync again.
using System;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEditor;
// ensure class initializer is called whenever scripts recompile
[InitializeOnLoadAttribute]
public class LobbyManager : MonoBehaviour
{
// We want this to start as true in development
private static bool _isEditorInPlayMode = true;
LobbyManager()
{
if(Application.isEditor) {
_isEditorInPlayMode = false; // Set to false in the editor
UnityEditor.EditorApplication.playModeStateChanged += LogPlayModeState; // Monitor change
}
}
private static void LogPlayModeState(PlayModeStateChange state)
{
_isEditorInPlayMode = state == PlayModeStateChange.EnteredPlayMode;
}
private async void PeriodicallyRefreshLobby()
{
_updateLobbySource = new CancellationTokenSource();
await Task.Delay(2 * 1000);
while (!_updateLobbySource.IsCancellationRequested && lobby != null && _isEditorInPlayMode)
{
lobby = await Lobbies.Instance.GetLobbyAsync(lobby.Id);
UpdateUserInterface();
await Task.Delay(2 * 1000);
}
}
}