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

Async/Await Not Joining Main Thread After Await?

Discussion in '2017.1 Beta' started by Metastable, Jul 2, 2017.

  1. Metastable

    Metastable

    Joined:
    Apr 10, 2013
    Posts:
    50
    I'm looking to use async/await to clean up some callbacks. I'm currently having an issue with doing Main thread work, such as accessing `gameObject` after an await but I'm getting errors that it's not being called on the main thread. I was under the presumption that Unity automatically synchronizes to the main thread context now. Here's a snippet of code that I'm working with.

    Code (csharp):
    1.  
    2. protected Task Execute(Task<ServerReply> target) {
    3.     PopupManager.Instance.DisplayLoadingScreen(popupMessage, popupBlackout);
    4.     return Task.Run(async () => {
    5.         var reply = await target;
    6.         PopupManager.Instance.HideLoadingScreen(); //Problem here as this calls `gameObject.SetActive(false)`
    7.         HandleReply(reply);
    8.     });
    9. }
    10.  
    Is there anything else I should be doing to get this working correctly?

    Here's the exception
     
  2. Dizzy-Dalvin

    Dizzy-Dalvin

    Joined:
    Jul 4, 2013
    Posts:
    54
    I think you're using Task.Run the wrong way. In your code when you use await it's already being executed on a thread pool so Unity SynchronizationContext isn't there to capture. You should use await outside of Task.Run if you want it to return to the UI thread after finishing.

    Can you give an example of the target task you're passing in?
     
    Last edited: Jul 2, 2017
  3. Metastable

    Metastable

    Joined:
    Apr 10, 2013
    Posts:
    50
    Yeah here's a snippet.

    Code (csharp):
    1.  
    2. public override Task Act() {
    3.     return Execute(ConnectionHandler.Instance.GetRanks()); //Get Ranks returns a Task<ServerReply>
    4. }
    5.  
    The reason for the Task.Run is I wanted to clean up my popup after target has completed and handle the response.

    Here's what's going on internally for the GetRanks call just for context, in case it helps.

    Code (csharp):
    1.  
    2. Task<ServerReply> PushToEmpireChannel(string eventName, JObject data) {
    3.     var taskCompletionSource = new TaskCompletionSource<ServerReply>();
    4.  
    5.     empireChannel
    6.     .PushJson(eventName, data)
    7.     .Receive(Phoenix.Reply.Status.Ok, (reply) =>
    8.         taskCompletionSource.SetResult(new ServerReply() { status = ServerReply.Status.Ok, response = reply.response })
    9.     )
    10.     .Receive(Phoenix.Reply.Status.Error, (reply) =>
    11.         taskCompletionSource.SetResult(new ServerReply() { status = ServerReply.Status.Error, response = reply.response })
    12.     )
    13.     .Receive(Phoenix.Reply.Status.Timeout, (reply) =>
    14.         taskCompletionSource.SetResult(new ServerReply() { status = ServerReply.Status.Timeout, response = reply.response })
    15.     );
    16.  
    17.     return taskCompletionSource.Task;
    18. }
    19.  
     
  4. Metastable

    Metastable

    Joined:
    Apr 10, 2013
    Posts:
    50
    @Dizzy-Dalvin Your comment gave me a lead into what to search and I came up with a solution that works.

    Code (csharp):
    1.  
    2. protected Task Execute(Task<ServerReply> target) {
    3.     TaskScheduler taskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
    4.  
    5.     PopupManager.Instance.DisplayLoadingScreen(popupMessage, popupBlackout);
    6.     return Task.Factory.StartNew(async () => {
    7.         var reply = await target;
    8.         PopupManager.Instance.HideLoadingScreen();
    9.         HandleReply(reply);
    10.     }, CancellationToken.None, TaskCreationOptions.None, taskScheduler);
    11. }
    12.  
    So it looks like you were correct about the Unity context not capturing. The second await would be on the ThreadPool context.
     
  5. Biro456

    Biro456

    Joined:
    Dec 22, 2012
    Posts:
    8
    Based on pure curiosity, why didn't you write the method like this:

    Code (CSharp):
    1. protected async Task Execute(Task<ServerReply> target) {
    2.     PopupManager.Instance.DisplayLoadingScreen(popupMessage, popupBlackout);
    3.  
    4.     var reply = await target;
    5.  
    6.     PopupManager.Instance.HideLoadingScreen();
    7.     HandleReply(reply);
    8. }
    It would capture the Unity SynchronizationContext implicitly as long as you called the method from the main thread.
     
    Last edited: Jul 2, 2017
    alexzzzz likes this.
  6. Dizzy-Dalvin

    Dizzy-Dalvin

    Joined:
    Jul 4, 2013
    Posts:
    54
    Okay, I haven't tested it but I think the following code should do what you want:
    Code (CSharp):
    1. protected async Task Execute(Task<ServerReply> target)
    2.     {
    3.         PopupManager.Instance.DisplayLoadingScreen(popupMessage, popupBlackout);
    4.         ServerReply reply = await Task.Run(async () => await target);
    5.         PopupManager.Instance.HideLoadingScreen();
    6.         HandleReply(reply);
    7.     }
    This makes sense only if you know that your target task executes on main thread entirely. And the method should probably be called something like ExecuteOnBackgroundThread.
    If your empireChannel offloads its work to background threads and only returns to main thread to invoke the Receive callbacks, then there's no need for Task.Run at all. You can just write:

    Code (CSharp):
    1. protected async Task Execute(Task<ServerReply> target)
    2.     {
    3.         PopupManager.Instance.DisplayLoadingScreen(popupMessage, popupBlackout);
    4.         ServerReply reply = await target;
    5.         PopupManager.Instance.HideLoadingScreen();
    6.         HandleReply(reply);
    7.     }
     
  7. Metastable

    Metastable

    Joined:
    Apr 10, 2013
    Posts:
    50
    @Biro456 @Dizzy-Dalvin You were both correct on simply using

    Code (csharp):
    1.  
    2. protected async Task Execute(Task<ServerReply> target) {
    3.     PopupManager.Instance.DisplayLoadingScreen(popupMessage, popupBlackout);
    4.     var reply = await target;
    5.     PopupManager.Instance.HideLoadingScreen();
    6.     HandleReply(reply);
    7. }
    8.  
    I'm still learning how to properly use async/await and incorrectly thought you needed to return a task to 'chain' correctly. As @Dizzy-Dalvin correctly presumed, empireChannel offloads onto another background thread that it manages itself so I can implicitly capture the context.

    Thanks to both of you!
     
  8. alexzzzz

    alexzzzz

    Joined:
    Nov 20, 2010
    Posts:
    1,447
    From my experience, when you introduce async/await and your code looks more complex than before, you are doing something wrong.