Search Unity

Async/Await in Editor script?

Discussion in 'Experimental Scripting Previews' started by Foriero, Jul 3, 2017.

  1. Foriero

    Foriero

    Joined:
    Jan 24, 2012
    Posts:
    472
    Hello, how does unity synchronization to the main editor thread works? Can we have a simple example?
     
  2. alexzzzz

    alexzzzz

    Joined:
    Nov 20, 2010
    Posts:
    1,410
    It seems that UnitySynchronizationContext is set as the current context for the main thread in edit-time, but doesn't really do much. The continuations are accumulated but never get executed, except the moment when the editor starts script recompilation process.

    In this example the code after await (line 39) doesn't run until I press "execute pending continuations" button. In play-time it works as expected though since the pending continuations are executed every frame.

    No idea whether this behavior is intentional or not.
    Code (CSharp):
    1. using System.Reflection;
    2. using System.Threading;
    3. using System.Threading.Tasks;
    4. using UnityEditor;
    5. using UnityEngine;
    6.  
    7. public class Foo : MonoBehaviour
    8. {
    9. }
    10.  
    11. [CustomEditor(typeof(Foo))]
    12. public class FooEditor : Editor
    13. {
    14.     public override async void OnInspectorGUI()
    15.     {
    16.         base.OnInspectorGUI();
    17.  
    18.         if (GUILayout.Button("Execute pending continuations"))
    19.         {
    20.             var context = SynchronizationContext.Current;
    21.             var execMethod = context.GetType().GetMethod("Exec", BindingFlags.NonPublic | BindingFlags.Instance);
    22.             execMethod.Invoke(context, null);
    23.         }
    24.  
    25.         if (GUILayout.Button("Post"))
    26.         {
    27.             SynchronizationContext.Current.Post(_ => Debug.Log("Submitted via Post"), null);
    28.         }
    29.  
    30.         if (GUILayout.Button("Send"))
    31.         {
    32.             SynchronizationContext.Current.Send(_ => Debug.Log("Submitted via Send"), null);
    33.         }
    34.  
    35.         if (GUILayout.Button("Do time consuming stuff"))
    36.         {
    37.             Debug.Log("before: " + Thread.CurrentThread.ManagedThreadId);
    38.             await Task.Run(() => DoTimeConsumingStuff());
    39.             Debug.Log("after: " + Thread.CurrentThread.ManagedThreadId);
    40.         }
    41.     }
    42.  
    43.     private void DoTimeConsumingStuff()
    44.     {
    45.         Debug.Log("doing...");
    46.         Thread.Sleep(1000);
    47.         Debug.Log("done: " + Thread.CurrentThread.ManagedThreadId);
    48.     }
    49. }
     
    Last edited: Jul 3, 2017
  3. alexzzzz

    alexzzzz

    Joined:
    Nov 20, 2010
    Posts:
    1,410
    This example works as expected in edit-time. The continuations don't accumulate and get executed when there are any. (I would consider it a hack though.)
    Code (CSharp):
    1. using System.Reflection;
    2. using System.Threading;
    3. using System.Threading.Tasks;
    4. using UnityEditor;
    5. using UnityEngine;
    6.  
    7. public class Foo : MonoBehaviour
    8. {
    9. }
    10.  
    11. [CustomEditor(typeof(Foo))]
    12. public class FooEditor : Editor
    13. {
    14.     [InitializeOnLoadMethod]
    15.     private static void Initialize() => EditorApplication.update += ExecuteContinuations;
    16.  
    17.     public override async void OnInspectorGUI()
    18.     {
    19.         base.OnInspectorGUI();
    20.  
    21.         if (GUILayout.Button("Do time consuming stuff"))
    22.         {
    23.             Debug.Log("before: " + Thread.CurrentThread.ManagedThreadId);
    24.             await Task.Run(() => DoTimeConsumingStuff());
    25.             Debug.Log("after: " + Thread.CurrentThread.ManagedThreadId);
    26.             return;
    27.         }
    28.  
    29.         if (GUILayout.Button("Post"))
    30.         {
    31.             SynchronizationContext.Current.Post(_ => Debug.Log("Submitted via Post"), null);
    32.         }
    33.  
    34.         if (GUILayout.Button("Send"))
    35.         {
    36.             SynchronizationContext.Current.Send(_ => Debug.Log("Submitted via Send"), null);
    37.         }
    38.     }
    39.  
    40.     private static void ExecuteContinuations()
    41.     {
    42.         var context = SynchronizationContext.Current;
    43.         var execMethod = context.GetType().GetMethod("Exec", BindingFlags.NonPublic | BindingFlags.Instance);
    44.         execMethod.Invoke(context, null);
    45.     }
    46.  
    47.     private void DoTimeConsumingStuff()
    48.     {
    49.         Debug.Log("doing...");
    50.         Thread.Sleep(1000);
    51.         Debug.Log("done: " + Thread.CurrentThread.ManagedThreadId);
    52.     }
    53. }
    This example also demonstrates that async/await doesn't work nicely with the immediate mode gui. Notice that return statement in line 26. If you comment it, the first time you call something gui-related you'll get "ArgumentException: You can only call GUI functions from inside OnGUI" because the continuation doesn't run "from inside OnGUI" but rather from inside EditorApplication.update in this case.
     
    Last edited: Jul 3, 2017
    rakkarage likes this.
  4. joncham

    joncham

    Unity Technologies

    Joined:
    Dec 1, 2011
    Posts:
    251
    This is not intentional. We'll need to look into supporting async/await properly when not in play mode.
     
  5. johnseghersmsft

    johnseghersmsft

    Joined:
    May 18, 2015
    Posts:
    28
    Thanks to Alexzzzz's sample, I've created a standalone class you can add to a Unity project that will enable the update pump for edit mode:
    Code (CSharp):
    1.  
    2. using System.Reflection;
    3. using System.Threading;
    4. using UnityEditor;
    5. namespace UI
    6. {
    7.     public class EditorAsyncPump
    8.     {
    9.         [InitializeOnLoadMethod]
    10.         private static void Initialize()
    11.         {
    12.             EditorApplication.update += ExecuteContinuations;
    13.         }
    14.         private static void ExecuteContinuations()
    15.         {
    16.             if (EditorApplication.isPlayingOrWillChangePlaymode)
    17.             {
    18.                 // Not in Edit mode, don't interfere
    19.                 return;
    20.             }
    21.             var context = SynchronizationContext.Current;
    22.             if (_execMethod == null)
    23.             {
    24.                 _execMethod = context.GetType().GetMethod("Exec", BindingFlags.NonPublic | BindingFlags.Instance);
    25.             }
    26.             _execMethod.Invoke(context, null);
    27.         }
    28.         private static MethodInfo _execMethod;
    29.     }
    30. }
    31.  
     
    MCPGNZ likes this.
  6. RDeluxe

    RDeluxe

    Joined:
    Sep 29, 2013
    Posts:
    94
    Any news on this ? The pump is working, but I suspect it to be responsible for the Unity editor crashs that's I'm getting.
     
  7. Invertex

    Invertex

    Joined:
    Nov 7, 2013
    Posts:
    967
    Yeah same issue as well.

    @joncham or @JoshPeterson any word on this?

    Also don't know if I should make another thread, but just wondering if using AssetPostprocessor's messages like OnPostprocessModel will properly properly handle being set as Task instead of void? It seems to compile fine if I make it an async Task, but the AssetPostprocessor doesn't seem to actually await the OnPostprocessModel, and ends up processing multiple assets at once. We do some heavy processing to various imported assets that would help to have async in the postprocessor, but obviously don't want to create issues by not having the pipeline run linearly.
     
  8. JoshPeterson

    JoshPeterson

    Unity Technologies

    Joined:
    Jul 21, 2014
    Posts:
    4,219
    We've not corrected this yet. To help us out, could someone submit a bug report for async/await in the editor? That will give us all the details we need in one place, and it should prevent us from forgetting about this. :)

    This is probably some time away yet. In most of the Unity C# code/API we can't start using new C# features until we no longer support the old scripting runtime. We're hoping to be able to deprecate the old scripting runtime after a few major Unity releases, but that will still be on the order of months in the best case. Only then can we start to work on new C# features in the Unity API.
     
  9. RDeluxe

    RDeluxe

    Joined:
    Sep 29, 2013
    Posts:
    94
    Hello Josh,

    It's pretty simple ; without the Pump a Task won't work except if the user moves the mouse / force an editor refresh, then the task will execute.

    I could create a test repo if you like, but there is not much to show : it's simply not working (without the aforementioned pump)
     
  10. JoshPeterson

    JoshPeterson

    Unity Technologies

    Joined:
    Jul 21, 2014
    Posts:
    4,219
    Please do create a project to reproduce this and submit a bug report. Thanks!
     
  11. RDeluxe

    RDeluxe

    Joined:
    Sep 29, 2013
    Posts:
    94
    Alright, I just thought you knew as @joncham clearly stated that async/await was not working when not in Play Mode.
     
  12. StephenHodgson-Valorem

    StephenHodgson-Valorem

    Joined:
    Mar 8, 2017
    Posts:
    148
    Hey guys, any update on this?
     
  13. VOTRUBEC

    VOTRUBEC

    Joined:
    Dec 17, 2014
    Posts:
    49
    That moment when you think to yourself "Everything's correct, but, it's almost as if 'await' isn't working in an Editor Windows. Let me check Google..."
     
    Dissolute likes this.
  14. mkderoy

    mkderoy

    Unity Technologies

    Joined:
    Oct 25, 2016
    Posts:
    22
    Changes to support async/await in edit mode are in the process of being landed in trunk at the moment
     
  15. Dissolute

    Dissolute

    Joined:
    Oct 6, 2012
    Posts:
    2
    Was this released? I'm still encountering this issue when running EditMode tests in Unity 2018.2.0f2.

    Never mind, I was deadlocking myself. This simple test works fine:

    Code (CSharp):
    1.         [Test]
    2.         public async void When_Simple_Async_Test()
    3.         {
    4.             UnityEngine.Debug.Log("SC before:");
    5.             UnityEngine.Debug.Log(SynchronizationContext.Current);
    6.             await Task.Delay(10);
    7.             UnityEngine.Debug.Log("SC after:");
    8.             UnityEngine.Debug.Log(SynchronizationContext.Current);
    9.         }
     
    Last edited: Aug 12, 2018
  16. IC_

    IC_

    Joined:
    Jan 27, 2016
    Posts:
    43
    Useless test. Increase delay and add an assertion depending on task result. It will success in test runner and then throw an assertion error in console after delay
     
  17. awesomesaucelabs

    awesomesaucelabs

    Joined:
    Aug 6, 2019
    Posts:
    3
    For latecomers to this thread:

    It looks like the problems with using async/await in the Editor are fixed now. I ran @alexzzzz's test code (from post #2) in Unity 2018.3.0f2 and async/await worked correctly (without using the "pump").

    FYI, the issue discussed in this thread was submitted to the Unity issue tracker here. It was marked as a duplicate of another issue (the connection between the two issues is not obvious to me), and that issue was marked as "fixed" in Unity 2018.2.
     
  18. Bshsf_9527

    Bshsf_9527

    Joined:
    Sep 6, 2017
    Posts:
    12
    Is there any update for exception :"ArgumentException: You can only call GUI functions from inside OnGUI".
    as the continuation not occur on the main thread's OnGUI/OnInspectorGUI.
     
    Last edited: Oct 27, 2019
  19. JoshPeterson

    JoshPeterson

    Unity Technologies

    Joined:
    Jul 21, 2014
    Posts:
    4,219
    I believe that this should be working now. If it is not, can you drop us a new bug report for it?
     
  20. Bshsf_9527

    Bshsf_9527

    Joined:
    Sep 6, 2017
    Posts:
    12
    Code (CSharp):
    1. using System.Threading;
    2. using System.Threading.Tasks;
    3. using UnityEditor;
    4. using UnityEngine;
    5. public class Foo : MonoBehaviour { }
    6. [CustomEditor(typeof(Foo))]
    7. public class FooEditor : Editor
    8. {
    9.     public override async  void OnInspectorGUI()
    10.     {
    11.         if (GUILayout.Button("Do time consuming stuff"))
    12.         {
    13.             Debug.Log("before: " + Thread.CurrentThread.ManagedThreadId);
    14.             await Task.Run(() => DoTimeConsumingStuff()); // when await task finished here, an error will rise when draw a GUI below 。
    15.             //var _ = Task.Run(() => DoTimeConsumingStuff()); //but this one will not throw any error.
    16.             Debug.Log("after: " + Thread.CurrentThread.ManagedThreadId);
    17.         }
    18.  
    19.         // when draw this button , error occured ,if you use await before .
    20.         if (GUILayout.Button("Send")){}
    21.     }
    22.  
    23.     private void DoTimeConsumingStuff()
    24.     {
    25.         Debug.Log("In task: " + Thread.CurrentThread.ManagedThreadId);
    26.     }
    27. }
    Reproduce with unity 2018.2.16 and unity 2019.1.14
     
    Last edited: Oct 31, 2019
  21. JoshPeterson

    JoshPeterson

    Unity Technologies

    Joined:
    Jul 21, 2014
    Posts:
    4,219
    Thanks, can you submit this in a bug report? That way we can feedback to you when it is fixed.
     
  22. Bshsf_9527

    Bshsf_9527

    Joined:
    Sep 6, 2017
    Posts:
    12
    what is the "bug report" ,and what should i do then?
     
  23. JoshPeterson

    JoshPeterson

    Unity Technologies

    Joined:
    Jul 21, 2014
    Posts:
    4,219
    Bshsf_9527 likes this.
  24. Bshsf_9527

    Bshsf_9527

    Joined:
    Sep 6, 2017
    Posts:
    12
    This page has no 404 error but blank,I can do nothing,by the way I visited this website in China..
     
  25. Bshsf_9527

    Bshsf_9527

    Joined:
    Sep 6, 2017
    Posts:
    12
    JoshPeterson likes this.
  26. pld

    pld

    Joined:
    Dec 12, 2014
    Posts:
    7
    @Bshsf_9527, I don't think it's a bug. I can think of no way for Unity to implement immediate-mode GUI that works properly with async code. What you're doing is essentially this:

    Code (CSharp):
    1. void OnInspectorGUI() {
    2.   GUI.BeginHorizontal();
    3.   SynchronizationContext.Current.Post( () => GUI.EndHorizontal() );
    4. }
    OnInspectorGUI's going to be called every frame; and the caller will first of all complain that you've started but not ended a horizontal block. And some time later, EndHorizontal() will complain that it's being called at some unexpected time.
     
unityunity