Oczekiwanie na wiele zadań z różnymi wynikami

237

Mam 3 zadania:

private async Task<Cat> FeedCat() {}
private async Task<House> SellHouse() {}
private async Task<Tesla> BuyCar() {}

Wszystkie muszą uruchomić się, zanim mój kod będzie mógł kontynuować, a ja też potrzebuję wyników od każdego z nich. Żaden z wyników nie ma ze sobą nic wspólnego

Jak zadzwonić i poczekać na wykonanie 3 zadań, a następnie na uzyskanie wyników?

Ian Vink
źródło
25
Czy masz jakieś wymagania dotyczące zamawiania? Czy chcesz nie sprzedawać domu, dopóki kot nie zostanie nakarmiony?
Eric Lippert,

Odpowiedzi:

411

Po użyciu WhenAllmożesz wyciągnąć wyniki indywidualnie za pomocą await:

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

await Task.WhenAll(catTask, houseTask, carTask);

var cat = await catTask;
var house = await houseTask;
var car = await carTask;

Możesz także użyć Task.Result(ponieważ wiesz, że do tego momentu wszystkie zostały pomyślnie ukończone). Jednak zalecam używanie, awaitponieważ jest wyraźnie poprawne, a Resultmoże powodować problemy w innych scenariuszach.

Stephen Cleary
źródło
83
Możesz po prostu WhenAllcałkowicie usunąć z tego; Oczekuje, że nie przejdziesz 3 późniejszych zadań, dopóki zadania nie zostaną zakończone.
Servy
134
Task.WhenAll()pozwala uruchomić zadanie w trybie równoległym . Nie rozumiem, dlaczego @Servy zaproponował, aby go usunąć. Bez WhenAllnich będą biegać jeden po drugim
Sergey G.
87
@Sergey: zadania zaczynają się natychmiast wykonywać. Np. catTaskJuż działa, zanim zostanie zwrócony FeedCat. Każde z tych podejść będzie działać - jedynym pytaniem jest, czy chcesz awaitje pojedynczo, czy wszystkie razem. Obsługa błędów jest nieco inna - jeśli go użyjesz Task.WhenAll, zrobi awaitto wszystko, nawet jeśli jeden z nich zawiedzie wcześnie.
Stephen Cleary
23
@Sergey Calling WhenAllnie ma wpływu na czas wykonywania operacji ani sposób ich wykonywania. To tylko ma żadnej możliwości dokonania w jaki sposób wyniki są przestrzegane. W tym konkretnym przypadku jedyną różnicą jest to, że błąd w jednej z dwóch pierwszych metod spowodowałby zgłoszenie wyjątku w tym stosie wywołań wcześniej w mojej metodzie niż w przypadku Stephena (chociaż ten sam błąd byłby zawsze zgłaszany, jeśli byłyby jakieś ).
Servy
37
@Sergey: Kluczem jest to, że metody asynchroniczne zawsze zwracają „gorące” (już uruchomione) zadania.
Stephen Cleary,
99

Tylko awaittrzy zadania osobno, po uruchomieniu ich wszystkich.

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

var cat = await catTask;
var house = await houseTask;
var car = await carTask;
Servy
źródło
8
@Bargitta Nie, to nieprawda. Będą wykonywać swoją pracę równolegle. Uruchom go i przekonaj się sam.
Servy
5
Ludzie po latach zadają to samo pytanie ... Uważam, że ważne jest, aby podkreślić, że zadanie „ zaczyna się od utworzenia ” w treści odpowiedzi : może nie zawracają sobie głowy czytaniem komentarzy
9
@StephenYork Dodanie Task.WhenAllzmian dosłownie nic na temat zachowania programu, w jakikolwiek możliwy do zaobserwowania sposób. Jest to czysto zbędne wywołanie metody. Możesz go dodać, jeśli chcesz, jako wybór estetyczny, ale nie zmienia to, co robi kod. Czas wykonania kodu będzie identyczny z tym wywołaniem metody lub bez niego (cóż, technicznie będzie to naprawdę niewielki narzut związany z wywoływaniem WhenAll, ale powinno to być nieistotne), co spowoduje , że ta wersja będzie działać nieco dłużej niż ta wersja.
Servy
4
@StephenYork Twój przykład uruchamia operacje sekwencyjnie z dwóch powodów. Twoje metody asynchroniczne nie są w rzeczywistości asynchroniczne, są synchroniczne. Fakt, że masz metody synchroniczne, które zawsze zwracają już ukończone zadania, uniemożliwia ich jednoczesne działanie. Następnie nie robisz tak, jak pokazano w tej odpowiedzi, rozpoczynając wszystkie trzy metody asynchroniczne, a następnie oczekując kolejno na trzy zadania. Twój przykład nie wywołuje każdej metody, dopóki poprzednia nie zakończy się, w ten sposób wyraźnie uniemożliwiając uruchomienie jednej, dopóki poprzednia nie zakończy się, w przeciwieństwie do tego kodu.
Servy
4
@MarcvanNieuwenhuijzen To w oczywisty sposób nieprawda, jak zostało to omówione w komentarzach tutaj i na innych odpowiedziach. Dodawanie WhenAlljest zmianą czysto estetyczną. Jedyną zauważalną różnicą w zachowaniu jest to, czy czekasz na zakończenie późniejszych zadań, jeśli wcześniejsze zadanie zawiedzie, co zwykle nie jest konieczne. Jeśli nie wierzysz w liczne wyjaśnienia, dlaczego twoje stwierdzenie nie jest prawdziwe, możesz po prostu uruchomić kod dla siebie i przekonać się, że to nieprawda.
Servy
37

Jeśli używasz C # 7, możesz użyć przydatnej metody otoki takiej jak ta ...

public static class TaskEx
{
    public static async Task<(T1, T2)> WhenAll<T1, T2>(Task<T1> task1, Task<T2> task2)
    {
        return (await task1, await task2);
    }
}

... aby włączyć taką wygodną składnię, gdy chcesz czekać na wiele zadań z różnymi typami zwrotów. Oczywiście musiałbyś wykonać wiele przeciążeń, aby czekała na nich różna liczba zadań.

var (someInt, someString) = await TaskEx.WhenAll(GetIntAsync(), GetStringAsync());

Jednak zapoznaj się z odpowiedzią Marca Gravella na kilka optymalizacji dotyczących ValueTask i już wykonanych zadań, jeśli zamierzasz zamienić ten przykład w coś prawdziwego.

Joel Mueller
źródło
Krotki są jedyną włączoną tutaj funkcją C # 7. Te są zdecydowanie w ostatecznej wersji.
Joel Mueller
Wiem o krotkach ic # 7. Mam na myśli, że nie mogę znaleźć metody WhenAll, która zwraca krotki. Jaka przestrzeń nazw / pakiet?
Yury Scherbakov
@YuryShcherbakov Task.WhenAll()nie zwraca krotki. Jeden jest konstruowany z Resultwłaściwości dostarczonych zadań po zakończeniu zadania zwróconego przez Task.WhenAll().
Chris Charabaruk,
2
Sugeruję zastąpienie .Resultpołączeń zgodnie z rozumowaniem Stephena, aby uniknąć kopiowania przykładu przez innych ludzi, którzy utrwalają złą praktykę.
julealgon
Zastanawiam się, dlaczego ta metoda nie jest częścią frameworka? Wydaje się to bardzo przydatne. Czy zabrakło im czasu i musieli zatrzymać się na jednym typie zwrotu?
Ian Grainger
14

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 awaittego:

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 asyncoperacji i tylko awaitpo 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 awaitzamiast 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/ awaiti 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/ awaitgeneruje 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:

  1. w ostatnim C # wspólny wzorzec dla asyncmetody 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);
    }
  2. 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);
    }
  3. jeśli to możliwe, wolą IsCompletedSuccessfullysię Status == TaskStatus.RanToCompletion; teraz istnieje w .NET Core dla Taski wszędzie dlaValueTask<T>

Marc Gravell
źródło
„W przeciwieństwie do różnych odpowiedzi tutaj, użycie funkcji czekania zamiast zadania. Gdy AlAll nie ma znaczenia, jak działają zadania (jednocześnie, sekwencyjnie itp.)” Nie widzę żadnej odpowiedzi, która by to powiedziała. Skomentowałbym już ich, mówiąc tyle, gdyby tak zrobili. Istnieje wiele komentarzy na wiele odpowiedzi, ale nie ma odpowiedzi. Do którego masz na myśli? Pamiętaj również, że twoja odpowiedź nie obsługuje wyniku zadań (lub radzi sobie z tym, że wszystkie wyniki są innego rodzaju). Skomponowałeś je w metodzie, która zwraca po prostu, Taskgdy wszystko jest zrobione bez użycia wyników.
Servy
@Servy masz rację, to były komentarze; Dodam drobną korektę, aby pokazać wyniki przy użyciu wyników
Marc Gravell
Dodano poprawkę @Servy
Marc Gravell
Również jeśli zamierzasz wcześnie zrezygnować z wykonywania zadań synchronicznych, równie dobrze możesz poradzić sobie z zadaniami synchronicznie anulowanymi lub błędnymi, a nie tylko tymi, które zostały pomyślnie ukończone. Jeśli podjąłeś decyzję, że jest to optymalizacja, której potrzebuje Twój program (co będzie rzadkie, ale nastąpi), równie dobrze możesz przejść całą drogę.
Servy
@Servy, który jest złożonym tematem - otrzymujesz inną semantykę wyjątku od dwóch scenariuszy - oczekiwanie na wyzwolenie wyjątku zachowuje się inaczej niż uzyskiwanie dostępu do .Result, aby wywołać wyjątek. IMO w tym momencie powinniśmy awaituzyskać „lepszą” semantykę wyjątków, przy założeniu, że wyjątki są rzadkie, ale znaczące
Marc Gravell
12

Możesz przechowywać je w zadaniach, a następnie czekać na nich wszystkich:

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

await Task.WhenAll(catTask, houseTask, carTask);

Cat cat = await catTask;
House house = await houseTask;
Car car = await carTask;
Reed Copsey
źródło
nie var catTask = FeedCat()wykonuje funkcji FeedCat()i nie przechowuje wyniku, catTaskaby await Task.WhenAll()część stała się bezużyteczna, ponieważ metoda już wykonała?
Kraang Prime
1
@ sanuel, jeśli zwrócą zadanie <t>, to nie ... zaczną otwierać asynchronię, ale nie czekaj na nią
Reed Copsey
Nie sądzę, żeby to było poprawne, proszę zobaczyć dyskusje pod odpowiedzią @ StephenCleary ... zobacz także odpowiedź Servy.
Rosdi Kasim
1
jeśli muszę dodać .ConfigrtueAwait (false). Czy dodam go tylko do Task.WhenAll lub do każdego następnego czekającego?
AstroSharp,
@AstroSharp w ogólności, dobrym pomysłem jest dodanie go do wszystkich (jeśli pierwszy zostanie ukończony, zostanie skutecznie zignorowany), ale w tym przypadku prawdopodobnie byłoby w porządku po prostu zrobić pierwsze - chyba że jest więcej asynchronizacji rzeczy dzieją się później.
Reed Copsey
6

W przypadku próby zarejestrowania wszystkich błędów upewnij się, że zachowałeś wiersz Task.WhenAll w kodzie, wiele komentarzy sugeruje, że możesz go usunąć i poczekać na poszczególne zadania. Task.WhenAll jest naprawdę ważny dla obsługi błędów. Bez tego wiersza potencjalnie pozostawiasz swój kod otwarty dla nieobserwowanych wyjątków.

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

await Task.WhenAll(catTask, houseTask, carTask);

var cat = await catTask;
var house = await houseTask;
var car = await carTask;

Wyobraź sobie FeedCat zgłasza wyjątek w następującym kodzie:

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

var cat = await catTask;
var house = await houseTask;
var car = await carTask;

W takim przypadku nigdy nie będziesz czekać na houseTask ani carTask. Istnieją tutaj 3 możliwe scenariusze:

  1. SellHouse jest już pomyślnie zakończony, gdy FeedCat nie powiódł się. W takim przypadku wszystko w porządku.

  2. SellHouse nie jest kompletny i w pewnym momencie kończy się niepowodzeniem. Wyjątek nie jest przestrzegany i zostanie ponownie wprowadzony w wątku finalizatora.

  3. SellHouse nie jest kompletny i zawiera w sobie oczekiwania. W przypadku, gdy Twój kod działa w ASP.NET SellHouse zawiedzie, gdy tylko niektóre z oczekiwań zostaną w nim ukończone. Dzieje się tak, ponieważ po prostu wywołałeś fire & zapomnij, że kontekst połączenia i synchronizacji został utracony, gdy tylko awaria FeedCat się nie powiodła.

Oto błąd, który otrzymasz w przypadku (3):

System.AggregateException: A Task's exception(s) were not observed either by Waiting on the Task or accessing its Exception property. As a result, the unobserved exception was rethrown by the finalizer thread. ---> System.NullReferenceException: Object reference not set to an instance of an object.
   at System.Web.ThreadContext.AssociateWithCurrentThread(Boolean setImpersonationContext)
   at System.Web.HttpApplication.OnThreadEnterPrivate(Boolean setImpersonationContext)
   at System.Web.HttpApplication.System.Web.Util.ISyncContext.Enter()
   at System.Web.Util.SynchronizationHelper.SafeWrapCallback(Action action)
   at System.Threading.Tasks.Task.Execute()
   --- End of inner exception stack trace ---
---> (Inner Exception #0) System.NullReferenceException: Object reference not set to an instance of an object.
   at System.Web.ThreadContext.AssociateWithCurrentThread(Boolean setImpersonationContext)
   at System.Web.HttpApplication.OnThreadEnterPrivate(Boolean setImpersonationContext)
   at System.Web.HttpApplication.System.Web.Util.ISyncContext.Enter()
   at System.Web.Util.SynchronizationHelper.SafeWrapCallback(Action action)
   at System.Threading.Tasks.Task.Execute()<---

W przypadku (2) pojawi się podobny błąd, ale z oryginalnym śledzeniem stosu wyjątku.

W przypadku platformy .NET 4.0 i nowszych można przechwytywać nieobserwowane wyjątki za pomocą TaskScheduler.UnobservedTaskException. W przypadku platformy .NET 4.5 i nowszych nieobserwowane wyjątki są domyślnie połykane, aw przypadku .NET 4.0 nieobserwowany wyjątek spowoduje awarię procesu.

Więcej informacji tutaj: Obsługa wyjątków zadań w .NET 4.5

samfromlv
źródło
2

Możesz użyć, Task.WhenAlljak wspomniano, lub Task.WaitAll, w zależności od tego, czy wątek ma czekać. Spójrz na link, aby uzyskać wyjaśnienie obu.

WaitAll vs WhenAll

christiandev
źródło
2

Użyj, Task.WhenAlla następnie poczekaj na wyniki:

var tCat = FeedCat();
var tHouse = SellHouse();
var tCar = BuyCar();
await Task.WhenAll(tCat, tHouse, tCar);
Cat cat = await tCat;
House house = await tHouse;
Tesla car = await tCar; 
//as they have all definitely finished, you could also use Task.Value.
It'sNotALie.
źródło
mm ... nie Task.Value (może kiedyś istniał w 2013 roku?), a raczej tCat.Result, tHouse.Result lub tCar.Result
Stephen York
1

Przekaż ostrzeżenie

Wystarczy szybki przegląd tych, którzy odwiedzają ten i inne podobne wątki, szukając sposobu na zrównoleglenie EntityFramework za pomocą async + czekaj + zestaw narzędzi do zadań : pokazany tutaj wzór jest solidny, jednak jeśli chodzi o specjalny płatek śniegu EF, nie będziesz wykonać równoległe wykonywanie, chyba że użyjesz osobnej (nowej) instancji kontekstu db w każdym zaangażowanym wywołaniu * Async ().

Jest to konieczne ze względu na ograniczenia projektowe kontekstów ef-db, które zabraniają jednoczesnego uruchamiania wielu zapytań w tej samej instancji kontekstu ef-db.


Wykorzystując już udzielone odpowiedzi, jest to sposób na upewnienie się, że zbierasz wszystkie wartości, nawet w przypadku, gdy jedno lub więcej zadań powoduje wyjątek:

  public async Task<string> Foobar() {
    async Task<string> Awaited(Task<Cat> a, Task<House> b, Task<Tesla> c) {
        return DoSomething(await a, await b, await c);
    }

    using (var carTask = BuyCarAsync())
    using (var catTask = FeedCatAsync())
    using (var houseTask = SellHouseAsync())
    {
        if (carTask.Status == TaskStatus.RanToCompletion //triple
            && catTask.Status == TaskStatus.RanToCompletion //cache
            && houseTask.Status == TaskStatus.RanToCompletion) { //hits
            return Task.FromResult(DoSomething(catTask.Result, carTask.Result, houseTask.Result)); //fast-track
        }

        cat = await catTask;
        car = await carTask;
        house = await houseTask;
        //or Task.AwaitAll(carTask, catTask, houseTask);
        //or await Task.WhenAll(carTask, catTask, houseTask);
        //it depends on how you like exception handling better

        return Awaited(catTask, carTask, houseTask);
   }
 }

Alternatywną implementacją, która ma mniej więcej taką samą charakterystykę wydajności, może być:

 public async Task<string> Foobar() {
    using (var carTask = BuyCarAsync())
    using (var catTask = FeedCatAsync())
    using (var houseTask = SellHouseAsync())
    {
        cat = catTask.Status == TaskStatus.RanToCompletion ? catTask.Result : (await catTask);
        car = carTask.Status == TaskStatus.RanToCompletion ? carTask.Result : (await carTask);
        house = houseTask.Status == TaskStatus.RanToCompletion ? houseTask.Result : (await houseTask);

        return DoSomething(cat, car, house);
     }
 }
XDS
źródło
-1
var dn = await Task.WhenAll<dynamic>(FeedCat(),SellHouse(),BuyCar());

jeśli chcesz uzyskać dostęp do Cat, wykonaj następujące czynności:

var ct = (Cat)dn[0];

Jest to bardzo proste do zrobienia i bardzo przydatne w użyciu, nie ma potrzeby szukania złożonego rozwiązania.

byk
źródło
1
Jest tylko jeden problem: dynamicdiabeł. Jest przeznaczony do skomplikowanej interakcji COM i tym podobnych, i nie powinien być używany w żadnej sytuacji, w której nie jest to absolutnie potrzebne. Szczególnie jeśli zależy Ci na wydajności. Lub wpisz bezpieczeństwo. Lub refaktoryzacja. Lub debugowanie.
Joel Mueller