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);
}
}
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.
This is not intentional. Weâll need to look into supporting async/await properly when not in play mode.
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;
}
}
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.
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.
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âŚâ
Changes to support async/await in edit mode are in the process of being landed in trunk at the moment
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