You can also convert Awaitable to UniTask with AsUniTask.
Awaitable seems to mainly exist for 0 dependency library development, Unity’s own packages and as a GC friendlier alternative to Unity Coroutines. It is not a Task/UniTask replacement.
Thank you for your insight, I am wondering though if we can get an answer from someone at unity as to why there is no support for this, or is is there going to be a support for this?
I’ve created an AwaitableUtility which contains a bunch of WaitAll methods.
All the WhenAll methods work similar to Task.WhenAll:
The returned Awaitable only completes once all the provided awaitables have completed (duh!).
They catch exceptions from all the provided awaitables that fail, and group them inside a single AggregateException.
The returned AggregateException excludes OperationCancelledExceptions - unless only OperationCancelledExceptions were thrown, in which case it returns an AggregateException containing a single OperationCancelledException(*).
(*) If I recall correctly, Task.WhenAll actually returns an AggregateException with zero inner exceptions, if all thrown exceptions were OperationCancelledExceptions. But I’m intentionally deviating from that in my implementation, because I find it potentially surprising behaviour if an AggregateException is thrown with a null InnerException.
Usage:
var awaitable1 = AwaitableUtility.FromResult(true);
var awaitable2 = AwaitableUtility.FromResult(1);
var (result1, result2) = await AwaitableUtility.WhenAll(awaitable1, awaitable2);
All the methods in the utility class have been unit tested to make sure they work as expected.
This is the only way we can have WhenAll functionality with Awaitables. I admire your patience in writing all those different WhenAll methods!
Just two points of caution for anyone reading this:
If your code is in a performance-critical path and you don’t expect your Awaitables to throw exceptions, consider awaiting them one by one manually, as the try/catch blocks can introduce a slight performance impact.
Since Awaitables are pooled, you won’t be able to go back and inspect each Awaitable to check which ones succeeded or failed, or to view any errors for failed Awaitables, as they will have returned to the pool.
Apart from these two limitations, unavoidable due to the nature of Awaitable, this is the best possible implementation of WhenAll.
I’m a big fan of pulling complexity down, so I’m always happy to put in the work for things like this, if I think it can help lower complexity and fragility across the entire codebase
Great, relevant points from an absolute legend. You might very well be right that Unity’s developers intentionally decided to not introduce WhenAny/WhenAll support for some of the aforementioned reasons.
But I do think that the context of Awaitables is different enough from ValueTask, that it should warrant a whole different discussion.
In the C# world, Task is the core abstraction intended to be used for async results, and ValueTask is just this optional little thing, that may be used in some rare edge cases if it’s convenient. If ValueTask doesn’t do it, it’s not a biggie - just use a Task instead.
On the Unity side, Awaitable is all we’ve got, the main abstraction for the async/await pattern. This means that it not having WhenAll support is a much bigger pain point in practice.
And while it’s not impossible to convert from an Awaitable to a Task, there is no easy shortcut to do this like Awaitable.AsTask, and doing that would potentially have major and far-reaching implications on cancellation handling in many cases anyways.
Generating garbage, I think, is also a way bigger deal in the context of game development in particular, which means that converting all awaitables into tasks, sticking them inside a new array, and passing them to a method that returns yet another new array, is quite far from ideal.
Thank you so much @meredoth and @sisus_co for your contribution i highly appreciate the code and considerations when using it.
It is kind of a shame that unity provide such a cool tool like awaitables, yet it is lacking some core functionality, the alternative of using UniTask to use whenAll is a bit incomplete as it will once again diverge from the game time and will continue to execute even when we stop the game.
In the end, it was a decision between performance and usability. If Unity wanted to implement WhenAll or WhenAny without any drawbacks, the Awaitable instances would have to remain unpooled so they could still exist after their results were set, which would generate garbage.
Unity had to choose between prioritizing performance over usability, or implement these methods with a focus on usability over performance, or find a middle-ground solution, such as making Awaitable instances poolable but only manually disposable by the user. In my opinion, that compromise would impact both usability and performance and ultimately satisfy no one.
Having to await manually each Awaitable when you are sure there won’t be any exceptions thrown, or use @sisus_co implementation, is not that much of a problem in my opinion.
The WhenAny is a bigger issue, but something like that cannot be reliably implemented because after the result is set, as far as I have seen we cannot unregister any callbacks from the Awaitable before sending it back to the pool.
I don’t agree that it’s that bad. It’s 100% possible to implement WhenAll and WhenAny as Awaitable exists today.
There are only two problems, as far as I see it:
Handling multiple awaitables with different types of results can’t be done without introducing many overloads.
It might not be obvious to users that if they pass an Awaitable to a WhenAll method, they can no longer await it themselves.
When it comes to the first problem, well, they could at least add WhenAll(Awaitable[]), WhenAll<T>(Awaitable<T>[]), WhenAny(Awaitable[]) and WhenAny<T>(Awaitable<T>[]) very easily.
And while it wouldn’t make for the most elegant, minimalistic API to have several overloads to support awaitables with mixed return types, it’s certainly still an option. I mean, this thing isn’t the most elegant thing in the world either, yet I still think the decision to add the Action<T...> types into .NET was a great one.
As for the second problem, while I can understand where the argument is coming from, I don’t think it’s that bad in practice, for a lot of reasons:
In my view, it’s pretty obvious that when you pass awaitables to a WhenAll method and await the result of that method, the awaitables that were passed to the method also get implicitly awaited. I don’t really see how the risk of users accessing members on those awaitables after the fact would be any higher, than it is for them to access members on individual awaitables after they’ve awaited them manually (as long as the WhenAll method returns all the awaitables’ results, which I think is crucial!).
I think the problem can be alleviated even further, simply by adding a note about this behaviour to the documentation.
My intuition is that not offering built-in When and WhenAll functionality is even riskier, as it pushes users to try and manually implement these methods for themselves. Since the methods aren’t exactly trivial to implement, given the implementation details of awaitables, the risk of these user-created solutions having bugs is quite high.
I think it’s worth pointing out that the same issue applies to every single situation where an Awaitable is assigned to any kind of variable. That is not to say that this in any way diminishes the validity of the point, I think it is good practice to avoid doing this in general. But my point is that Awaitables kinda just are dangerous-by-design, and I think it’s more effective to teach people how they can and can’t be used, rather than act as if these limitations didn’t exist, by avoiding implementing extremely useful tools like WhenAll.
You can pass all the awaitables directly to WhenAll from methods that return them, so that they don’t get assigned to any local variables, making it impossible to accidentally examine their results after they have been awaited.
I haven’t run into the need for WhenAny myself so far, so it’s not part of my AwaitableUtils, but I believe it can be implemented using AwaitableCompletionSource.TrySetResult and AwaitableCompletionSource.TrySetException:
Not making it allocate anything and be thread safe would be more challenging, though. It would require using object pooling for the AwaitableCompletionSource, similar to what Unity does with the awaitables themselves using ThreadLocal<ObjectPool<Awaitable>>.
Perhaps I didn’t explain it correctly, when I said
I meant that in the case we have one or more exceptions thrown, because the instances go back to the pool there is no way to know what Awaitable threw, and no way to get the result from the Awaitables that didn’t. Let’s suppose that we have the following three Awaitables that each may throw some exception:
private async Awaitable<int> Awaitable1()
{
var rnd = Random.value;
await Awaitable.EndOfFrameAsync();
if (rnd < 0.25) // Some Random Exceptions
throw new IndexOutOfRangeException();
if (0.25 <= rnd && rnd < 0.5)
throw new ArgumentException();
return 42;
}
private async Awaitable<int> Awaitable2()
{
var rnd = Random.value;
await Awaitable.EndOfFrameAsync();
if (rnd < 0.25) // Some Random Exceptions
throw new IndexOutOfRangeException();
if (0.25 <= rnd && rnd < 0.5)
throw new ArgumentException();
return 10;
}
private async Awaitable<int> Awaitable3()
{
var rnd = Random.value;
await Awaitable.EndOfFrameAsync();
if (rnd < 0.25) // Some Random Exceptions
throw new IndexOutOfRangeException();
if (0.25 <= rnd && rnd < 0.5)
throw new ArgumentException();
return 420;
}
If Unity has code that implements WhenAll, which means that we cannot change it, then this:
creates some problems because the Awaitables after the WhenAll are back into the pool:
We don’t know which of the Awaitables failed and with what exception each, we only know the number of Awaitables that failed and all the exceptions they have thrown.
We don’t know which succeeded and we don’t have a way to get the result from the ones that did.
Essentially the only thing that we can do, is try to await each one individually from the beginning, check which fails and do something about it, check which succeeds and store the result. This will be double the work, if there are any failures.
In this case, it is the same as awaiting all of them individually from the beginning each with its own try/catch block.
The WhenAny implementation actually works as expected from my limited testing.
Ah, right. Yeah, that’s a good point Sometimes it might be important to be able to extract partial results even if an exception is thrown by some of the awaitables.
One way that I can think of enabling that possiblity, would be to attach all the results that were acquired into the aggregate exception that is thrown:
public sealed class WhenAllException : AggregateException
{
private readonly object[] results;
private readonly Exception[] exceptions;
public bool TryGetMemberResult<TResult>(int memberIndex, out TResult result)
{
if(exceptions[memberIndex] is null)
{
result = (TResult)results[memberIndex];
return true;
}
result = default;
return false;
}
public bool TryGetMemberException<TException>(int memberIndex, out TException exception) where TException : Exception
{
if(exceptions[memberIndex] is TException memberException)
{
exception = memberException;
return true;
}
exception = default;
return false;
}
}
Another possibility would be to have WhenAll return a custom result object, which has a GetAwaiter method. Awaiting this object would never cause an exception to be thrown immediately, but instead any exceptions thrown by the member awaitables would get attached to the result object, and only get triggered if/when the user tries to access their results via the result object.
public readonly struct CompletedAwaitables<TResult1, TResult2>
{
// NOTE: These would not be the same awaitable instances that were already awaited,
// but new instances acquired using AwaitableUtility.FromResult<T> / FromException<T>.
public Awaitable<TResult1> First { get; }
public Awaitable<TResult2> Second { get; }
public Awaitable<Results<TResult1, TResult2>> GetAwaiter() => ...
}
Is it possible to implement Awaitable versions of WhenAll, WhenAny, etc. which behave 100% like the Task versions in all cases? → No
Is it possible to implement Awaitable versions which are “good enough” for 99% of the cases? → Yes
Will Unity ever add these versions themself? → Maybe, but even if they decide to add them it will take a long time, so it’s better to just add them yourself
Both of these could work, and both of these will add a performance overhead, the first because of the different castings and the second because of the creation of a new object and the storing of the stack for the asynchronous execution from the GetAwaiter to the heap.
As I mentioned earlier, given how Awaitables are implemented, any extensions to their functionality will inherently come with performance trade-offs. In my opinion, Unity designed Awaitables to prioritize maximum performance, even if that meant certain useful Task features would be cumbersome to implement.
That said, I don’t see why implementing a WhenAll for three Awaitables is significantly better than simply writing three await statements manually, especially considering the additional complexity and code required. If exceptions are a possibility, wrapping each in a try/catch should be enough. In any case exceptions should be really rare in any program both for architectural and performance reasons.
Finally, it’s important to recognize that Unity doesn’t provide a true Task equivalent. Instead, Awaitables are more akin to ValueTask in that they are optimized for performance and can only be awaited once. For those who need full Task functionality, it’s better to use UniTask or the standard Task with cancellations as appropriate. I think that making this distinction would make everyone less stressed about it.
The first approach will only introduce additional overhead in the case of an exception being thrown. And at that point the exceptions themselves will likely have orders of magnitude more overhead than some casting.
The second approach would only need to introduce a miniscule overhead from creating some additional instances of structs, or from unpooling/pooling some re-used instances, depending on how it’s implemented.
I personally feel like it would be pretty extreme micro-optimization, to sacrifice readability for the sake of avoid overhead of that level.
If you care about being able to catch exceptions from all awaitables that might fail, instead of just the first one, then I think there’s a quite drastic benefit to readability:
WhenAll:
var (result1, result2, result) = await AwaitableUtility.WhenAll(GetAwaitable1(), GetAwaitable2(), GetAwaitable3());
In cases where you don’t care about that, and don’t want to introduce any sort of exception handling for failing awaitables, then yeah, awaiting each one by hand can also be done without sacrificing readability.
That might also depend on where you focus your analysis.
If you look the overall complexity and amount of code in the codebase, then reusable utility methods that encapsulate a lot of complexity behind a simple interface can also help a lot in bringing them down.
One could also say that classes like HashSet<T> and Dictionary<T> “introduce a lot of additional complexity and code” to your codebase. But I would argue that in practice they actually tend to help a lot in reducing the overall complexity of codebases.
Once the work has been put in to implement a good, deep abstraction, you don’t need to worry about the complexities of its implementation details every time you use it, which can help reduce mental overhead from reading code in all places where the abstraction is used.
I think I misunderstood the first example. If your intention was to create a class that casts or boxes the results into object only when there is at least one exception, then yes, the performance cost would be negligible compared to the cost of throwing an exception.
Regarding the second example, I initially considered the additional performance cost of the async machine saving the stack to the heap when an asynchronous operation isn’t completed immediately upon being awaited, for the CompletedAwaitables struct. However, after re-reading it, I realized that all operations would complete synchronously because they are created using FromResult or FromException.
The second approach seems more appealing to me but will require more code due to handling different arities. In any case, I suggest trying simple implementations for both approaches and benchmarking them to compare their performance before committing to one.
P.S. I don’t think I’ll be using Awaitables frequently enough to justify writing all that code myself, but I would certainly use your implementation if I needed a WhenAll and you were willing to share it.
I’m not sure we want the same thing, but for me I just wanted to wait the array before moving on.
So when I read that Awaitables don’t have this method I made this one, and it worked just fine for me:
public static async Awaitable WhenAll(this Awaitable[] toWait)
{
var completedCount = 0;
for (int i = 0; i < toWait.Length; i++)
{
toWait[i].GetAwaiter().OnCompleted(() => completedCount++);
}
while (completedCount < toWait.Length)
await Awaitable.EndOfFrameAsync();
}
You can avoid the check every frame if you use a AwaitableCompletionSource instead and set the “result” in the OnCompleted callback of the last completed task.
Yeah, a downside with that implementation (besides the risk of some errors getting lost if multiple awaitables were to fail) is that the combined Awaitable’s completion will get delayed until the end of the frame, instead of happening at the exact moment that all the awaitables in the array have completed.
I would just do this instead:
public static async Awaitable WhenAll(this Awaitable[] awaitables)
{
foreach(var awaitable in awaitables)
{
await awaitable;
}
}