Search Unity

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

Discussion in 'Scripting' started by Prodigga, Aug 9, 2018.

  1. Prodigga

    Prodigga

    Joined:
    Apr 13, 2011
    Posts:
    1,123
    Hi, pretty much just the title. All static variables become null, coroutines stop, but async methods continue executing. Is this intended?
     
  2. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,537
    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.
     
  3. Prodigga

    Prodigga

    Joined:
    Apr 13, 2011
    Posts:
    1,123
    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.
     
  4. jvo3dc

    jvo3dc

    Joined:
    Oct 11, 2013
    Posts:
    1,520
    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.
     
  5. Prodigga

    Prodigga

    Joined:
    Apr 13, 2011
    Posts:
    1,123
    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:

    Code (CSharp):
    1. async void Test()
    2. {
    3.     string result = await LongRunningTask();//completes in 10 seconds
    4.     gameobject.name = result;
    5. }
    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.
     
    Last edited: Aug 9, 2018
  6. Brice-xCIT

    Brice-xCIT

    Joined:
    Jun 22, 2017
    Posts:
    5
    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.
     
    mugglerone likes this.
  7. AlexINF

    AlexINF

    Joined:
    Aug 12, 2016
    Posts:
    1
    Still occurring in Unity 2019.2.1f1. This is an important issue, Unity should look into this ASAP.
     
    pinkmustachecat likes this.
  8. CPlusSharp22

    CPlusSharp22

    Joined:
    Dec 1, 2012
    Posts:
    111
    Still happening in Unity 2019.3.0f3
     
    pinkmustachecat likes this.
  9. pinkmustachecat

    pinkmustachecat

    Joined:
    Nov 9, 2019
    Posts:
    4
    Still happening in Unity 2019.3.1f1. This is EMERGENCY!
     
  10. jwlondon98

    jwlondon98

    Joined:
    Mar 27, 2015
    Posts:
    12
    lol anyone know how to stop this dang threads. holy cow i just spawned like 1000 toasters after exiting play mode :eek:

    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:

    Code (CSharp):
    1. var tokenSource = new System.Threading.CancellationTokenSource();
    2. tokenSource.Token.ThrowIfCancellationRequested();
    3. await Task.Delay(time, tokenSource.token);

    then when i want to cancel it, which i do OnApplicationQuit():
    tokenSource.Cancel();
     
    Last edited: Oct 17, 2020
  11. marcospgp

    marcospgp

    Joined:
    Jun 11, 2018
    Posts:
    194
    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: https://gist.github.com/marcospgp/291a8239f5dcb1a326fad37d624f3630#file-safetask-cs
     
    Last edited: May 10, 2022
    ontrigger, gzeeebra and Sluggy like this.
  12. koirat

    koirat

    Joined:
    Jul 7, 2012
    Posts:
    2,074
    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
    Code (csharp):
    1.  
    2.     async ValueTask Start() {
    3.         ValueTask task = UpdateLoopAsync(this);
    4.         await task;
    5.     }
    6.  
    7.  
    8.     async ValueTask UpdateLoopAsync(MonoBehaviour behaviour) {
    9.  
    10.         while (true) {
    11.             if(behaviour == null) {
    12.                 break;
    13.             }
    14.  
    15.             Debug.Log(Time.time);
    16.             await Task.Yield();
    17.         }
    18.     }
    19.  
     
    Last edited: May 10, 2022
    gzeeebra likes this.
  13. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,930
    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.
     
  14. koirat

    koirat

    Joined:
    Jul 7, 2012
    Posts:
    2,074
    Yes I know this one, but I like to stick with official stuff.
     
    konsnos likes this.
  15. marcospgp

    marcospgp

    Joined:
    Jun 11, 2018
    Posts:
    194
    Same here, I'm very reluctant on adopting heavy handed packages when a simpler solution that I can fully understand will do
     
  16. koirat

    koirat

    Joined:
    Jul 7, 2012
    Posts:
    2,074
    Do you know how task sheduler is "ticking" (when/how often for example) in Unity async ?
     
    Last edited: May 10, 2022
  17. marcospgp

    marcospgp

    Joined:
    Jun 11, 2018
    Posts:
    194
    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.
     
  18. Raveler

    Raveler

    Joined:
    Sep 24, 2012
    Posts:
    40
    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.
     
    Snoozed likes this.
  19. marcospgp

    marcospgp

    Joined:
    Jun 11, 2018
    Posts:
    194
    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
     
  20. ViktorMSc

    ViktorMSc

    Joined:
    May 28, 2022
    Posts:
    14
    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.
    Code (CSharp):
    1. using System;
    2. using System.Threading;
    3. using System.Threading.Tasks;
    4. using UnityEngine;
    5. using UnityEditor;
    6.  
    7. // ensure class initializer is called whenever scripts recompile
    8. [InitializeOnLoadAttribute]
    9. public class LobbyManager : MonoBehaviour
    10. {
    11.     // We want this to start as true in development
    12.     private static bool _isEditorInPlayMode = true;
    13.  
    14.     LobbyManager()
    15.     {
    16.         if(Application.isEditor) {
    17.             _isEditorInPlayMode = false; // Set to false in the editor
    18.             UnityEditor.EditorApplication.playModeStateChanged += LogPlayModeState; // Monitor change
    19.         }
    20.     }
    21.  
    22.     private static void LogPlayModeState(PlayModeStateChange state)
    23.     {
    24.         _isEditorInPlayMode = state == PlayModeStateChange.EnteredPlayMode;
    25.     }
    26.  
    27.     private async void PeriodicallyRefreshLobby()
    28.     {
    29.         _updateLobbySource = new CancellationTokenSource();
    30.         await Task.Delay(2 * 1000);
    31.         while (!_updateLobbySource.IsCancellationRequested && lobby != null && _isEditorInPlayMode)
    32.         {
    33.             lobby = await Lobbies.Instance.GetLobbyAsync(lobby.Id);
    34.             UpdateUserInterface();
    35.             await Task.Delay(2 * 1000);
    36.         }
    37.     }
    38. }
     
  21. Tymac

    Tymac

    Joined:
    Nov 12, 2015
    Posts:
    10
    To handle this issue properly you should be using CancellationTokenSource. Here is an example of how to set it up and plug it into a static generic wait method:

    Code (CSharp):
    1.  
    2.       public static CancellationTokenSource AppClosingToken;
    3.  
    4.       [RuntimeInitializeOnLoadMethod]
    5.       static void RunOnStart()
    6.       {
    7.            Application.quitting += ApplicationQuitting;
    8.            AppClosingToken = new CancellationTokenSource();
    9.       }
    10.  
    11.       private static void ApplicationQuitting()
    12.       {
    13.            //Run any cleanup operations....
    14.            AppClosingToken.Cancel();
    15.            AppClosingToken.Token.ThrowIfCancellationRequested();
    16.       }
    17.  
    18.       public static async void Wait<T>(Action<T> _ActionCallBack, T _GenericRef, float _WaitSeconds)
    19.       {
    20.             int sleepSeconds = (int)(_WaitSeconds * 1000f);
    21.             await Task.Run(() =>
    22.             {
    23.                 Thread.Sleep(sleepSeconds);
    24.                 if (AppClosingToken.Token.IsCancellationRequested)
    25.                     AppClosingToken.Token.ThrowIfCancellationRequested();
    26.             }, AppClosingToken.Token);
    27.  
    28.             if (_ActionCallBack != null)
    29.                 _ActionCallBack.Invoke(_GenericRef);
    30.        }
    31.  
    The AppClosingToken will throwIfCancellationRequested in any active async wait operations when exiting play for all active Waiting operations using the Wait method.

    It should be noted that calling await.Run is extremely slow and should not be implemented in any framework design that could have numerous Wait calls queued up. A better solution to waiting for any script that has an Updater (Update,FixedUpdate, LateUpdate) would be to set a float waitTimer at the start of the Updater that blocks if waitTimer has been set with a delay:
    Code (CSharp):
    1.  
    2.     private void Update()
    3.     {
    4.         if(waitTimer != 0f)
    5.         {
    6.             waitTimer-= Time.deltaTime;
    7.             if(waitTimer < 0f)
    8.                 waitTimer = 0f;
    9.         }
    10.     }
     
    Last edited: Jun 28, 2023
  22. Tymac

    Tymac

    Joined:
    Nov 12, 2015
    Posts:
    10
    FYI - example on how to call the Wait method from a GameManager singleton script:
    Code (CSharp):
    1. GameManager.Wait<float>( MyFloatCallback, [float], waitDuration);
    2. private void MyFloatCallback(float _FloatValue) { ...}
    Or any object type (such as GameObject):
    Code (CSharp):
    1. GameManager.Wait<GameObject>( MyGameObjectCallback, [GameObject], waitDuration);
    2. private void MyGameObjectCallback(GameObject _GameObject) { ...}
    3.  
     
  23. MaskedMouse

    MaskedMouse

    Joined:
    Jul 8, 2014
    Posts:
    1,092
    CodeRonnie, lordofduct and Bunny83 like this.
  24. marcospgp

    marcospgp

    Joined:
    Jun 11, 2018
    Posts:
    194
  25. MaskedMouse

    MaskedMouse

    Joined:
    Jul 8, 2014
    Posts:
    1,092
    When exiting playmode all game objects are destroyed if I'm not mistaken. Thus the
    destroyCancellationToken
    would be cancelled. Any task running can just check whether it is cancelled and handle it.
    Even during runtime, if you destroy the component or game object the
    destroyCancellationToken
    would be cancelled.
    So I don't see why it wouldn't make sense. It's the end of the lifetime of the component and thus also the task (if it was busy)
     
  26. KevinCastejon

    KevinCastejon

    Joined:
    Aug 10, 2021
    Posts:
    109
    ok we can use CancellationToken with our own async Task-based methods.. but what about the native ones?
    Unity services are all using async methods that returns Task, none of these offer a way to pass a cancellationtoken and all of them continue running after exiting playmode, it's a real pain as it callbacks things on the editor while it's not even running (making it fail to retrieve the "pre-playmode" values, positions, etc...)
     
    Finijumper and Prodigga like this.
  27. Raveler

    Raveler

    Joined:
    Sep 24, 2012
    Posts:
    40
    Just use Unitask, it’s free, does everything that Unity’s S***ty implementation does but does it better and does a lot more as well. Don’t bother trying to fix Unity’s system.
     
  28. KevinCastejon

    KevinCastejon

    Joined:
    Aug 10, 2021
    Posts:
    109
    I like avoiding third party tool...
    I've find a solution on another post that I can't find again but here is the script to paste in an "Editor" folder, this will prevent Tasks from keep running after exiting playmode


    Code (CSharp):
    1. using System.Reflection;
    2. using System.Threading;
    3. using UnityEditor;
    4. using UnityEngine;
    5.  
    6. public static class SynchronizationContextUtils
    7. {
    8.     [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    9.     private static void OnBeforeSceneLoad()
    10.     {
    11.         EditorApplication.playModeStateChanged += state =>
    12.         {
    13.             if (state == PlayModeStateChange.ExitingPlayMode)
    14.             {
    15.                 OnPlayModeExit();
    16.             }
    17.         };
    18.     }
    19.  
    20.     private static void OnPlayModeExit()
    21.     {
    22.         KillCurrentSynchronizationContext();
    23.     }
    24.  
    25.     /// <summary>
    26.     /// Kills the current synchronization context after exiting play mode to avoid Tasks continuing to run.
    27.     /// </summary>
    28.     private static void KillCurrentSynchronizationContext()
    29.     {
    30.         var synchronizationContext = SynchronizationContext.Current;
    31.  
    32.         var constructor = synchronizationContext
    33.             .GetType()
    34.             .GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { typeof(int) }, null);
    35.  
    36.         if (constructor == null)
    37.         {
    38.             return;
    39.         }
    40.  
    41.         object newContext = constructor.Invoke(new object[] { Thread.CurrentThread.ManagedThreadId });
    42.         SynchronizationContext.SetSynchronizationContext(newContext as SynchronizationContext);
    43.     }
    44. }
    45.  
     
  29. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,930
    This is a third party tool you're doing yourself a disservice by not using.

    Allocation-free async code is too good to pass on.
     
    StarManta, Nad_B and lordofduct like this.
  30. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,537
    I second this statement.

    Unitask is top-notch.
     
    Nad_B likes this.
  31. CodeRonnie

    CodeRonnie

    Joined:
    Oct 2, 2015
    Posts:
    531
    What are all of these Unity services you refer to, and does killing the synchronization context solve the problem? It seems like a drastic step.
     
  32. KevinCastejon

    KevinCastejon

    Joined:
    Aug 10, 2021
    Posts:
    109
    I'm talking about any unity service as Relay, Lobby, Economy, Friends, etc... all are using async methods.
    Code (CSharp):
    1. LobbyService.Instance.CreateLobbyAsync()
    From what i've tested now it has completely solved the issue of tasks keeping run after exiting playmode.
    But you're right that I don't really know what it implies to kill sync and if it's gonna generate trouble or not.
     
  33. CodeRonnie

    CodeRonnie

    Joined:
    Oct 2, 2015
    Posts:
    531
    I see. I don't use those personally, but if they are indeed not playing well with the editor I would be inclined to raise a bug report, and/or a feature request for them to take a cancellation token just as is recommended practice for pretty much any async task.

    I am also not an expert on async synchronization contexts. I have looked into async tasks enough to see that they are pretty much like coroutines under the hood. They are both syntactic sugar that the compiler turns into secret objects that implement a state machine of callbacks to maintain asynchronous operations and continuations. But, when it comes to synchronization context manipulation, the thread pool, and other deep lore, my understanding gets a bit fuzzy.
     
    Last edited: Oct 31, 2023
    Nad_B likes this.
  34. Finijumper

    Finijumper

    Joined:
    Jul 12, 2016
    Posts:
    79
    I was using UniTask and I'm facing the same issue.

    I want to convert a callback-style code into async. So I'm doing something like:
    Code (CSharp):
    1. public void GetStringFromUrl(string url, Action<string> onCompleted);
    2.  
    3. public UniTask<string> GetStringFromUrl(string url)
    4. {
    5.     var t = new UniTaskCompletionSource<string>();
    6.     GetStringFromUrl(url, s => t.TrySetResult(s));
    7.     return t.Task;
    8. }
    But the issue is, if I await this UniTask<string> and I stop the game in the editor, when the callback hasn't been called yet, the thread is still awaiting in the background.

    I was hoping UniTask would not behave like that :(

    Any suggestions for converting callback-style to await async with UniTask?
     
  35. Nad_B

    Nad_B

    Joined:
    Aug 1, 2021
    Posts:
    730
    UniTask has a GetCancellationTokenOnDestroy() you can get from a MonoBehaviour. Just add a CancellationToken parameter to your method and pass it, then combine it with your UniTaskCompletionSource.

    But I see that your Task calls a non async method GetStringFromUrl()... so you don't have a full chain of async tasks... so basically there's no way to do a proper cancellation (I mean your UniTask GetStringFromUrl will be cancelled, but your synchronous GetStringFromUrl will always finish executing)
     
  36. Finijumper

    Finijumper

    Joined:
    Jul 12, 2016
    Posts:
    79
    I am awaiting the Task from a non-MonoBehaviour class.
     
  37. KevinCastejon

    KevinCastejon

    Joined:
    Aug 10, 2021
    Posts:
    109
    Did you try the script I uploaded on my previous answer?
     
  38. Yuchen_Chang

    Yuchen_Chang

    Joined:
    Apr 24, 2020
    Posts:
    127
    Application.exitCancellationToken
    is also a choice, if you're using Unity 2022.2 above.
     
  39. Deleted User

    Deleted User

    Guest

    Where can I read more information about this? The unity docs don’t have much.
     
  40. Yuchen_Chang

    Yuchen_Chang

    Joined:
    Apr 24, 2020
    Posts:
    127
  41. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,930
    Like above it's worth noting a lot of this async support has only really hit Unity 2023.

    Async coding is a general C# thing too, so you can learn about it separate to Unity and apply that knowledge to Unity.
     
  42. Epsilon_Delta

    Epsilon_Delta

    Joined:
    Mar 14, 2018
    Posts:
    258
    I tried the Application.exitCancellationToken in 2022.3.0 and it does not work when exiting playmode.
    Or I have a bug in my code:
    Code (CSharp):
    1.     public static async Task Delay(float seconds)
    2.     {
    3.         var elapsed = 0f;
    4.         while (elapsed < seconds)
    5.         {
    6.             if (Application.exitCancellationToken.IsCancellationRequested)
    7.                 return;
    8.             elapsed += Time.deltaTime;
    9.             await Task.Yield();
    10.         }
    11.     }
    Edit: I tested 2022.3.22 and it indeed does not work, so I filed a bug report. Also tested MonoBehviour.destroyCancellationToken and that works fine.
     
    Last edited: Mar 28, 2024