Biorąc pod uwagę trzy zadania - FeedCat()
, SellHouse()
i BuyCar()
istnieją dwa ciekawe przypadki: albo wszystkie one kompletne synchronicznie (z jakiegoś powodu, może buforowania lub błędu), albo ich nie ma.
Powiedzmy, że mamy, z pytania:
Task<string> DoTheThings() {
Task<Cat> x = FeedCat();
Task<House> y = SellHouse();
Task<Tesla> z = BuyCar();
// what here?
}
Teraz prostym podejściem byłoby:
Task.WhenAll(x, y, z);
ale ... to nie jest wygodne do przetwarzania wyników; zazwyczaj chcielibyśmy await
tego:
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);
}
ale powoduje to duże obciążenie i przydziela różne tablice (w tym params Task[]
tablicę) i listy (wewnętrznie). Działa, ale to nie jest świetne IMO. Na wiele sposobów łatwiej jest korzystać z async
operacji i tylko await
po kolei:
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);
}
W przeciwieństwie do niektórych z powyższych komentarzy, użycie await
zamiast nieTask.WhenAll
ma żadnego znaczenia w sposobie działania zadań (jednocześnie, sekwencyjnie itp.). Na najwyższym poziomie Task.WhenAll
poprzedza dobrą obsługę kompilatora dla async
/ await
i był przydatny, gdy te rzeczy nie istniały . Jest to również przydatne, gdy masz dowolny zestaw zadań, a nie 3 zadania dyskretne.
Ale: nadal mamy problem, który async
/ await
generuje dużo hałasu kompilatora dla kontynuacji. Jeśli jest prawdopodobne, że zadania mogą faktycznie zostać wykonane synchronicznie, możemy to zoptymalizować, budując ścieżkę synchroniczną z asynchroniczną rezerwą:
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);
}
Takie podejście do „ścieżki synchronizacji z zastępowaniem asynchronicznym” jest coraz bardziej powszechne, szczególnie w kodach o wysokiej wydajności, w których synchroniczne uzupełnianie jest stosunkowo częste. Zauważ, że to wcale nie pomoże, jeśli zakończenie jest zawsze rzeczywiście asynchroniczne.
Obowiązują tutaj dodatkowe rzeczy:
w ostatnim C # wspólny wzorzec dla async
metody rezerwowej jest zwykle implementowany jako funkcja lokalna:
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);
}
preferuje ValueTask<T>
się Task<T>
, jeśli istnieje duża szansa rzeczy nigdy całkowicie synchronicznie z wieloma różnymi wartościami powrotów:
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);
}
jeśli to możliwe, wolą IsCompletedSuccessfully
się Status == TaskStatus.RanToCompletion
; teraz istnieje w .NET Core dla Task
i wszędzie dlaValueTask<T>