Jaka jest różnica między Task.Start / Wait a Async / Await?

206

Być może czegoś mi brakuje, ale jaka jest różnica między robieniem:

public void MyMethod()
{
  Task t = Task.Factory.StartNew(DoSomethingThatTakesTime);
  t.Wait();
  UpdateLabelToSayItsComplete();
}

public async void MyMethod()
{
  var result = Task.Factory.StartNew(DoSomethingThatTakesTime);
  await result;
  UpdateLabelToSayItsComplete();
}

private void DoSomethingThatTakesTime()
{
  Thread.Sleep(10000);
}
Jon
źródło

Odpowiedzi:

395

Być może czegoś mi brakuje

Ty jesteś.

Jaka jest różnica między robieniem Task.Waita await task?

Lunch zamawiasz u kelnera w restauracji. Chwilę po złożeniu zamówienia wchodzi znajomy, siada obok ciebie i rozpoczyna rozmowę. Teraz masz dwie możliwości. Możesz zignorować przyjaciela, dopóki zadanie nie zostanie zakończone - możesz poczekać, aż pojawi się zupa i nie robić nic innego, czekając. Lub możesz odpowiedzieć swojemu przyjacielowi, a kiedy przyjaciel przestanie mówić, kelner przyniesie ci zupę.

Task.Waitblokuje do momentu ukończenia zadania - ignorujesz znajomego, dopóki zadanie nie zostanie ukończone. awaitprzetwarza wiadomości w kolejce komunikatów, a po zakończeniu zadania kolejkuje komunikat z informacją „podnieś tam, gdzie przerwałeś, po tym, aż poczeka”. Rozmawiasz z przyjacielem, a gdy dochodzi do przerwy w rozmowie, przybywa zupa.

Eric Lippert
źródło
5
@ronag Nie, to nie jest. Jak byś chciał, gdyby oczekiwanie na Task10 ms faktycznie wykonało 10-godzinny Taskwątek, blokując w ten sposób całe 10 godzin?
svick,
62
@StrugglingCoder: The Oczekujcie operator nie robić nic oprócz oceny jego argument, a następnie natychmiast powrócić zadanie bieżącego rozmówcy . Ludzie mają w głowie ten pomysł, że asynchronię można osiągnąć jedynie poprzez odciążenie wątków, ale to nieprawda. Możesz ugotować śniadanie i poczytać gazetę, gdy toster jest w tosterze, nie zatrudniając kucharza, który ogląda toster. Ludzie mówią, że dobrze, wewnątrz tostera musi być ukryty wątek - robotnik, ale zapewniam cię, że jeśli spojrzysz na toster, nie będzie tam małego faceta obserwującego toast.
Eric Lippert,
11
@StrugglingCoder: Więc kto wykonuje pracę, o którą prosisz? Być może inny wątek wykonuje pracę i ten wątek został przypisany do procesora, więc praca jest faktycznie wykonywana. Być może praca jest wykonywana przez sprzęt i nie ma żadnego wątku. Ale z pewnością mówisz, że w sprzęcie musi być jakiś wątek . Nie. Sprzęt istnieje poniżej poziomu wątków. Nie musi być wątku! Możesz przeczytać artykuł Stephena Cleary'ego Nie ma nic.
Eric Lippert,
6
@StrugglingCoder: Pytanie, załóżmy, że jest wykonywana praca asynchroniczna, nie ma sprzętu i nie ma innego wątku. Jak to jest możliwe? Przypuśćmy, że rzecz, na którą czekałeś, ustawia w kolejce serię komunikatów systemu Windows , z których każda działa trochę? Co się teraz stanie? Przywracasz kontrolę w pętli komunikatów, zaczyna ona wyciągać wiadomości z kolejki, za każdym razem wykonując odrobinę pracy, a ostatnim wykonanym zadaniem jest „kontynuacja zadania”. Bez dodatkowego wątku!
Eric Lippert,
8
@StrugglingCoder: Pomyśl o tym, co właśnie powiedziałem. Wiesz już, że tak działa system Windows . Wykonujesz serię ruchów myszy i kliknięć przycisków i tak dalej. Wiadomości są kolejkowane, przetwarzane kolejno, każda powoduje niewielką ilość pracy, a kiedy wszystko jest zrobione, system nadal działa. Asynchronizacja w jednym wątku jest niczym więcej niż tym, do czego już jesteś przyzwyczajony: dzielenie dużych zadań na małe kawałki, ustawianie ich w kolejce i wykonywanie wszystkich małych bitów w określonej kolejności. Niektóre z tych egzekucji powodują kolejkowanie innych zadań i życie toczy się dalej. Jeden wątek!
Eric Lippert,
121

Aby zademonstrować odpowiedź Erica, oto kod:

public void ButtonClick(object sender, EventArgs e)
{
  Task t = new Task.Factory.StartNew(DoSomethingThatTakesTime);
  t.Wait();  
  //If you press Button2 now you won't see anything in the console 
  //until this task is complete and then the label will be updated!
  UpdateLabelToSayItsComplete();
}

public async void ButtonClick(object sender, EventArgs e)
{
  var result = Task.Factory.StartNew(DoSomethingThatTakesTime);
  await result;
  //If you press Button2 now you will see stuff in the console and 
  //when the long method returns it will update the label!
  UpdateLabelToSayItsComplete();
}

public void Button_2_Click(object sender, EventArgs e)
{
  Console.WriteLine("Button 2 Clicked");
}

private void DoSomethingThatTakesTime()
{
  Thread.Sleep(10000);
}
Jon
źródło
27
+1 za kod (lepiej uruchomić raz, niż czytać sto razy). Ale wyrażenie „ //If you press Button2 now you won't see anything in the console until this task is complete and then the label will be updated!” wprowadza w błąd. Po naciśnięciu przycisku t.Wait();w module obsługi zdarzeń kliknięcia przycisku ButtonClick()nie można nic nacisnąć, a następnie wyświetlić czegoś w konsoli i aktualizacji etykiety „do czasu zakończenia tego zadania”, ponieważ GUI jest zawieszone i nie odpowiada, to znaczy kliknięcia lub interakcje z GUI są LOST aż do zakończenia zadania oczekiwania
Giennadij Vanin Геннадий Ванин
2
Myślę, że Eric zakłada, że ​​masz podstawową wiedzę na temat interfejsu API Zadania. Patrzę na ten kod i mówię sobie: „tak, blokuję t.Waitgłówny wątek, dopóki zadanie nie zostanie zakończone”.
The Muffin Man
50

Ten przykład bardzo wyraźnie pokazuje różnicę. Przy async / oczekuj wątek wywołujący nie będzie blokował i kontynuował wykonywanie.

static void Main(string[] args)
{
    WriteOutput("Program Begin");
    // DoAsTask();
    DoAsAsync();
    WriteOutput("Program End");
    Console.ReadLine();
}

static void DoAsTask()
{
    WriteOutput("1 - Starting");
    var t = Task.Factory.StartNew<int>(DoSomethingThatTakesTime);
    WriteOutput("2 - Task started");
    t.Wait();
    WriteOutput("3 - Task completed with result: " + t.Result);
}

static async Task DoAsAsync()
{
    WriteOutput("1 - Starting");
    var t = Task.Factory.StartNew<int>(DoSomethingThatTakesTime);
    WriteOutput("2 - Task started");
    var result = await t;
    WriteOutput("3 - Task completed with result: " + result);
}

static int DoSomethingThatTakesTime()
{
    WriteOutput("A - Started something");
    Thread.Sleep(1000);
    WriteOutput("B - Completed something");
    return 123;
}

static void WriteOutput(string message)
{
    Console.WriteLine("[{0}] {1}", Thread.CurrentThread.ManagedThreadId, message);
}

Wyjście DoAsTask:

[1] Rozpocznij program
[1] 1 - Uruchamianie
[1] 2 - Zadanie rozpoczęte
[3] A - Coś zaczął
[3] B - Coś ukończonego
[1] 3 - Zadanie zakończone wynikiem: 123
[1] Koniec programu

Wyjście DoAsAsync:

[1] Rozpocznij program
[1] 1 - Uruchamianie
[1] 2 - Zadanie rozpoczęte
[3] A - Coś zaczął
[1] Koniec programu
[3] B - Coś ukończonego
[3] 3 - Zadanie zakończone wynikiem: 123

Aktualizacja: ulepszony przykład pokazujący identyfikator wątku na wyjściu.

Mas
źródło
4
Ale jeśli zrobię: nowe zadanie (DoAsTask) .Start (); zamiast DoAsAsync (); dostaję taką samą funkcjonalność, więc gdzie jest korzyść z oczekiwania…
omriman12
1
Z twoją propozycją wynik zadania należy ocenić gdzie indziej, może inną metodą lub lambda. Oczekiwanie na asynchronię ułatwia śledzenie kodu asynchronicznego. To tylko wzmacniacz składni.
Mas
@Mas nie rozumiem, dlaczego Program kończy się po A - coś zaczął. Z mojego zrozumienia, jeśli chodzi o oczekiwanie, proces słowa kluczowego powinien natychmiast przejść do głównego kontekstu, a następnie wrócić.
@JimmyJimm Z mojego zrozumienia Task.Factory.StartNew wypali nowy wątek, aby uruchomić DoSomethingThatTakesTime. W związku z tym nie ma gwarancji, że program zostanie zakończony lub program A - Started Something zostanie wykonany jako pierwszy.
RiaanDP
@JimmyJimm: Zaktualizowałem próbkę, aby wyświetlić identyfikatory wątków. Jak widać, „Koniec programu” i „A - coś zaczęło” działają w różnych wątkach. Tak więc kolejność nie jest deterministyczna.
Mas
10

Wait (), spowoduje uruchomienie potencjalnie asynchronicznego kodu w sposób zsynchronizowany. czekam nie będzie.

Na przykład masz aplikację internetową asp.net. Użytkownik A wywołuje / punkt końcowy getUser / 1. Pula aplikacji asp.net wybierze wątek z puli wątków (Thread1) i ten wątek wykona połączenie http. Jeśli wykonasz funkcję Wait (), ten wątek zostanie zablokowany do momentu rozwiązania połączenia HTTP. Podczas oczekiwania, jeśli UserB wywoła / getUser / 2, pula aplikacji będzie musiała obsługiwać inny wątek (Thread2), aby ponownie nawiązać połączenie HTTP. Właśnie utworzyłeś (cóż, właściwie pobrany z puli aplikacji) inny wątek bez powodu, ponieważ nie możesz użyć Thread1, został on zablokowany przez Wait ().

Jeśli użyjesz funkcji Oczekiwanie na wątku 1, SyncContext zarządza synchronizacją między wątkiem 1 a połączeniem http. Po prostu powiadomi o zakończeniu połączenia HTTP. W międzyczasie, jeśli UserB wywołuje / getUser / 2, wtedy użyjesz Thread1 ponownie, aby wykonać połączenie HTTP, ponieważ zostało ono zwolnione, gdy czeka na trafienie. Wtedy kolejne żądanie może z niego skorzystać, jeszcze więcej. Po zakończeniu połączenia HTTP (użytkownik 1 lub użytkownik 2) Wątek 1 może uzyskać wynik i powrócić do dzwoniącego (klienta). Wątek 1 był używany do wielu zadań.

Teoman Shipahi
źródło
9

W tym przykładzie praktycznie niewiele. Jeśli czekasz na zadanie, które powraca w innym wątku (np. Wywołanie WCF) lub zrzeka się kontroli nad systemem operacyjnym (takim jak File IO), czekanie zużyje mniej zasobów systemowych, nie blokując wątku.

foson
źródło
3

W powyższym przykładzie możesz użyć „TaskCreationOptions.HideScheduler” i znacznie zmodyfikować metodę „DoAsTask”. Sama metoda nie jest asynchroniczna, tak jak dzieje się to w przypadku „DoAsAsync”, ponieważ zwraca wartość „Zadanie” i jest oznaczona jako „asynchroniczna”, tworząc kilka kombinacji. W ten sposób daje mi dokładnie to samo, co użycie „asynchronicznie / oczekuj” :

static Task DoAsTask()
{
    WriteOutput("1 - Starting");
    var t = Task.Factory.StartNew<int>(DoSomethingThatTakesTime, TaskCreationOptions.HideScheduler); //<-- HideScheduler do the magic

    TaskCompletionSource<int> tsc = new TaskCompletionSource<int>();
    t.ContinueWith(tsk => tsc.TrySetResult(tsk.Result)); //<-- Set the result to the created Task

    WriteOutput("2 - Task started");

    tsc.Task.ContinueWith(tsk => WriteOutput("3 - Task completed with result: " + tsk.Result)); //<-- Complete the Task
    return tsc.Task;
}
użytkownik8545699
źródło