Search Unity

Async/Await inside a Coroutine

Discussion in 'Editor & General Support' started by waldgeist, Aug 15, 2020.

  1. waldgeist

    waldgeist

    Joined:
    May 6, 2017
    Posts:
    388
    We are using an external library that lets us await a longer task via the async/await pattern. We need to await this job while an animation is running at the same time. If we just await the task naively, the animation gets stuck until the task has been finished.

    So we packed everything in a Coroutine like this:

    Code (CSharp):
    1. IEnumerator WaitForOurTask() {
    2.   yield return FunctionReturningTheAsyncTask();
    3.   ProcessingTheResultsOfTheTask();
    4. }
    We did not get an error for this and expected the Coroutine to await the Task. But this is not the case. Instead, it seems to just wait for the next frame (like with yield return null; ) and then continues with ProcessingTheResultsOfTheTask(), which in our case causes a race condition, because the results of the task are not available yet.

    We found this third party library:
    https://github.com/zsaladin/Asyncoroutine
    which allows to do this:

    Code (CSharp):
    1. IEnumerator WaitForOurTask() {
    2.   yield return FunctionReturningTheAsyncTask().AsCoroutine();
    3.   ProcessingTheResultsOfTheTask();
    4. }
    This works fine. But the library is from 2017 and does not appear to be actively maintained. The latest issue is over one year old, without any response. And this issue also includes a hint that the library is not really working reliably. Introducing such a library always bears the risk of causing problems with future Unity releases.

    So I am wondering if there's a better (built-in) way to actually await a Task inside a Coroutine as of 2020?
     
  2. waldgeist

    waldgeist

    Joined:
    May 6, 2017
    Posts:
    388
    As a side-note: Wrapping the async function in yet another async void function did not work either, because async void functions don't cause the caller to wait, and hence we're running into the race condition again.
     
  3. waldgeist

    waldgeist

    Joined:
    May 6, 2017
    Posts:
    388
    This seems to work, too:

    Code (CSharp):
    1. IEnumerator WaitForOurTask() {
    2.   Task task = FunctionReturningTheAsyncTask();
    3.   yield return new WaitUntil(() => task.IsCompleted);
    4.   ProcessingTheResultsOfTheTask();
    5. }
    Unsure if it's the preferred way, though.
     
  4. Neto_Kokku

    Neto_Kokku

    Joined:
    Feb 15, 2018
    Posts:
    1,751
    Yes, that's how you do when waiting for anything that isn't directly compatible with Unity's coroutine system.
     
  5. CasualT_Bossfight

    CasualT_Bossfight

    Joined:
    Oct 26, 2016
    Posts:
    8
    Thanks waldgeist! I just had this same problem of taking an existing Coroutine and meshing it with an asynchronous Task and your code worked perfectly!
     
  6. MikkoK-HS

    MikkoK-HS

    Joined:
    Sep 26, 2017
    Posts:
    53
    How did you get this to work? For me doing
    Code (CSharp):
    1. yield return new WaitUntil(()=> task.isCompleted)
    simply hangs the entire app and doesn't continue to the next frame until the task is done?
     
    dr4 likes this.
  7. OnYtoln147

    OnYtoln147

    Joined:
    Sep 3, 2021
    Posts:
    3
  8. nir_unity198

    nir_unity198

    Joined:
    Jun 13, 2022
    Posts:
    5
    It's great! thanks alow!
    However, it won't work if the async function is inside a 'try' clause.
     
  9. ViktorMSc

    ViktorMSc

    Joined:
    May 28, 2022
    Posts:
    14
    Here is another solution

    Code (CSharp):
    1.     IEnumerator RefreshLobbyCoroutine()
    2.     {
    3.         var delay = new WaitForSecondsRealtime(2);
    4.         while (lobby != null)
    5.         {
    6.            // Run task and wait for it to complete
    7.             var t = Task.Run(async () => await Lobbies.Instance.GetLobbyAsync(lobby.Id));
    8.             yield return new WaitUntil(() => t.IsCompleted);
    9.  
    10.            // Task is complete, get the result
    11.             lobby = t.Result;
    12.             yield return delay;
    13.         }
    14.     }
     
  10. gwelkind

    gwelkind

    Joined:
    Sep 16, 2015
    Posts:
    66
    Hey, it's me, from the future.

    I just wanted to say that, after searching for this a while, I found the a 3rd party solution that Unity veterans probably all know about already.

    https://github.com/Cysharp/UniTask

    This is probably what most people are looking for when it comes to a general solution for integrating async/await with coroutines in most cases.

    I haven't used this in a professional project yet, so take with a grain of salt, but it seems like it's at least somewhat maintained, and claims to work across release platforms (though, if I were you, I'd confirm on all your targets before investing in this library, don't just take this rando's uninformed opinion on it).
     
    Last edited: Nov 12, 2022
  11. HajiyevEl

    HajiyevEl

    Joined:
    Feb 19, 2020
    Posts:
    47
    Hi, i have the same issue. Have you found the solution?
     
  12. euden_one

    euden_one

    Joined:
    Apr 4, 2022
    Posts:
    33
    That's because the task is launched on the main thread, use Task.Run() to send the task to a thread pool.
     
    Gernata likes this.
  13. UnityBrains

    UnityBrains

    Joined:
    Mar 10, 2013
    Posts:
    5
    Sorry for the necro, but can someone please explain the difference between

    Task t = FunctionReturningTheAsyncTask();

    and
    Task t = Task.Run(async () => await FunctionReturningTheAsyncTask());


    both followed by
    yield return new WaitUntil(() => t.IsCompleted);


    Edit; Unity 2021.3.2f1

    both of which have been suggested in this thread. Why can I call Unity API functions in the task created in the former fashion, but I get exceptions regarding the main/update thread when using the latter? I don't understand the nuts and bolts of the difference between those invocations, and hoping someone can clarify.
     
  14. euden_one

    euden_one

    Joined:
    Apr 4, 2022
    Posts:
    33
    Task.Run creates a new thread (kinda but no need to know all the details) to run that block of code. Unity API can only be called from the main thread to avoid freezing or crashing the app. You can check what thread number you are on with Thread.CurrentThread, main thread should be "1" if I remember correctly.

    Meanwhile, the first method is just wrapping the Task capabilities and running on the main thread.

    Both methods check for the task completion on the main thread so there's no problem there.
     
    UnityBrains likes this.