Search Unity

  1. Unity Asset Manager is now available in public beta. Try it out now and join the conversation here in the forums.
    Dismiss Notice
  2. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  3. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Non-stopping async method after in-editor game is stopped

Discussion in 'Experimental Scripting Previews' started by lluo_lilith, Sep 20, 2018.

  1. lluo_lilith

    lluo_lilith

    Joined:
    Aug 20, 2018
    Posts:
    1
    Unity3D version: 2018.3.0b1
    1. Create an empty object in a scene
    2. Add the following script to the newly created object
    3. Click the "Run" button inside the editor
    4. Observe the Console output like:
    > TestAsync(): (0, hello)
    5. Click the "Run" button again, so the in-editor game would stop, observe the console output, you would see that the "TestAsync()" output continues
    6. If the script is reloaded (e.g. edit and save), the message output stops.

    I'm wondering that the Unity SynchronizationContext should be created per game inside the editor, so when the game is stopped, the game specific SynchronizationContext should be stopped as well, so the async method should effectively not be active anymore?

    Code (CSharp):
    1. using System;
    2. using System.Threading.Tasks;
    3.  
    4. using UnityEngine;
    5.  
    6. public class Test : MonoBehaviour
    7. {
    8.     // Start is called before the first frame update
    9.     void Start()
    10.     {
    11.         TestAsync();
    12.     }
    13.  
    14.     // Update is called once per frame
    15.     void Update()
    16.     {
    17.      
    18.     }
    19.  
    20.     async void TestAsync()
    21.     {
    22.         int i = 0;
    23.         while (true)
    24.         {
    25.             await Task.Delay(TimeSpan.FromSeconds(1));
    26.  
    27.             Debug.Log($"TestAsync(): {(i++, "hello")}");
    28.         }
    29.     }
    30. }
    31.  
     
  2. mkderoy

    mkderoy

    Unity Technologies

    Joined:
    Oct 25, 2016
    Posts:
    22
    This behavior is expected, and is the same behavior as if you spawned a thread that loops and prints output. The next time you enter playmode (or edit a script), a domain reload occurs, which clears up any tasks that were being queued.
     
    VOTRUBEC likes this.
  3. Dejavu2017

    Dejavu2017

    Joined:
    Sep 29, 2017
    Posts:
    14
    How is this expected behavior? Do you mean when the playmode is exited, some of the long running user scripts belonging to the current playmode *session* would still keep running? this would cause a lot of confusion and unexpected behavior. Specifically, it would be reasonable to stop polling the SynchronizationContext of the play mode when it's not active - are there independent SynchronizationContext's for different mode inside the editor? e.g. the editor itself might have it's own Sync Context, while each new playmode session would have its own SyncContext.
     
  4. Prodigga

    Prodigga

    Joined:
    Apr 13, 2011
    Posts:
    1,123
    Bump. @mkderoy , you chimed in here too:
    https://forum.unity.com/threads/asy...or-potentially-dangerous.591235/#post-3954637

    With a similar response. I was wondering if there was any movement on this? You mentioned you guys were looking into an overarching solution which is great! I am just pinging you and asking for an update to see if there has been any development? Async methods are new and exciting for Unity devs, but we're avoiding them until there is some movement on this 'issue'. :(
     
  5. joncham

    joncham

    Unity Technologies

    Joined:
    Dec 1, 2011
    Posts:
    276
    There is no movement on this topic. It is unlikely to get any movement in near future.
     
  6. Prodigga

    Prodigga

    Joined:
    Apr 13, 2011
    Posts:
    1,123
    Ok thanks for the transparency!
     
  7. jasonmcguirk

    jasonmcguirk

    Joined:
    Apr 20, 2013
    Posts:
    10
    Heya, this started hitting me recently in editor as I've added a few simulated delays to better reflect real world conditions. This lead to orphaned game objects in the scene that were created after the task was resolved and after the application stopped playing.

    After trying to poke into the SyncContext and try and flush state, I decided to set the SyncContext.Current to a new instance on ApplicationQuit() in editor.


    Code (CSharp):
    1.      
    2.        void OnApplicationQuit()
    3.        {
    4.            #if UNITY_EDITOR
    5.                var constructor = SynchronizationContext.Current.GetType().GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] {typeof(int)}, null);
    6.                var newContext = constructor.Invoke(new object[] {Thread.CurrentThread.ManagedThreadId });
    7.                SynchronizationContext.SetSynchronizationContext(newContext as SynchronizationContext);  
    8.            #endif
    9.        }
    10.  
    This appears to do the trick for me (no more game objects being tossed into the scene after stop, and the logs indicate the tasks are not being resolved).

    Note two things

    1.) This leverages the internal constructor on UnitySynchronizationContext - There is no guarantee this won't change in a subsequent update. If this makes you feel queasy, You can toss in an empty SynchronizationContext(), but I think if you are relying on async/await for editor functionality, it might be busted until next domain reload (exercise left to the reader).
    2.) This leaves your orphaned tasks in a somewhat undefined state until the next domain reload. These are presumably blocked on the orphaned context and won't be "cleaned up" until the next domain reload. I haven't noticed any fall out here just yet.

    Cheers!
     
  8. BimicoreEnt

    BimicoreEnt

    Joined:
    Feb 18, 2019
    Posts:
    5

    It worked for me. Thanks!
     
  9. TheZombieKiller

    TheZombieKiller

    Joined:
    Feb 8, 2013
    Posts:
    265
    The "proper" way to solve this is to use CancellationToken, which is .NET's implementation of cooperative cancellation (it's a good idea to make any async/threaded APIs support cancellation anyway).

    Given a helper class like the following:
    Code (CSharp):
    1. using System.Threading;
    2. using UnityEngine;
    3. #if UNITY_EDITOR
    4. using UnityEditor;
    5. #endif
    6.  
    7. #if UNITY_EDITOR
    8. [InitializeOnLoad]
    9. #endif
    10. public static class ThreadingUtility
    11. {
    12.     static readonly CancellationTokenSource quitSource;
    13.  
    14.     public static CancellationToken QuitToken { get; }
    15.  
    16.     public static SynchronizationContext UnityContext { get; private set; }
    17.  
    18.     static ThreadingUtility()
    19.     {
    20.         quitSource = new CancellationTokenSource();
    21.         QuitToken  = quitSource.Token;
    22.     }
    23.  
    24. #if UNITY_EDITOR
    25.     [InitializeOnLoadMethod]
    26. #endif
    27.     [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    28.     static void MainThreadInitialize()
    29.     {
    30.         UnityContext          = SynchronizationContext.Current;
    31.         Application.quitting += quitSource.Cancel;
    32.     #if UNITY_EDITOR
    33.         EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
    34.     #endif
    35.     }
    36.  
    37. #if UNITY_EDITOR
    38.     static void OnPlayModeStateChanged(PlayModeStateChange state)
    39.     {
    40.         if (state == PlayModeStateChange.ExitingPlayMode)
    41.             quitSource.Cancel();
    42.     }
    43. #endif
    44. }
    The example in the OP becomes:
    Code (CSharp):
    1. public class Test : MonoBehaviour
    2. {
    3.     async void Start()
    4.     {
    5.         // Start is async to ensure exceptions aren't lost
    6.         await TestAsync(ThreadingUtility.QuitToken);
    7.     }
    8.  
    9.     async Task TestAsync(CancellationToken token)
    10.     {
    11.         for (int i = 0; ; i++)
    12.         {
    13.             // If you don't want to throw a cancellation exception, you can do
    14.             // the following instead of calling ThrowIfCancellationRequested:
    15.             //
    16.             //  if (token.IsCancellationRequested)
    17.             //      return;
    18.             //
    19.             token.ThrowIfCancellationRequested();
    20.             await Task.Delay(TimeSpan.FromSeconds(1));
    21.             Debug.LogFormat("{0}: ({1}, hello)", nameof(TestAsync), i);
    22.         }
    23.     }
    24. }
     
  10. Develax

    Develax

    Joined:
    Nov 14, 2017
    Posts:
    67
    Shouldn't it be implemented as the default behavior? For Unity users, the current default behavior does not make any sense and makes them write crutches.
     
    JoNax97 likes this.
  11. AbelSierra

    AbelSierra

    Joined:
    Jul 19, 2018
    Posts:
    19
    I understand the confusion about tasks for people that are just starting to use them.
    But this is the expected behaviour for parallel programming, you need to take care of your different processes and stop/cancel them when you need. Imagine, for instance, that you want a process to run after you exit for storing/saving some data, if this thread is stopped the data will be corrupt. Or for instance you have a timer that should work no matter what, even if the app is paused, if the timer is paused too when you unpause the app won't be in the state you wanted it to stay.

    Tasks are threads, which are concurrent/parallel processes and these are an advanced feature and you need to know how to use them wisely and carefully. I wouldn't use it if I am just starting with unity or do not have any experience in parallelization.

    The current default behaviour is what it is because it cannot be different. Unity cannot decide by itself when you want that process to stop or pause.

    For stopping tasks when stopping the editor or closing the app you just use CancellationToken and check inside a long run loop


    Code (CSharp):
    1.  
    2.  
    3. void OnEnable()
    4. {
    5.             cancelationToken = new CancellationTokenSource();
    6. }
    7.  
    8. void OnApplicationQuit()
    9. {
    10.             cancelationToken.Cancel();
    11. }
    12.  
    13. public async Task LongProcess()
    14. {
    15.  
    16.       while (true)
    17.        {
    18.                Debug.Log("Long task running");
    19.  
    20.                 await Task.Delay(20);
    21.  
    22.                     if (cancelationToken.IsCancellationRequested)//cancelation token to break the task when app exists or editor stops
    23.                         return;
    24.  
    25. #if UNITY_EDITOR
    26.                     await new WaitUntil(() => !EditorApplication.isPaused);//Wait while editor is paused
    27. #endif
    28.  
    29.        }
    30. }
    31.  
    32.  
    The WaitUntil functionality is not a default behaviour but an extension for Tasks. You can find different approximations for getting this functionality like in here. here: https://stackoverflow.com/questions/29089417/c-sharp-wait-until-condition-is-true
     
    Bodin likes this.
  12. Develax

    Develax

    Joined:
    Nov 14, 2017
    Posts:
    67
    Tasks are (still) not threads and async is not parallel especially in Unity (if you don't make them threads explicitly).

    In most cases, they are used as a coroutines replacement. This simply means they just "tick" every Update. And when the game stops all Updates should be stopped. Only in rear cases Task is required to survive from Play mode to Editor mode.

    For routine cases developers just want to use them as "improved coroutines" without having to write this kind of boilerplate every time. Isn't it logical to have a simple syntax for most cases and to write a boilerplate only for special ones?
     
    blisz, koirat and VolodymyrBS like this.
  13. p3k07

    p3k07

    Joined:
    Mar 30, 2014
    Posts:
    10
    I had an idea for using a public static cancelation token, but man, you knocked it out the park with ThreadingUtility.
    The final solution for people still looking, imo, is suck it up and add the token to your method. Either way, you're going to need more lines of code to exit the method correctly without issue. TheZombieKillers scripts takes < 30seconds to implement and put in your loops.

    Cheers man!
     
  14. huulong

    huulong

    Joined:
    Jul 1, 2013
    Posts:
    224
    Coming from https://github.com/jeffreylanters/unity-tweens where stopping the game inside async method before adding another tween will add an unwanted Tween (Driver) component on your game object in the scene, in Edit mode! It makes the scene more and more dirty as you playtest, I had dozens of those components on my game objects!

    I'll see with the developer if they can add a quick if (Application.isPlaying) test before adding stuff, surrounded by UNITY_EDITOR I guess.

    But in the meantime, your two solutions will help me a lot!

    A. jasonmcguirk's SynchronizationContext switch works out of the box.
    B. @TheZombieKiller it worked the first time, but subsequent plays would not renew the token (due to static lifetime). I had to renew it on Enter Play Mode, with this code:

    Code (CSharp):
    1. // https://forum.unity.com/threads/non-stopping-async-method-after-in-editor-game-is-stopped.558283/
    2. // original code by TheZombieKiller
    3.  
    4. using System.Threading;
    5. using UnityEngine;
    6. #if UNITY_EDITOR
    7. using UnityEditor;
    8. #endif
    9.  
    10. #if UNITY_EDITOR
    11. [InitializeOnLoad]
    12. #endif
    13. public static class ThreadingUtility
    14. {
    15.     // huulong: REMOVED readonly
    16.     static /*readonly*/ CancellationTokenSource quitSource;
    17.     // huulong: ADDED private set
    18.     public static CancellationToken QuitToken { get; private set; }
    19.     public static SynchronizationContext UnityContext { get; private set; }
    20.     static ThreadingUtility()
    21.     {
    22.         quitSource = new CancellationTokenSource();
    23.         QuitToken  = quitSource.Token;
    24.     }
    25. #if UNITY_EDITOR
    26.     [InitializeOnLoadMethod]
    27. #endif
    28.     [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    29.     static void MainThreadInitialize()
    30.     {
    31.         UnityContext          = SynchronizationContext.Current;
    32.         Application.quitting += quitSource.Cancel;
    33.     #if UNITY_EDITOR
    34.         EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
    35.     #endif
    36.     }
    37. #if UNITY_EDITOR
    38.     static void OnPlayModeStateChanged(PlayModeStateChange state)
    39.     {
    40.         if (state == PlayModeStateChange.ExitingPlayMode)
    41.         {
    42.             quitSource.Cancel();
    43.         }
    44.         // ADDED by huulong
    45.         else if (state == PlayModeStateChange.EnteredPlayMode)
    46.         {
    47.             // renew quit source and token, otherwise
    48.             // after restart game, it will still be considered cancelled
    49.             // and all ThrowIfCancellationRequested will throw
    50.             // quitSource.Dispose();
    51.             quitSource = new CancellationTokenSource();
    52.             QuitToken  = quitSource.Token;
    53.         }
    54.     }
    55. #endif
    56. }
     
  15. RunninglVlan

    RunninglVlan

    Joined:
    Nov 6, 2018
    Posts:
    182
    Hey, I'm not that experienced with async yet, but how is this different from using a flag?
    Code (CSharp):
    1. bool stopAsync;
    2. void OnEnable() { stopAsync = false; }
    3. void OnApplicationQuit() { stopAsync = true; }
    4. // ...
    5. if (stopAsync) { return; } // in async code
     
  16. xucian

    xucian

    Joined:
    Mar 7, 2016
    Posts:
    836
    Is this still not implemented by default? I just thought there's something wrong in my code and then came across this thread. This is a fundamental functionality to have in a system... when you exit a session, no pending jobs should continue
     
  17. DragonCoder

    DragonCoder

    Joined:
    Jul 3, 2015
    Posts:
    1,677
    Unlike Coroutines which are an Unity wrapper around another language feature, Async is purely a language feature. Part of that language feature is the cancellation token. Unity cannot magically inject it into your code though.
    A domain reload when stopping is the only way how the engine could enforce that all tasks are stopped and that would be a significant performance degradation of the editor unfortunately.

    It's as usual: Convenience either comes with risks or performance drawbacks.

    Edit: Wonder whether this library covers that issue: https://github.com/Cysharp/UniTask
     
    Last edited: Sep 2, 2022
  18. bdovaz

    bdovaz

    Joined:
    Dec 10, 2011
    Posts:
    1,042
    In 2022.2.x they have added:

    https://docs.unity3d.com/2022.2/Doc...e/MonoBehaviour-destroyCancellationToken.html

    This is the point where the async code must be cancelled.
     
    xucian, Twibby, Huszky and 2 others like this.
  19. xucian

    xucian

    Joined:
    Mar 7, 2016
    Posts:
    836
    That's a step in the right direction. Also, UniTask already provides something like that, but it's not ideal, as it has to add a component to the game object at runtime.
    As a workaround or just an additional safety measure, I recommend just manually reloading the domain when you detect exiting play mode.
    This is what I ended up doing and it works pretty well, with the small price of a modal is shown for a few seconds after exiting play mode:
    Code (CSharp):
    1.  
    2.         [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    3.         static void OnBeforeFirstSceneLoad_Static()
    4.         {
    5.             UnityEditor.EditorApplication.playModeStateChanged += state =>
    6.             {
    7.                 if (state == UnityEditor.PlayModeStateChange.ExitingPlayMode)
    8.                 {
    9.                     Debug.Log("[AppInit] Reloading scripts to prevent dangling Tasks from continuing executing in edit mode");
    10.                     UnityEditor.EditorUtility.RequestScriptReload();
    11.                     // Not sure if needed
    12.                     UnityEditor.EditorApplication.isPaused = paused;
    13.                 }
    14.             };
    15.         }
     
    sandolkakos, justtime and Prodigga like this.
  20. sandolkakos

    sandolkakos

    Joined:
    Jun 3, 2009
    Posts:
    282
    I was also having problems with my Tasks continuing to run after exiting the PlayMode. They were still playing some audio clips, creating new objects in the scene, changing the scene states, etc... And to be worse, without marking the scene as changed.

    In case anyone is still needing a solution for that kind of problem, here is the class I wrote based on @jasonmcguirk and @thefallengamesstudio ideas. Just add this class to any of your Editor folders and done!

    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. }
     
    ariansyah and xucian like this.
  21. xucian

    xucian

    Joined:
    Mar 7, 2016
    Posts:
    836
    Hey. I like this. If someone from Unity can confirm this is superior to the coarser approach I posted below, that'd be awesome
     
    sandolkakos likes this.