Uruchamianie wielu zadań asynchronicznych i oczekiwanie na ich zakończenie

265

Muszę uruchomić wiele zadań asynchronicznych w aplikacji konsoli i poczekać na zakończenie wszystkich zadań przed dalszym przetwarzaniem.

Istnieje wiele artykułów, ale im bardziej czytam, tym bardziej się mylę. Przeczytałem i rozumiem podstawowe zasady biblioteki Zadań, ale najwyraźniej gdzieś brakuje linku.

Rozumiem, że możliwe jest łączenie zadań w taki sposób, aby zaczynały się po ukończeniu kolejnego (co jest w zasadzie scenariuszem dla wszystkich artykułów, które przeczytałem), ale chcę, aby wszystkie moje zadania działały jednocześnie i chcę to wiedzieć raz wszystkie są ukończone.

Jaka jest najprostsza implementacja dla takiego scenariusza?

Daniel Minnaar
źródło

Odpowiedzi:

440

Obie odpowiedzi nie wspomniały o oczekiwanym Task.WhenAll:

var task1 = DoWorkAsync();
var task2 = DoMoreWorkAsync();

await Task.WhenAll(task1, task2);

Główna różnica między Task.WaitAlli Task.WhenAllpolega na tym, że pierwsze blokuje (podobnie jak Waitw przypadku pojedynczego zadania), podczas gdy drugie nie może i może być oczekiwane, dając kontrolę nad dzwoniącym, aż wszystkie zadania zakończą się.

Co więcej, obsługa wyjątków różni się:

Task.WaitAll:

Przynajmniej jedna z instancji Zadania została anulowana lub zgłoszony został wyjątek podczas wykonywania co najmniej jednej z instancji Zadania. Jeśli zadanie zostało anulowane, AggregateException zawiera OperationCanceledException w kolekcji InnerExceptions.

Task.WhenAll:

Jeśli którekolwiek z dostarczonych zadań zostaną zakończone w stanie błędu, zwrócone zadanie zostanie również ukończone w stanie Usterka, w którym jego wyjątki będą zawierać agregację zestawu nieopakowanych wyjątków z każdego z dostarczonych zadań.

Jeśli żadne z dostarczonych zadań nie spowodowało błędu, ale co najmniej jedno z nich zostało anulowane, zwrócone zadanie zakończy się w stanie Anulowane.

Jeśli żadne z zadań nie uległo awarii i żadne z zadań nie zostało anulowane, wynikowe zadanie zakończy się w stanie RanToCompletion. Jeśli podana tablica / wyliczenie nie zawiera żadnych zadań, zwrócone zadanie natychmiast przejdzie w stan RanToCompletion, zanim zostanie zwrócone do programu wywołującego.

Yuval Itzchakov
źródło
4
Kiedy próbuję, moje zadania działają sekwencyjnie? Czy każde zadanie trzeba wcześniej rozpocząć indywidualnie await Task.WhenAll(task1, task2);?
Zapnologica
4
@Zapnologica Task.WhenAllnie uruchamia zadań dla Ciebie. Musisz podać je jako „gorące”, co oznacza, że ​​już się rozpoczęło.
Yuval Itzchakov
2
Dobrze. To ma sens. Więc co zrobi twój przykład? Ponieważ ich nie uruchomiłeś?
Zapnologica
2
@YuvalItzchakov bardzo dziękuję! To takie proste, ale dzisiaj bardzo mi pomogło! Jest wart co najmniej +1000 :)
Daniel Dušek
1
@Pierre Nie obserwuję. Co StartNewi spinning nowych zadań ma wspólnego z asynchronicznym czekaniem na nich wszystkich?
Yuval Itzchakov
106

Możesz utworzyć wiele zadań, takich jak:

List<Task> TaskList = new List<Task>();
foreach(...)
{
   var LastTask = new Task(SomeFunction);
   LastTask.Start();
   TaskList.Add(LastTask);
}

Task.WaitAll(TaskList.ToArray());
Wirus
źródło
48
Polecam WhenAll
Ravi
Czy można uruchomić wiele nowych wątków jednocześnie, używając słowa kluczowego Oczekiwanie zamiast .Start ()?
Matt W
1
@MattW Nie, gdy użyjesz funkcji czekaj, będzie czekać na jej zakończenie. W takim przypadku nie można utworzyć środowiska wielowątkowego. To jest powód, dla którego wszystkie zadania są czekane na końcu pętli.
Wirus
5
Głosuj za przyszłymi czytelnikami, ponieważ nie jest jasne, że jest to połączenie blokujące.
JRoughan
Zobacz zaakceptowaną odpowiedź z powodów, dla których nie możesz tego zrobić.
EL MOJO,
26

Najlepszą opcją, jaką widziałem, jest następująca metoda rozszerzenia:

public static Task ForEachAsync<T>(this IEnumerable<T> sequence, Func<T, Task> action) {
    return Task.WhenAll(sequence.Select(action));
}

Nazwij to tak:

await sequence.ForEachAsync(item => item.SomethingAsync(blah));

Lub z asynchroniczną lambda:

await sequence.ForEachAsync(async item => {
    var more = await GetMoreAsync(item);
    await more.FrobbleAsync();
});
me22
źródło
26

Możesz użyć, WhenAllktóry zwróci oczekiwany Tasklub WaitAllktóry nie ma typu zwrotu i zablokuje dalsze wykonywanie kodu podobnie do Thread.Sleepmomentu, aż wszystkie zadania zostaną zakończone, anulowane lub zakłócone.

wprowadź opis zdjęcia tutaj

Przykład

var tasks = new Task[] {
    TaskOperationOne(),
    TaskOperationTwo()
};

Task.WaitAll(tasks);
// or
await Task.WhenAll(tasks);

Jeśli chcesz uruchomić zadania w praktycznej kolejności, możesz uzyskać inspirację z tego odpowiedzi .

NtFreX
źródło
przepraszam za spóźnienie na imprezę, ale dlaczego masz awaitza każdą operację i jednocześnie użyj WaitAlllub WhenAll. Czy zadania Task[]inicjujące nie powinny być bez await?
dee zg
@dee zg Masz rację. Oczekiwanie powyżej pokonuje cel. Zmienię swoją odpowiedź i usunę je.
NtFreX
tak, to jest to. dzięki za wyjaśnienie! (głosowanie za miłą odpowiedzią)
dee zg
8

Czy chcesz Taskpołączyć łańcuchy , czy można je wywoływać równolegle?

Do łączenia
Po prostu zrób coś takiego

Task.Run(...).ContinueWith(...).ContinueWith(...).ContinueWith(...);
Task.Factory.StartNew(...).ContinueWith(...).ContinueWith(...).ContinueWith(...);

i nie zapomnij sprawdzić poprzedniej Taskinstancji w każdej z nich, ContinueWithponieważ może to być wina.

Dla równoległego sposobu
Najprostsza metoda, na którą natknąłem się: w Parallel.Invoke przeciwnym razie Task.WaitAllmożesz użyć WaitHandles do odliczania do zera akcji (poczekaj, jest nowa klasa:) CountdownEventlub ...

Andreas Niedermair
źródło
3
Doceń odpowiedź, ale twoje sugestie można by wyjaśnić nieco więcej.
Daniel Minnaar
@drminnaar, jakie inne wyjaśnienie obok linków do msdn z przykładami potrzebujesz? nawet nie kliknąłeś linków, prawda?
Andreas Niedermair
4
Kliknąłem linki i przeczytałem treść. Miałem ochotę na Invoke, ale było wiele If i But chodziło o to, czy działa asynchronicznie, czy nie. Ciągle edytujesz swoją odpowiedź. Link WaitAll, który opublikowałeś był doskonały, ale wybrałem odpowiedź, która wykazała tę samą funkcjonalność w szybszy i łatwiejszy do odczytania sposób. Nie obrażaj się, twoja odpowiedź wciąż stanowi dobrą alternatywę dla innych podejść.
Daniel Minnaar
@drminnaar bez obrazy wzięty tutaj, jestem po prostu ciekawy :)
Andreas Niedermair
5

Tak to robię za pomocą tablicy Func <> :

var tasks = new Func<Task>[]
{
   () => myAsyncWork1(),
   () => myAsyncWork2(),
   () => myAsyncWork3()
};

await Task.WhenAll(tasks.Select(task => task()).ToArray()); //Async    
Task.WaitAll(tasks.Select(task => task()).ToArray()); //Or use WaitAll for Sync
DalSoft
źródło
1
Dlaczego nie zachowasz go jako Tablicy zadań?
Talha Talip Açıkgöz
1
Jeśli nie jesteś ostrożny @ talha-talip-açıkgöz, wykonaj zadania, których się nie spodziewałeś. Robiąc to jako delegat Func, jasno określa twoje zamiary.
DalSoft,
5

Jeszcze jedna odpowiedź ... ale zwykle znajduję się w przypadku, gdy muszę jednocześnie ładować dane i umieszczać je w zmiennych, takich jak:

var cats = new List<Cat>();
var dog = new Dog();

var loadDataTasks = new Task[]
{
    Task.Run(async () => cats = await LoadCatsAsync()),
    Task.Run(async () => dog = await LoadDogAsync())
};

try
{
    await Task.WhenAll(loadDataTasks);
}
catch (Exception ex)
{
    // handle exception
}
Yehor Hromadskyi
źródło
1
Jeśli LoadCatsAsync()i LoadDogAsync()to tylko połączenia z bazą danych, są one powiązane z operacjami we / wy. Task.Run()jest przeznaczony do pracy z procesorem; dodaje dodatkowy niepotrzebny narzut, jeśli wszystko, co robisz, to czeka na odpowiedź z serwera bazy danych. Akceptowana odpowiedź Yuvala to właściwy sposób na pracę związaną z IO.
Stephen Kennedy,
@StephenKennedy, czy możesz wyjaśnić, jaki rodzaj kosztów ogólnych i jak bardzo może to wpłynąć na wydajność? Dzięki!
Yehor Hromadskyi,
Trudno byłoby to podsumować w polu komentarzy :) Zamiast tego polecam przeczytanie artykułów Stephena Cleary'ego - on jest ekspertem w tej dziedzinie. Zacznij tutaj: blog.stephencleary.com/2013/10/…
Stephen Kennedy
-1

Przygotowałem kawałek kodu, aby pokazać, jak korzystać z zadania w niektórych z tych scenariuszy.

    // method to run tasks in a parallel 
    public async Task RunMultipleTaskParallel(Task[] tasks) {

        await Task.WhenAll(tasks);
    }
    // methode to run task one by one 
    public async Task RunMultipleTaskOneByOne(Task[] tasks)
    {
        for (int i = 0; i < tasks.Length - 1; i++)
            await tasks[i];
    }
    // method to run i task in parallel 
    public async Task RunMultipleTaskParallel(Task[] tasks, int i)
    {
        var countTask = tasks.Length;
        var remainTasks = 0;
        do
        {
            int toTake = (countTask < i) ? countTask : i;
            var limitedTasks = tasks.Skip(remainTasks)
                                    .Take(toTake);
            remainTasks += toTake;
            await RunMultipleTaskParallel(limitedTasks.ToArray());
        } while (remainTasks < countTask);
    }
Sayah Imad
źródło
1
jak uzyskać wyniki zadań? Na przykład, aby scalić „wiersze” (z N zadań równolegle) w danych i powiązać je z gridview asp.net?
PreguntonCojoneroCabrón