Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Resolved Async/Await Future Design

Discussion in 'Scripting Dev Blitz Day 2023 - Q&A' started by stonstad, Feb 22, 2023.

  1. stonstad

    stonstad

    Joined:
    Jan 19, 2018
    Posts:
    599
    I understand work is ongoing to add support for Async/Await. Will async/await in the .NET core version of Unity work like normal C# async/await programming? ValueTask provides a low-allocation alternative to Task, and it would be awesome to have a design that mirrors what is possible in .NET today!

    Question for the Unity team -- if the planned design diverges from how it is done in C#/.NET today, what design considerations forced your hand down this path? Thanks!
     
    M_MG_S, mandisaw and tvardero like this.
  2. simon-ferquel-unity

    simon-ferquel-unity

    Unity Technologies

    Joined:
    Apr 1, 2021
    Posts:
    65
    First, for always async constructs, ValueTask is actually less efficient than Task. ValueTask is usefull only if there is a chance that the operation is completed when it is awaited (if it is not, under the hood a Task is allocated).
    Second, Task is a very generic way with lots of default behaviours that don't fit well with Unity needs:
    - It captures the execution context (which is far from cheap, especially with the current Mono runtime Unity uses)
    - It defaults to run continuations by posting to the synchronization context (in Unity, that adds 1 frame of latency for completion)
    - It does managed allocations

    Unity's Awaitable type can be used with async await (both as an awaitable type and as an async return type) and uses a lot of object pooling under the hood to mitigate allocations. That is, if a given "async Awaitable" method is called multiple times, it will only allocate once. It also does not capture the execution context, and continuation are run synchronously on completion (that is your code will resume at the exact frame where the completion is triggered)
     
    M_MG_S, mandisaw, CodeSmile and 3 others like this.
  3. simon-ferquel-unity

    simon-ferquel-unity

    Unity Technologies

    Joined:
    Apr 1, 2021
    Posts:
    65
    However, if you have to work with 3rd party libraries that expose tasks, you can write things like:

    Code (CSharp):
    1. async Awaitable MyCode(){
    2.   await Awaitable.NextFrameAsync();
    3.   await SomeLib.ThatReturnsATaskAsync();
    4. }
    mixing both awaitables and tasks usage in the same method.
     
    mandisaw, CodeSmile, tvardero and 3 others like this.
  4. stonstad

    stonstad

    Joined:
    Jan 19, 2018
    Posts:
    599
    I appreciate the detail around why the .NET approach isn't ideal for Unity. Thank you for the awesome design -- I'm looking forward to using it!
     
  5. ProtoTerminator

    ProtoTerminator

    Joined:
    Nov 19, 2013
    Posts:
    566
    That is an oversimplification, and not completely true. ValueTask can also be used to back it with a reusable IValueTaskSource in a safe way.

    How safe is your reusable "Awaitable"? Does it protect from accidentally awaiting an instance that already completed and was awaited a while ago? ValueTask protects against that.

    If an "async Awaitable" method is called again before the original completed, how are you not allocating? It would be incorrect to return the same Awaitable instance that is still running.

    Also, are all of the static Awaitable functions non-allocating, like
    NextFrameAsync
    and
    WaitForSecondsAsync
    ?
     
    Elringus likes this.
  6. john_primitive

    john_primitive

    Joined:
    Jan 4, 2019
    Posts:
    7
    @simon-ferquel-unity what is the equivalent of

    Code (CSharp):
    1. await Task.WhenAll(...)
    using Awaitable?

    I tried rewriting my code to look like this:

    Code (CSharp):
    1.  
    2. List<Awaitable> tasks = (my Awaitables...).ToList();
    3.  
    4. while (tasks.Any(x => !x.IsCompleted))
    5.             {
    6.                 await Awaitable.NextFrameAsync();
    7.             }
    but it never returns. Any help from anyone would be appreciated
     
  7. simon-ferquel-unity

    simon-ferquel-unity

    Unity Technologies

    Joined:
    Apr 1, 2021
    Posts:
    65
    One of the biggest difference between Task and Awaitable is that the Awaitable objects are pooled and reused. So they need to be awaited only once (once completion has been raised they are put back in the Awaitable pool to enable signaling another asynchronous operation completion).

    Thus I would implement WhenAll as:

    foreach (var a in awaitables) await a;

    (Plus maybe some exception handling if necessary). This will await all objects once and only once.
     
    john_primitive likes this.
  8. imKoi

    imKoi

    Joined:
    Nov 23, 2016
    Posts:
    3
    Hello! I suggest this solution
    Code (CSharp):
    1. public static Awaitable<T[]> WhenAll<T>(params Awaitable<T>[] awaitables)
    2.         {
    3.             var completionSource = new AwaitableCompletionSource<T[]>();
    4.             var count = 2;
    5.  
    6.             var result = new T[awaitables.Length];
    7.  
    8.             for (var i = 0; i < awaitables.Length; i++)
    9.             {
    10.                 StartAwaitableAndInvokeCallback(awaitables[i], i, OnAwaitableCompleted);
    11.             }
    12.  
    13.             void OnAwaitableCompleted(int awaitableIndex, T awaitableResult)
    14.             {
    15.                 count--;
    16.  
    17.                 result[awaitableIndex] = awaitableResult;
    18.  
    19.                 if (count <= 0)
    20.                 {
    21.                     completionSource.TrySetResult(result);
    22.                 }
    23.             }
    24.  
    25.             return completionSource.Awaitable;
    26.         }
    27.        
    28.         public static Awaitable<T> WhenAny<T>(params Awaitable<T>[] awaitables)
    29.         {
    30.             var completionSource = new AwaitableCompletionSource<T>();
    31.  
    32.             for (var i = 0; i < awaitables.Length; i++)
    33.             {
    34.                 StartAwaitableAndInvokeCallback(awaitables[i], i, OnAwaitableCompleted);
    35.             }
    36.  
    37.             void OnAwaitableCompleted(int awaitableIndex, T awaitableResult)
    38.             {
    39.                 completionSource.TrySetResult(awaitableResult);
    40.             }
    41.  
    42.             return completionSource.Awaitable;
    43.         }
    44.  
    45.         private static async void StartAwaitableAndInvokeCallback<T>(Awaitable<T> awaitable, int awaitableIndex, Action<int, T> callback)
    46.         {
    47.             var result = await awaitable;
    48.  
    49.             callback.Invoke(awaitableIndex, result);
    50.         }
    Of course you guys can take result awaitable directly from pull and make codegen to avoid allocation for params. After that we will have allocation for result array and for the callbacks. But it still better then nothing / custom solutions on each project
     
    Last edited: Jul 28, 2023
    ArtemSvirid likes this.