Async methods do not stop executing when exiting play mode. Intended?

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. :slight_smile:

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.

1 Like

Still occurring in Unity 2019.2.1f1. This is an important issue, Unity should look into this ASAP.

1 Like

Still happening in Unity 2019.3.0f3

1 Like

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 :hushed:

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();

6 Likes

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

3 Likes

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 !! :slight_smile:
Pressed stop and sucks it still works !! :frowning:

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();
        }
    }
1 Like

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.

6 Likes

Yes I know this one, but I like to stick with official stuff.

2 Likes

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.

1 Like

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);
        }
    }
}