Given three tasks - FeedCat()
, SellHouse()
and BuyCar()
, there are two interesting cases: either they all complete synchronously (for some reason, perhaps caching or an error), or they don't.
Let's say we have, from the question:
Task<string> DoTheThings() {
Task<Cat> x = FeedCat();
Task<House> y = SellHouse();
Task<Tesla> z = BuyCar();
// what here?
}
Now, a simple approach would be:
Task.WhenAll(x, y, z);
but ... that isn't convenient for processing the results; we'd typically want to await
that:
async Task<string> DoTheThings() {
Task<Cat> x = FeedCat();
Task<House> y = SellHouse();
Task<Tesla> z = BuyCar();
await Task.WhenAll(x, y, z);
// presumably we want to do something with the results...
return DoWhatever(x.Result, y.Result, z.Result);
}
but this does lots of overhead and allocates various arrays (including the params Task[]
array) and lists (internally). It works, but it isn't great IMO. In many ways it is simpler to use an async
operation and just await
each in turn:
async Task<string> DoTheThings() {
Task<Cat> x = FeedCat();
Task<House> y = SellHouse();
Task<Tesla> z = BuyCar();
// do something with the results...
return DoWhatever(await x, await y, await z);
}
Contrary to some of the comments above, using await
instead of Task.WhenAll
makes no difference to how the tasks run (concurrently, sequentially, etc). At the highest level, Task.WhenAll
predates good compiler support for async
/await
, and was useful when those things didn't exist. It is also useful when you have an arbitrary array of tasks, rather than 3 discreet tasks.
But: we still have the problem that async
/await
generates a lot of compiler noise for the continuation. If it is likely that the tasks might actually complete synchronously, then we can optimize this by building in a synchronous path with an asynchronous fallback:
Task<string> DoTheThings() {
Task<Cat> x = FeedCat();
Task<House> y = SellHouse();
Task<Tesla> z = BuyCar();
if(x.Status == TaskStatus.RanToCompletion &&
y.Status == TaskStatus.RanToCompletion &&
z.Status == TaskStatus.RanToCompletion)
return Task.FromResult(
DoWhatever(a.Result, b.Result, c.Result));
// we can safely access .Result, as they are known
// to be ran-to-completion
return Awaited(x, y, z);
}
async Task Awaited(Task<Cat> a, Task<House> b, Task<Tesla> c) {
return DoWhatever(await x, await y, await z);
}
This "sync path with async fallback" approach is increasingly common especially in high performance code where synchronous completions are relatively frequent. Note it won't help at all if the completion is always genuinely asynchronous.
Additional things that apply here:
with recent C#, a common pattern is for the async
fallback method is commonly implemented as a local function:
Task<string> DoTheThings() {
async Task<string> Awaited(Task<Cat> a, Task<House> b, Task<Tesla> c) {
return DoWhatever(await a, await b, await c);
}
Task<Cat> x = FeedCat();
Task<House> y = SellHouse();
Task<Tesla> z = BuyCar();
if(x.Status == TaskStatus.RanToCompletion &&
y.Status == TaskStatus.RanToCompletion &&
z.Status == TaskStatus.RanToCompletion)
return Task.FromResult(
DoWhatever(a.Result, b.Result, c.Result));
// we can safely access .Result, as they are known
// to be ran-to-completion
return Awaited(x, y, z);
}
prefer ValueTask<T>
to Task<T>
if there is a good chance of things ever completely synchronously with many different return values:
ValueTask<string> DoTheThings() {
async ValueTask<string> Awaited(ValueTask<Cat> a, Task<House> b, Task<Tesla> c) {
return DoWhatever(await a, await b, await c);
}
ValueTask<Cat> x = FeedCat();
ValueTask<House> y = SellHouse();
ValueTask<Tesla> z = BuyCar();
if(x.IsCompletedSuccessfully &&
y.IsCompletedSuccessfully &&
z.IsCompletedSuccessfully)
return new ValueTask<string>(
DoWhatever(a.Result, b.Result, c.Result));
// we can safely access .Result, as they are known
// to be ran-to-completion
return Awaited(x, y, z);
}
if possible, prefer IsCompletedSuccessfully
to Status == TaskStatus.RanToCompletion
; this now exists in .NET Core for Task
, and everywhere for ValueTask<T>