Async/Await in Editor script?

Hello, how does unity synchronization to the main editor thread works? Can we have a simple example?

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.

using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;

public class Foo : MonoBehaviour
{
}

[CustomEditor(typeof(Foo))]
public class FooEditor : Editor
{
    public override async void OnInspectorGUI()
    {
        base.OnInspectorGUI();

        if (GUILayout.Button("Execute pending continuations"))
        {
            var context = SynchronizationContext.Current;
            var execMethod = context.GetType().GetMethod("Exec", BindingFlags.NonPublic | BindingFlags.Instance);
            execMethod.Invoke(context, null);
        }

        if (GUILayout.Button("Post"))
        {
            SynchronizationContext.Current.Post(_ => Debug.Log("Submitted via Post"), null);
        }

        if (GUILayout.Button("Send"))
        {
            SynchronizationContext.Current.Send(_ => Debug.Log("Submitted via Send"), null);
        }

        if (GUILayout.Button("Do time consuming stuff"))
        {
            Debug.Log("before: " + Thread.CurrentThread.ManagedThreadId);
            await Task.Run(() => DoTimeConsumingStuff());
            Debug.Log("after: " + Thread.CurrentThread.ManagedThreadId);
        }
    }

    private void DoTimeConsumingStuff()
    {
        Debug.Log("doing...");
        Thread.Sleep(1000);
        Debug.Log("done: " + Thread.CurrentThread.ManagedThreadId);
    }
}
4 Likes

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.)

using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;

public class Foo : MonoBehaviour
{
}

[CustomEditor(typeof(Foo))]
public class FooEditor : Editor
{
    [InitializeOnLoadMethod]
    private static void Initialize() => EditorApplication.update += ExecuteContinuations;

    public override async void OnInspectorGUI()
    {
        base.OnInspectorGUI();

        if (GUILayout.Button("Do time consuming stuff"))
        {
            Debug.Log("before: " + Thread.CurrentThread.ManagedThreadId);
            await Task.Run(() => DoTimeConsumingStuff());
            Debug.Log("after: " + Thread.CurrentThread.ManagedThreadId);
            return;
        }

        if (GUILayout.Button("Post"))
        {
            SynchronizationContext.Current.Post(_ => Debug.Log("Submitted via Post"), null);
        }

        if (GUILayout.Button("Send"))
        {
            SynchronizationContext.Current.Send(_ => Debug.Log("Submitted via Send"), null);
        }
    }

    private static void ExecuteContinuations()
    {
        var context = SynchronizationContext.Current;
        var execMethod = context.GetType().GetMethod("Exec", BindingFlags.NonPublic | BindingFlags.Instance);
        execMethod.Invoke(context, null);
    }

    private void DoTimeConsumingStuff()
    {
        Debug.Log("doing...");
        Thread.Sleep(1000);
        Debug.Log("done: " + Thread.CurrentThread.ManagedThreadId);
    }
}

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.

1 Like

This is not intentional. We’ll need to look into supporting async/await properly when not in play mode.

9 Likes

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:

using System.Reflection;
using System.Threading;
using UnityEditor;
namespace UI
{
    public class EditorAsyncPump
    {
        [InitializeOnLoadMethod]
        private static void Initialize()
        {
            EditorApplication.update += ExecuteContinuations;
        }
        private static void ExecuteContinuations()
        {
            if (EditorApplication.isPlayingOrWillChangePlaymode)
            {
                // Not in Edit mode, don't interfere
                return;
            }
            var context = SynchronizationContext.Current;
            if (_execMethod == null)
            {
                _execMethod = context.GetType().GetMethod("Exec", BindingFlags.NonPublic | BindingFlags.Instance);
            }
            _execMethod.Invoke(context, null);
        }
        private static MethodInfo _execMethod;
    }
}
1 Like

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.

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.

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

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.

2 Likes

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)

Please do create a project to reproduce this and submit a bug report. Thanks!

Alright, I just thought you knew as @joncham clearly stated that async/await was not working when not in Play Mode.

Hey guys, any update on this?

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…”

3 Likes

Changes to support async/await in edit mode are in the process of being landed in trunk at the moment

11 Likes

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:

        [Test]
        public async void When_Simple_Async_Test()
        {
            UnityEngine.Debug.Log("SC before:");
            UnityEngine.Debug.Log(SynchronizationContext.Current);
            await Task.Delay(10);
            UnityEngine.Debug.Log("SC after:");
            UnityEngine.Debug.Log(SynchronizationContext.Current);
        }

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

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.

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.

I believe that this should be working now. If it is not, can you drop us a new bug report for it?

using System.Threading;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;
public class Foo : MonoBehaviour { }
[CustomEditor(typeof(Foo))]
public class FooEditor : Editor
{
    public override async  void OnInspectorGUI()
    {
        if (GUILayout.Button("Do time consuming stuff"))
        {
            Debug.Log("before: " + Thread.CurrentThread.ManagedThreadId);
            await Task.Run(() => DoTimeConsumingStuff()); // when await task finished here, an error will rise when draw a GUI below 。
            //var _ = Task.Run(() => DoTimeConsumingStuff()); //but this one will not throw any error.
            Debug.Log("after: " + Thread.CurrentThread.ManagedThreadId);
        }

        // when draw this button , error occured ,if you use await before .
        if (GUILayout.Button("Send")){}
    }

    private void DoTimeConsumingStuff()
    {
        Debug.Log("In task: " + Thread.CurrentThread.ManagedThreadId);
    }
}

Reproduce with unity 2018.2.16 and unity 2019.1.14