Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    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:
    658
    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:
    67
    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)
     
    Maeslezo, M_MG_S, mandisaw and 4 others like this.
  3. simon-ferquel-unity

    simon-ferquel-unity

    Unity Technologies

    Joined:
    Apr 1, 2021
    Posts:
    67
    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:
    658
    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:
    583
    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:
    67
    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.
  9. Onigiri

    Onigiri

    Joined:
    Aug 10, 2014
    Posts:
    469
    Doesn't seem to be working
     
  10. Onigiri

    Onigiri

    Joined:
    Aug 10, 2014
    Posts:
    469
  11. ProtoTerminator

    ProtoTerminator

    Joined:
    Nov 19, 2013
    Posts:
    583
    For advanced async usages like WhenAll/WhenAny, I suggest using a complete async library. Some good options are UniTask or my own ProtoPromise.

    You can convert an
    Awaitable
    to a
    Promise
    /
    UniTask
    :

    Code (CSharp):
    1. public static async Promise<T> ToPromise(this Awaitable<T> awaitable)
    2. {
    3.     return await awaitable;
    4. }
    Then use
    Promise.All/Race
    or
    UniTask.WhenAll/WhenAny
    :

    Code (CSharp):
    1. await Promise.All(
    2.     awaitable1.ToPromise(),
    3.     awaitable2.ToPromise(),
    4. );
     
    Trigve and mockah like this.
  12. Onigiri

    Onigiri

    Joined:
    Aug 10, 2014
    Posts:
    469
    I had to install UniTask for exactly this single method and I don't like 3rd party libraries(at least I'm trying to use as little as possible). That's why I'd like to have this by default. But thanks for help I'll try to search for some simple promise implementation.
     
  13. ProtoTerminator

    ProtoTerminator

    Joined:
    Nov 19, 2013
    Posts:
    583
    If that's the case, you could just use
    System.Threading.Tasks.Task
    , because that's built-in. Though it's considerably heavy compared to the 3rd-party libraries I mentioned.
     
  14. stonstad

    stonstad

    Joined:
    Jan 19, 2018
    Posts:
    658
    If you are not creating Systen.Threading tasks within an Update method, the overhead (memory + perf) is negligible.
     
  15. mockah

    mockah

    Joined:
    Jan 19, 2023
    Posts:
    5

    Great tip and was exactly what I needed.

    Would like to point out that Unitask also comes with an extension method included to convert
    Awaitable
    ->
    Unitask
    with
    AsUnitask
    and
    AsUnitask<T>
    . Though, it's not directly stated on the repo page (just mentions
    Task
    conversion). I currently can't say ProtoPromise has a similar extension.
     
  16. ProtoTerminator

    ProtoTerminator

    Joined:
    Nov 19, 2013
    Posts:
    583
    It's a trivial extension. Is that something you'd be interested in having added to ProtoPromise?
     
  17. mockah

    mockah

    Joined:
    Jan 19, 2023
    Posts:
    5
    I've yet to use ProtoPromise and to be honest this was the first time I ended up using Unitask as well. Personally, I appreciate the convenience when a library offers simple conversions built in (provided it's relevant) instead of me having to reinvent the wheel on each new project but that's just me. So I'd vote yes on including the extension.
     
  18. John_Leorid

    John_Leorid

    Joined:
    Nov 5, 2012
    Posts:
    646
    this should be
    int count = awaitables.Length;

    I think.
     
  19. ProtoTerminator

    ProtoTerminator

    Joined:
    Nov 19, 2013
    Posts:
    583
    Thanks for the feedback. I will add it.
     
    mockah likes this.