Czy musisz ustawić Task.Run w metodzie, aby była asynchroniczna?

304

Próbuję zrozumieć, że async czeka w najprostszej formie. Chcę stworzyć bardzo prostą metodę, która doda dwie liczby na potrzeby tego przykładu, oczywiście, to wcale nie jest czas przetwarzania, to tylko kwestia sformułowania tutaj przykładu.

Przykład 1

private async Task DoWork1Async()
{
    int result = 1 + 2;
}

Przykład 2

private async Task DoWork2Async()
{
    Task.Run( () =>
    {
        int result = 1 + 2;
    });
}

Jeśli czekam DoWork1Async(), czy kod będzie działał synchronicznie czy asynchronicznie?

Czy muszę owinąć kod synchronizacji, Task.Runaby metoda była oczekiwana ORAZ asynchroniczna, aby nie blokować wątku interfejsu użytkownika?

Próbuję dowiedzieć się, czy moja metoda jest a Taskczy zwraca, Task<T>czy muszę owinąć kod, Task.Runaby był asynchroniczny.

Głupie pytanie, jestem pewien, ale widzę przykłady w sieci, gdzie ludzie czekają na kod, który nie ma nic asynchronicznego w środku i nie jest owinięty w Task.Runlub StartNew.

Neal
źródło
30
Czy Twój pierwszy fragment nie zawiera żadnych ostrzeżeń?
sickick

Odpowiedzi:

586

Najpierw wyjaśnijmy trochę terminologii: „asynchroniczny” ( async) oznacza, że ​​może odzyskać kontrolę nad wątkiem wywołującym przed jego uruchomieniem. W asyncmetodzie te punkty „ustępowania” są awaitwyrażeniami.

Jest to bardzo różni się od terminu „asynchroniczny”, ponieważ (błędnie) używany w dokumentacji MSDN od lat oznacza „wykonuje się w wątku w tle”.

Dalsze mylenie problemu asyncjest zupełnie inne niż „oczekiwanie”; istnieją asyncmetody, których typy zwracane są nieoczekiwane, i wiele metod zwracających oczekiwane typy, które nie są oczekiwane async.

Dość o tym, czym nie ; oto czym one :

  • Słowo asynckluczowe pozwala na metodę asynchroniczną (tzn. Pozwala na awaitwyrażenia). asyncmetody mogą zwrócić Task, Task<T>lub (jeśli musisz) void.
  • Każdy typ zgodny z określonym wzorcem może być oczekiwany. Najczęściej spotykanymi typami są Taski Task<T>.

Jeśli więc przeformułujemy twoje pytanie na „jak mogę uruchomić operację na wątku w tle w sposób, w jaki jest to oczekiwane”, odpowiedzią jest użycie Task.Run:

private Task<int> DoWorkAsync() // No async because the method does not need await
{
  return Task.Run(() =>
  {
    return 1 + 2;
  });
}

(Ale ten wzór jest złym podejściem; patrz poniżej).

Ale jeśli twoje pytanie brzmi: „jak utworzyć asyncmetodę, która może ustępować swojemu programowi wywołującemu zamiast blokować”, odpowiedzią jest zadeklarowanie metody asynci użycie awaitjej punktów „ustępujących”:

private async Task<int> GetWebPageHtmlSizeAsync()
{
  var client = new HttpClient();
  var html = await client.GetAsync("http://www.example.com/");
  return html.Length;
}

Zatem podstawowym wzorem rzeczy jest asynczależność kodu od „oczekiwań” w jego awaitwyrażeniach. Te „oczekiwane” mogą być innymi asyncmetodami lub zwykłymi metodami zwracającymi oczekiwane. Zwykłe metody powracający Task/ Task<T> można użyć Task.Rundo wykonania kodu w wątku tła, lub (częściej) mogą używać TaskCompletionSource<T>lub jeden z jego (skróty TaskFactory.FromAsync, Task.FromResultitp). I nie polecam owijania całą metodę in Task.Run; metody synchroniczne powinny mieć sygnatury synchroniczne i konsument powinien pozostawić pytanie, czy ma być zawinięty w Task.Run:

private int DoWork()
{
  return 1 + 2;
}

private void MoreSynchronousProcessing()
{
  // Execute it directly (synchronously), since we are also a synchronous method.
  var result = DoWork();
  ...
}

private async Task DoVariousThingsFromTheUIThreadAsync()
{
  // I have a bunch of async work to do, and I am executed on the UI thread.
  var result = await Task.Run(() => DoWork());
  ...
}

Mam async/ awaitintro na moim blogu; na końcu są dobre zasoby uzupełniające. Dokumenty MSDN dla asyncsą również niezwykle dobre.

Stephen Cleary
źródło
8
@sgnsajgon: Tak. asyncMetody muszą powrócić Task, Task<T>albo void. Taski Task<T>są oczekiwane; voidnie jest.
Stephen Cleary
3
W rzeczywistości async voidkompiluje się podpis metody, to po prostu straszny pomysł, gdy tracisz wskaźnik do zadania asynchronicznego
IEatBagels
4
@TopinFrassi: Tak, będą się kompilować, ale voidnie jest to oczekiwane.
Stephen Cleary
4
@ohadinho: Nie, mówię o tym w blogu, gdy cała metoda jest tylko wywołaniem Task.Run(jak DoWorkAsyncw tej odpowiedzi). Korzystanie Task.Runaby nazwać metodę z kontekstu UI jest odpowiednia (jak DoVariousThingsFromTheUIThreadAsync).
Stephen Cleary
2
Tak, dokładnie. Można użyć tej metody, Task.Runaby wywołać metodę, ale jeśli jest w niej Task.Runcały (lub prawie cały) kod metody, to jest to anty-wzorzec - po prostu utrzymuj synchronizację tej metody i przejdź Task.Runwyżej.
Stephen Cleary
22

Jedną z najważniejszych rzeczy do zapamiętania podczas dekorowania metody za pomocą asynchronii jest to, że przynajmniej jeden operator oczekuje w metodzie. W twoim przykładzie przetłumaczyłbym to, jak pokazano poniżej, przy użyciu TaskCompletionSource .

private Task<int> DoWorkAsync()
{
    //create a task completion source
    //the type of the result value must be the same
    //as the type in the returning Task
    TaskCompletionSource<int> tcs = new TaskCompletionSource<int>();
    Task.Run(() =>
    {
        int result = 1 + 2;
        //set the result to TaskCompletionSource
        tcs.SetResult(result);
    });
    //return the Task
    return tcs.Task;
}

private async void DoWork()
{
    int result = await DoWorkAsync();
}
Ronald Ramos
źródło
26
Dlaczego używasz TaskCompletionSource, zamiast tylko zwracać zadanie zwrócone przez metodę Task.Run () (i zmieniać jego treść, aby zwracać wynik)?
ironiczny
4
Tylko notka dodatkowa. Metoda, która ma sygnaturę „asynchroniczna nieważność”, jest ogólnie złą praktyką i jest uważana za zły kod, ponieważ może łatwo doprowadzić do impasu interfejsu użytkownika. Głównym wyjątkiem są asynchroniczne procedury obsługi zdarzeń.
Jazzeroki
12

Gdy używasz Task.Run do uruchomienia metody, Task pobiera wątek z puli wątków, aby uruchomić tę metodę. Tak więc z punktu widzenia wątku interfejsu użytkownika jest on „asynchroniczny”, ponieważ nie blokuje wątku interfejsu użytkownika. Jest to przydatne w przypadku aplikacji komputerowych, ponieważ zwykle nie potrzebujesz wielu wątków, aby zająć się interakcjami użytkownika.

Jednak w przypadku aplikacji WWW każde żądanie jest obsługiwane przez wątek puli wątków, a zatem liczbę aktywnych żądań można zwiększyć, zapisując takie wątki. Często używane wątki puli wątków do symulacji działania asynchronicznego nie są skalowalne dla aplikacji internetowych.

True Async niekoniecznie wymaga użycia wątku do operacji we / wy, takich jak dostęp do pliku / bazy danych itp. Możesz to przeczytać, aby zrozumieć, dlaczego operacja we / wy nie potrzebuje wątków. http://blog.stephencleary.com/2013/11/there-is-no-thread.html

W prostym przykładzie jest to wyłącznie obliczenie związane z procesorem, więc użycie Task.Run jest w porządku.

zheng yu
źródło
Więc jeśli muszę korzystać z synchronicznego zewnętrznego interfejsu API w kontrolerze interfejsu WWW, NIE powinienem zawijać synchronicznego wywołania w Task.Run ()? Jak już powiedziałeś, to spowoduje, że początkowy wątek żądania zostanie odblokowany, ale używa innego wątku puli do wywołania zewnętrznego interfejsu API. W rzeczywistości uważam, że to nadal dobry pomysł, ponieważ w ten sposób teoretycznie można użyć dwóch wątków puli do przetworzenia wielu żądań, np. Jeden wątek może przetworzyć wiele żądań przychodzących, a drugi może wywołać zewnętrzny interfejs API dla wszystkich tych żądań?
stt106
Zgadzam się. Nie twierdzę, że nie powinieneś absolutnie owijać wszystkich wywołań synchronicznych w Task.Run (). Po prostu wskazuję potencjalny problem.
zheng yu
1
@ stt106 I should NOT wrap the synchronous call in Task.Run()to prawda. Jeśli to zrobisz, po prostu zamieniasz wątki. tzn. odblokowujesz początkowy wątek żądania, ale pobierasz inny wątek z puli wątków, który mógł zostać wykorzystany do przetworzenia innego żądania. Jedynym rezultatem jest narzut przełączania kontekstu, gdy połączenie jest zakończone dla absolutnie zerowego wzmocnienia
Saeb Amini