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
  2. If you have experience with import & exporting custom (.unitypackage) packages, please help complete a survey (open until May 15, 2024).
    Dismiss Notice
  3. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice

Awaitable feature requests/bugs

Discussion in 'Experimental Scripting Previews' started by mdsitton, May 10, 2023.

  1. mdsitton

    mdsitton

    Joined:
    Jan 27, 2018
    Posts:
    66
    After diving into using the new async Awaitable i've noticed a few limitations. First WaitUntil is extremely useful in coroutines so having this officially implemented for Awaitable would remove some boilerplate when migrating code.

    Additionally not having WaitForSecondsRealtimeAsync is a real issue because any code that uses Time.timeScale to pause game play can cause issues when you may be using Awaitable in tandem with any UI code on-top of the game which may be paused. Also before anyone askes the currently supported Awaitable.WaitForSecondsAsync is effected by time scaling.

    @JoshPeterson any thoughts on adding these things?
     
    FoodFish_ likes this.
  2. mdsitton

    mdsitton

    Joined:
    Jan 27, 2018
    Posts:
    66
    Additionally i've found a bug with:
    await Awaitable.NextFrameAsync();

    When calling Time.timeScale = 0 (i believe on the same frame as the await) it will result in the NextFrameAsync not returning at some point and getting stuck.

    Code (CSharp):
    1.     public async void StartTest()
    2.     {
    3.         Debug.Log("Starting test");
    4.         _statusLine.text = $"Starting test";
    5.         Time.timeScale = 0;
    6.         int frames = 0;
    7.         while (true)
    8.         {
    9.             frames++;
    10.             _statusLine.text = $"Frames {frames} {Time.frameCount}";
    11.             await Awaitable.NextFrameAsync();
    12.         }
    13.     }

    I've reported the bug with a reproduction project: IN-40766
     
  3. simon-ferquel-unity

    simon-ferquel-unity

    Unity Technologies

    Joined:
    Apr 1, 2021
    Posts:
    68
    Hi, WaitUntil can be easily implemented in user code:

    Code (CSharp):
    1. public static async Awaitable(Func<bool> condition, CancellationTokenSource cancellationToken)
    2. {
    3.    while(!condition()){
    4.      cancellationToken.ThrowIfCancellationRequested();
    5.      await Awaitable.WaitForNextFrameAsync();
    6.   }
    7. }
    WaitForSecondsRealtimeAsync would be a welcome addition, right.

    For the timeScale issue, thanks for reporting it. We'll have a look at it.
     
  4. mdsitton

    mdsitton

    Joined:
    Jan 27, 2018
    Posts:
    66
    Awesome thanks! Also as for the timeScale issue i was told on the report that it was "by design" which doesn't make sense to me so i wouldn't mind some clarification on this.
     
  5. mdsitton

    mdsitton

    Joined:
    Jan 27, 2018
    Posts:
    66
    Also after playing around with my reproduction project i may have found another bug. Unless I am misunderstanding something here which if that is the case maybe the documentation needs to be clarified.

    Reading the docs here says
    Note: some methods returning an awaitable also accept a CancellationToken. Both cancelation models are equivalent.
    Which leads me to believe that it should be canceling the whole running task and stop execution then return to the caller with an exception. But what i am observing is a bit different.


    Code (CSharp):
    1.     async Awaitable CountFrames()
    2.     {
    3.         int frames = 0;
    4.         while (true)
    5.         {
    6.             frames++;
    7.             _statusLine.text = $"Frames {frames} {Time.frameCount}";
    8.             await Awaitable.EndOfFrameAsync();
    9.         }
    10.     }
    11.  
    12.     Awaitable currentTask = null;
    13.  
    14.     public async void StartTest()
    15.     {
    16.         if (currentTask != null)
    17.         {
    18.             Debug.Log("Cancel test");
    19.             currentTask.Cancel();
    20.             currentTask = null;
    21.             _statusLine.text = "Task canceled";
    22.             return;
    23.         }
    24.         Debug.Log("Starting test");
    25.         _statusLine.text = $"Starting test";
    26.         currentTask = CountFrames();
    27.         await currentTask;
    28.     }
    What happens here is that the CountFrames() task continues running indefinitely and doesn't stop while the caller in StartTest() gets the exception and exits.

    Also i am running unity 2023.1.0b15
     
    Last edited: May 11, 2023
  6. mdsitton

    mdsitton

    Joined:
    Jan 27, 2018
    Posts:
    66
    Any update on this?
     
  7. kdchabuk

    kdchabuk

    Joined:
    Feb 7, 2019
    Posts:
    52
    If I understand correctly, the code should be:
    Code (CSharp):
    1.     public static async Awaitable Until(Func<bool> condition, CancellationToken cancellationToken)
    2.     {
    3.        while(!condition()){
    4.          cancellationToken.ThrowIfCancellationRequested();
    5.          await Awaitable.NextFrameAsync();
    6.       }
    7.     }
    Then you can pass in a token with something like:
    Code (CSharp):
    1. cancellationTokenSource = new CancellationTokenSource();
    2. currentTask = Until(myCondition, cancellationTokenSource.Token);
    It's not clear how Awaitable.Cancel() affects the running awaitable itself.

    Edit:
    However you can cancel it by calling
    cancellationTokenSource.Cancel()
     
    Last edited: Nov 21, 2023
  8. mdsitton

    mdsitton

    Joined:
    Jan 27, 2018
    Posts:
    66
    Yes i could use a cancellation token but the point is that their docs say they should be functionally equivalent and they were not functioning as such
     
    kdchabuk likes this.
  9. mdsitton

    mdsitton

    Joined:
    Jan 27, 2018
    Posts:
    66
    kdchabuk likes this.
  10. FoodFish_

    FoodFish_

    Joined:
    May 3, 2018
    Posts:
    58
    WaitUntil, and even WaitUntilCanceled would really valuable feature which would encourage people from making the switch from UniTask framework to Awaitables.

    upload_2023-7-3_8-38-9.png
     
    Last edited: Jul 3, 2023
    Prodigga likes this.
  11. mdsitton

    mdsitton

    Joined:
    Jan 27, 2018
    Posts:
    66
    Agreed i would very much like to not have to pull in a 3rd party library or write my own implementation for something that should just be in the standard unity library from the beginning
     
    Prodigga and FoodFish_ like this.
  12. jGate99

    jGate99

    Joined:
    Oct 22, 2013
    Posts:
    1,952
    Could someone please explain this limitation?
    Does it mean i cant call WaitForSeconds 2 times while inside an async method?

    https://docs.unity3d.com/2023.2/Documentation/Manual/AwaitSupport.html
     
  13. simon-ferquel-unity

    simon-ferquel-unity

    Unity Technologies

    Joined:
    Apr 1, 2021
    Posts:
    68
    You can do something like this:

    Code (CSharp):
    1. async Awaitable Foo(){
    2.   await Awaitable.WaitForSecondsAsync(2);
    3.   // do something
    4.   await Awaitable.WaitForSecondsAsync(2);
    5.   // do something else
    6. }
    But you can't do something like this:

    Code (CSharp):
    1. async awaitable Bar(){
    2.   var taskWithResult = SomeAwaitableReturningFunction();
    3.   var awaitOnce = await taskWithResult;
    4.   // do something
    5.   var awaitTwice = await taskWithResult; // at this point taskWithResult has already been pooled back. This will mess with other async flows
    6.  }
     
    jGate99 likes this.
  14. jGate99

    jGate99

    Joined:
    Oct 22, 2013
    Posts:
    1,952
    Thanks for your prompt reply, its clear now
     
  15. jGate99

    jGate99

    Joined:
    Oct 22, 2013
    Posts:
    1,952
    Hi there
    I have another question. I'm using an async Task method with awaitable, which internally calls some other methods. One of those method calls another library method that throws an exception. I've implemented a try-catch within my internal method, which was called from my async method. However, despite the try-catch, it seems that my async-await method just stops or doesn't progress any further after exception happens.

    How could I solve this?

    Thanks
     
  16. Onigiri

    Onigiri

    Joined:
    Aug 10, 2014
    Posts:
    489
    Any plans to add Awaitable.WhenAll and Awaitable.WhenAny?
     
    Molder, mdsitton and NikolaNikolov like this.
  17. Tortuap

    Tortuap

    Joined:
    Dec 4, 2013
    Posts:
    137
    @simon-ferquel-unity Can we know where are you on this issue?

    I just reported the very same bug (and lost one hour doing) while discovering it is known since almost ten month :-/

    This is rather critical as it produces dead lock situations in NextFrameAsync, EndOfFrameAsync or FixedUpdateAsync.
     
    Last edited: Feb 13, 2024
  18. Tortuap

    Tortuap

    Joined:
    Dec 4, 2013
    Posts:
    137
    Here is my personal implementation, feel free to use:
    Code (CSharp):
    1. using System;
    2. using System.Collections.Generic;
    3. using System.Reflection;
    4. using System.Runtime.CompilerServices;
    5. using System.Runtime.ExceptionServices;
    6. using UnityEngine;
    7.  
    8. public static class Awaitables
    9. {
    10.     //! Gets an Awaitable that has already completed successfully.
    11.     public static Awaitable CompletedAwaitable
    12.     {
    13.         [MethodImpl ( MethodImplOptions.AggressiveInlining )]
    14.         get
    15.         {
    16.             var acs = new AwaitableCompletionSource();
    17.             acs.SetResult ();
    18.             return acs.Awaitable;
    19.         }
    20.     }
    21.  
    22.     //! Gets a Awaitable that will complete when all of the supplied Awaitable have completed.
    23.     [MethodImpl ( MethodImplOptions.AggressiveInlining )]
    24.     public static Awaitable WhenAll ( params Awaitable[] awaitables )
    25.     {
    26.         Debug.Assert ( awaitables != null );
    27.  
    28.         [MethodImpl ( MethodImplOptions.AggressiveInlining )]
    29.         static async Awaitable __AwaitAll ( params Awaitable[] awaitables )
    30.         {
    31.             for ( int i = 0; i < awaitables.Length; i++ )
    32.             {
    33.                 Debug.Assert ( awaitables[i] != null );
    34.                 await awaitables[i];
    35.             }
    36.         }
    37.  
    38.         if ( awaitables.Length == 0 )
    39.         {
    40. #if UNITY_EDITOR
    41.             Debug.LogWarning ( $"Performance warning : awaiting an empty array of Awaitable" );
    42. #endif
    43.             return CompletedAwaitable;
    44.         }
    45.  
    46.         if ( awaitables.Length == 1 )
    47.         {
    48.             Debug.Assert ( awaitables[0] != null );
    49.             return awaitables[0];
    50.         }
    51.  
    52.         return __AwaitAll ( awaitables );
    53.     }
    54.  
    55.     //! Gets a Awaitable that will complete when any of the supplied Awaitable have completed.
    56.     [MethodImpl ( MethodImplOptions.AggressiveInlining )]
    57.     public static Awaitable WhenAny ( params Awaitable[] awaitables )
    58.     {
    59.         Debug.Assert ( awaitables != null );
    60.  
    61.         [MethodImpl ( MethodImplOptions.AggressiveInlining )]
    62.         static async Awaitable __AwaitAny ( params Awaitable[] awaitables )
    63.         {
    64.             AwaitableCompletionSource awaited = new AwaitableCompletionSource ();
    65.  
    66.             [MethodImpl ( MethodImplOptions.AggressiveInlining )]
    67.             async Awaitable __WaitCompletion ( Awaitable awaitable )
    68.             {
    69.                 await awaitable;
    70.                 if ( awaited != null )
    71.                 {
    72.                     awaited.SetResult ();
    73.                     awaited = null;
    74.                 }
    75.             }
    76.  
    77.             for ( int i = 0; i < awaitables.Length; i++ )
    78.             {
    79.                 Debug.Assert ( awaitables[i] != null );
    80.                 Run ( __WaitCompletion ( awaitables[i] ) );
    81.             }
    82.  
    83.             if ( awaited != null )
    84.                 await awaited.Awaitable;
    85.         }
    86.  
    87.         if ( awaitables.Length == 0 )
    88.         {
    89. #if UNITY_EDITOR
    90.             Debug.LogWarning ( $"Performance warning : awaiting an empty array of Awaitable" );
    91. #endif
    92.             return CompletedAwaitable;
    93.         }
    94.  
    95.         if ( awaitables.Length == 1 )
    96.             return awaitables[0];
    97.  
    98.         return __AwaitAny ( awaitables );
    99.     }
    100.  
    101.     static FieldInfo __continuationFieldInfo = typeof ( Awaitable ).GetField ( "_continuation", BindingFlags.NonPublic | BindingFlags.Instance );
    102.     [MethodImpl ( MethodImplOptions.AggressiveInlining )]
    103.     static bool __HasContinuation ( Awaitable awaitable )
    104.         => __continuationFieldInfo.GetValue ( awaitable ) != null;
    105.  
    106.     //! Runs an Awaitable without awaiting for it.
    107.     //! On completion, rethrow any exception raised during execution.
    108.     [MethodImpl ( MethodImplOptions.AggressiveInlining )]
    109.     public static void Run ( this Awaitable self )
    110.     {
    111.         Debug.Assert ( self != null );
    112.         Debug.Assert ( ! __HasContinuation ( self ), "Awaitable already have a continuation, is it already awaited?" );
    113.  
    114.         var awaiter = self.GetAwaiter ();
    115.         awaiter.OnCompleted ( () => awaiter.GetResult () );
    116.     }
    117.  
    118.  
    119.     //! Run multiple Awaitable without awaiting for any.
    120.     //! On completion, rethrow any exception raised during execution.
    121.     [MethodImpl ( MethodImplOptions.AggressiveInlining )]
    122.     public static void Run ( IEnumerable<Awaitable> list )
    123.     {
    124.         Debug.Assert ( list != null );
    125.  
    126.         foreach ( var item in list )
    127.         {
    128.             Debug.Assert ( item != null );
    129.             var awaiter = item.GetAwaiter ();
    130.             awaiter.OnCompleted ( () => awaiter.GetResult () );
    131.         }
    132.     }
    133.  
    134.     //! Create an Awaitable that first await the supplied Awaitable, then execute the continuation, once completed.
    135.     [MethodImpl ( MethodImplOptions.AggressiveInlining )]
    136.     public static Awaitable WithContinuation ( this Awaitable self, Action continuation )
    137.     {
    138.         Debug.Assert ( self != null );
    139.         Debug.Assert ( continuation != null );
    140.  
    141.         if ( !self.IsCompleted )
    142.         {
    143.             [MethodImpl ( MethodImplOptions.AggressiveInlining )]
    144.             async Awaitable __AwaitAndContinue ()
    145.             {
    146.                 await self;
    147.                 continuation ();
    148.             }
    149.  
    150.             return __AwaitAndContinue ();
    151.         }
    152.         else
    153.         {
    154.             continuation ();
    155.             return self;
    156.         }
    157.     }
    158.  
    159.     //! Set a continuation, executed once the Awaitable has completed.
    160.     //! Note that continuation will be overwritten if Awaitable is awaited.
    161.     //! This is an unusual method to use, be sure what you are doing.
    162.     [MethodImpl ( MethodImplOptions.AggressiveInlining )]
    163.     public static void ContinueWith ( this Awaitable self, Action continuation )
    164.     {
    165.         Debug.Assert ( self != null );
    166.         Debug.Assert ( continuation != null );
    167.  
    168.         if ( ! self.IsCompleted )
    169.         {
    170.             var awaiter = self.GetAwaiter ();
    171.             awaiter.OnCompleted ( () =>
    172.             {
    173.                 continuation ();
    174.                 awaiter.GetResult ();
    175.             } );
    176.         }
    177.         else continuation ();
    178.     }
    179.  
    180.     // For debugging purpose only:
    181.     //
    182.  
    183.     /*
    184.     [MethodImpl ( MethodImplOptions.AggressiveInlining )]
    185.     public static IntPtr GetHandle ( this Awaitable awaitable )
    186.     {
    187.         Debug.Assert ( awaitable != null );
    188.         var handle = (IntPtr)awaitable.GetFieldValue ( "_handle" ).GetFieldValue ( "_handle" );
    189.         return handle;
    190.     }
    191.  
    192.     public static bool IsManagedAwaitableDone ( this Awaitable awaitable )
    193.     {
    194.         Debug.Assert ( awaitable != null );
    195.         return (bool)awaitable.GetFieldValue ( "_managedAwaitableDone" );
    196.     }
    197.  
    198.     public static Exception GetException ( this Awaitable awaitable )
    199.     {
    200.         Debug.Assert ( awaitable != null );
    201.         var edi = (ExceptionDispatchInfo)awaitable.GetFieldValue ( "_exceptionToRethrow" );
    202.         if ( edi != null )
    203.             return edi.SourceException;
    204.         return null;
    205.     }
    206.     */
    207. }
     
    Last edited: Feb 13, 2024
    Onigiri likes this.
  19. mdsitton

    mdsitton

    Joined:
    Jan 27, 2018
    Posts:
    66
    I abanonded use of awaitable for anything since i originally discovered the issue so i haven't tested the feature again since but they closed my original support ticket with the following response.

     
  20. mdsitton

    mdsitton

    Joined:
    Jan 27, 2018
    Posts:
    66
    Personally i think they misunderstood my request even though i tried several times to get them to understand. Even got them to reopen it after initially closing it as as-designed. Then they closed it again.

    My reproduction project included a frame counter as an illustration of the bug to show the unwanted behavior. The frame counter i had was not what i originally intended to use the functionality in my own code it was just to show that the feature was not working as expected.