Przykład async / await, który powoduje zakleszczenie

98

Natknąłem się na kilka najlepszych praktyk dotyczących programowania asynchronicznego przy użyciu słów kluczowych async/ awaitsłów kluczowych języka C # (jestem nowy w C # 5.0).

Jedna z udzielonych rad była następująca:

Stabilność: poznaj konteksty synchronizacji

... Niektóre konteksty synchronizacji są niewłączane i jednowątkowe. Oznacza to, że w danym czasie można wykonać tylko jedną jednostkę pracy w kontekście. Przykładem tego jest wątek interfejsu użytkownika systemu Windows lub kontekst żądania ASP.NET. W tych jednowątkowych kontekstach synchronizacji łatwo jest się zablokować. Jeśli odrodzisz zadanie z kontekstu jednowątkowego, a następnie zaczekasz na to zadanie w kontekście, Twój oczekujący kod może blokować zadanie w tle.

public ActionResult ActionAsync()
{
    // DEADLOCK: this blocks on the async task
    var data = GetDataAsync().Result;

    return View(data);
}

private async Task<string> GetDataAsync()
{
    // a very simple async method
    var result = await MyWebService.GetDataAsync();
    return result.ToString();
}

Jeśli spróbuję sam go przeanalizować, główny wątek odrodzi się do nowego MyWebService.GetDataAsync();, ale ponieważ główny wątek tam czeka, czeka na wynik w GetDataAsync().Result. W międzyczasie powiedzmy, że dane są gotowe. Dlaczego główny wątek nie kontynuuje swojej logiki kontynuacji i zwraca wynik w postaci ciągu GetDataAsync()?

Czy ktoś może mi wyjaśnić, dlaczego w powyższym przykładzie jest impas? Nie mam pojęcia, na czym polega problem ...

Dror Weiss
źródło
Czy na pewno jesteś pewien, że GetDataAsync kończy to? A może utknie, powodując po prostu blokadę, a nie impas?
Andrey
Oto przykład, który został podany. Mojego rozeznania powinien zakończyć ją za rzeczy i mają wynik jakiś gotowy ...
Dror Weiss
4
Dlaczego w ogóle czekasz na zadanie? Zamiast tego powinieneś czekać, ponieważ w zasadzie straciłeś wszystkie zalety modelu asynchronicznego.
Toni Petrina
Aby dodać do punktu @ ToniPetrina, nawet bez problemu zakleszczenia, var data = GetDataAsync().Result;jest wiersz kodu, który nigdy nie powinien być wykonywany w kontekście, którego nie należy blokować (żądanie UI lub ASP.NET). Nawet jeśli nie ma zakleszczenia, blokuje wątek przez nieokreślony czas. Więc zasadniczo jest to okropny przykład. [Musisz wyjść z wątku UI przed wykonaniem takiego kodu lub użyć go awaitrównież, jak sugeruje Toni.]
ToolmakerSteve

Odpowiedzi:

82

Spójrz na ten przykład , Stephen ma dla ciebie jasną odpowiedź:

A więc tak się dzieje, zaczynając od metody najwyższego poziomu ( Button1_Clickdla UI / MyController.Getdla ASP.NET):

  1. Wywołania metod najwyższego poziomu GetJsonAsync(w kontekście UI / ASP.NET).

  2. GetJsonAsyncuruchamia żądanie REST przez wywołanie HttpClient.GetStringAsync(nadal w kontekście).

  3. GetStringAsynczwraca nieukończone Task, wskazując, że żądanie REST nie zostało ukończone.

  4. GetJsonAsyncczeka na Taskzwrócony przez GetStringAsync. Kontekst jest przechwytywany i będzie używany do dalszego uruchamiania GetJsonAsyncmetody później. GetJsonAsynczwraca nieukończone Task, wskazując, że GetJsonAsyncmetoda nie jest kompletna.

  5. Metoda najwyższego poziomu synchronicznie blokuje Taskzwracany przez GetJsonAsync. To blokuje wątek kontekstu.

  6. ... Ostatecznie żądanie REST zostanie zakończone. To kończy pracę Taskzwróconą przez GetStringAsync.

  7. Kontynuacja dla GetJsonAsyncjest teraz gotowa do uruchomienia i czeka, aż kontekst będzie dostępny, aby mógł zostać wykonany w kontekście.

  8. Impas . Metoda najwyższego poziomu blokuje wątek kontekstu, czeka na GetJsonAsynczakończenie i GetJsonAsyncczeka, aż kontekst będzie wolny, aby mógł się zakończyć. W przypadku przykładu interfejsu użytkownika „kontekst” to kontekst interfejsu użytkownika; w przykładzie ASP.NET „kontekst” to kontekst żądania ASP.NET. Ten typ zakleszczenia może zostać spowodowany przez „kontekst”.

Kolejny link, który powinieneś przeczytać: Oczekiwanie, interfejs użytkownika i zakleszczenia! O mój!

cuongle
źródło
23
  • Fakt 1: GetDataAsync().Result;uruchomi się po zakończeniu zadania zwróconego przez GetDataAsync(), w międzyczasie blokuje wątek interfejsu użytkownika
  • Fakt 2: kontynuacja await ( return result.ToString()) jest umieszczana w kolejce do wątku interfejsu użytkownika w celu wykonania
  • Fakt 3: Zadanie zwrócone przez GetDataAsync()zakończy się, gdy zostanie uruchomiona jego kolejka do kontynuacji
  • Fakt 4: Kontynuacja w kolejce nigdy nie jest uruchamiana, ponieważ wątek interfejsu użytkownika jest zablokowany (fakt 1)

Impas!

Impas można przełamać zapewnionymi alternatywami, aby uniknąć faktu 1 lub faktu 2.

  • Unikaj 1,4. Zamiast blokować wątek interfejsu użytkownika, użyj var data = await GetDataAsync(), co umożliwia kontynuowanie działania wątku interfejsu użytkownika
  • Unikaj 2,3. Ustaw w kolejce kontynuację oczekiwania do innego wątku, który nie jest zablokowany, np. var data = Task.Run(GetDataAsync).ResultUse, co spowoduje wysłanie kontynuacji do kontekstu synchronizacji wątku puli wątków. Pozwala to GetDataAsync()na ukończenie zadania zwróconego przez .

Jest to bardzo dobrze wyjaśnione w artykule Stephena Touba , mniej więcej w połowie, gdzie posługuje się przykładem DelayAsync().

Phillip Ngan
źródło
Odnośnie, var data = Task.Run(GetDataAsync).Resultto dla mnie nowość. Zawsze myślałem, że zewnętrzna strona .Resultbędzie łatwo dostępna, gdy tylko GetDataAsynczostanie trafiony pierwszy element , więc datazawsze będzie default. Ciekawy.
nawfal
19

Właśnie ponownie bawiłem się tym problemem w projekcie ASP.NET MVC. Jeśli chcesz wywołać asyncmetody z a PartialView, nie możesz utworzyć PartialView async. Jeśli to zrobisz, dostaniesz wyjątek.

Możesz użyć następującego prostego obejścia w scenariuszu, w którym chcesz wywołać asyncmetodę z metody synchronizacji:

  1. Przed połączeniem wyczyść SynchronizationContext
  2. Zadzwoń, tu nie będzie już impasu, poczekaj, aż się skończy
  3. Przywróć plik SynchronizationContext

Przykład:

public ActionResult DisplayUserInfo(string userName)
{
    // trick to prevent deadlocks of calling async method 
    // and waiting for on a sync UI thread.
    var syncContext = SynchronizationContext.Current;
    SynchronizationContext.SetSynchronizationContext(null);

    //  this is the async call, wait for the result (!)
    var model = _asyncService.GetUserInfo(Username).Result;

    // restore the context
    SynchronizationContext.SetSynchronizationContext(syncContext);

    return PartialView("_UserInfo", model);
}
Herre Kuijpers
źródło
3

Inną ważną kwestią jest to, że nie należy blokować zadań i używać funkcji asynchronicznej do końca, aby zapobiec zakleszczeniom. Wtedy będzie to blokowanie asynchroniczne, a nie synchroniczne.

public async Task<ActionResult> ActionAsync()
{

    var data = await GetDataAsync();

    return View(data);
}

private async Task<string> GetDataAsync()
{
    // a very simple async method
    var result = await MyWebService.GetDataAsync();
    return result.ToString();
}
marvelTracker
źródło
6
Co się stanie, jeśli chcę, aby wątek główny (UI) był blokowany do czasu zakończenia zadania? Czy na przykład w aplikacji konsoli? Powiedzmy, że chcę używać HttpClient, który obsługuje tylko asynchroniczne ... Jak używać go synchronicznie bez ryzyka zakleszczenia ? To musi być możliwe. Jeśli WebClient może być używany w ten sposób (ze względu na metody synchronizacji) i działa doskonale, to dlaczego nie można tego zrobić również z HttpClient?
Dexter
Zobacz odpowiedź Philipa Ngana powyżej (wiem, że została ona opublikowana po tym komentarzu): Ustaw kolejkę kontynuacji oczekiwania do innego wątku, który nie jest zablokowany, np. Użyj var data = Task.Run (GetDataAsync). Wynik
Jeroen
@Dexter - re „ Co jeśli chcę, aby wątek główny (UI) był blokowany do czasu zakończenia zadania? ” - czy naprawdę chcesz, aby wątek interfejsu użytkownika był zablokowany, co oznacza, że ​​użytkownik nie może nic zrobić, nie może nawet anulować - lub jest to, że nie chcesz kontynuować metody, w której jesteś? „await” lub „Task.ContinueWith” obsługuje ten drugi przypadek.
ToolmakerSteve
@ToolmakerSteve oczywiście nie chcę kontynuować tej metody. Ale po prostu nie mogę użyć await, ponieważ nie mogę też używać async do końca - HttpClient jest wywoływany w main , który oczywiście nie może być asynchroniczny. A potem wspomniałem robi wszystko to w aplikacji konsoli - w tym przypadku chcę dokładnie były - nie chcę mojej aplikacji do nawet być wielowątkowe. Zablokuj wszystko .
Dexter
-1

Obejście, do którego doszedłem, polega na użyciu Joinmetody rozszerzającej w zadaniu przed zapytaniem o wynik.

Kod wygląda następująco:

public ActionResult ActionAsync()
{
  var task = GetDataAsync();
  task.Join();
  var data = task.Result;

  return View(data);
}

Gdzie metoda łączenia to:

public static class TaskExtensions
{
    public static void Join(this Task task)
    {
        var currentDispatcher = Dispatcher.CurrentDispatcher;
        while (!task.IsCompleted)
        {
            // Make the dispatcher allow this thread to work on other things
            currentDispatcher.Invoke(delegate { }, DispatcherPriority.SystemIdle);
        }
    }
}

Nie jestem na tyle w domenie, aby zobaczyć wady tego rozwiązania (jeśli występują)

Orace
źródło