czekają vs Task.Wait - Deadlock?

195

Nie do końca rozumiem różnicę między Task.Waiti await.

Mam coś podobnego do następujących funkcji w usłudze WebAPI ASP.NET:

public class TestController : ApiController
{
    public static async Task<string> Foo()
    {
        await Task.Delay(1).ConfigureAwait(false);
        return "";
    }

    public async static Task<string> Bar()
    {
        return await Foo();
    }

    public async static Task<string> Ros()
    {
        return await Bar();
    }

    // GET api/test
    public IEnumerable<string> Get()
    {
        Task.WaitAll(Enumerable.Range(0, 10).Select(x => Ros()).ToArray());

        return new string[] { "value1", "value2" }; // This will never execute
    }
}

Gdzie Getbędzie impas.

Co może to powodować? Dlaczego nie powoduje to problemów, gdy używam oczekiwania blokującego zamiast await Task.Delay?

ronag
źródło
@Servy: Wrócę z repozytorium, jak tylko będę miał czas. Na razie działa z Task.Delay(1).Wait()co jest wystarczająco dobry.
ronag
2
Task.Delay(1).Wait()jest w zasadzie dokładnie tym samym co Thread.Sleep(1000). W rzeczywistym kodzie produkcyjnym rzadko jest to właściwe.
Servy
@ronag: Twój WaitAllpowoduje impas. Zobacz link do mojego bloga w mojej odpowiedzi, aby uzyskać więcej informacji. Zamiast tego powinieneś użyć await Task.WhenAll.
Stephen Cleary
6
@ronag Ponieważ masz ConfigureAwait(false)do pojedynczego połączenia do Barlub Rosnie impasu, ale ponieważ masz przeliczalny, który jest tworzenie więcej niż jeden, a następnie czeka na wszystkich tych, pierwszy pasek impasu drugi. Jeśli await Task.WhenAllzamiast czekać na wszystkie zadania, aby nie zablokować kontekstu ASP, zobaczysz, że metoda powraca normalnie.
Servy
2
@ronag Inną opcją byłoby dodanie .ConfigureAwait(false) całego drzewa w górę, aż do momentu zablokowania, w ten sposób nic nigdy nie próbuje wrócić do głównego kontekstu; to powinno działać. Inną opcją byłoby rozwinięcie wewnętrznego kontekstu synchronizacji. Link . Jeśli umieścisz Task.WhenAllw sposób AsyncPump.Runzostanie ona skutecznie blokować na całość bez potrzeby ConfigureAwaitgdziekolwiek, ale to prawdopodobnie nadmiernie skomplikowane rozwiązanie.
Servy

Odpowiedzi:

268

Waiti await- choć podobne koncepcyjnie - są w rzeczywistości zupełnie inne.

Waitbędzie synchronicznie blokować do momentu zakończenia zadania. Tak więc bieżący wątek jest dosłownie zablokowany, czekając na zakończenie zadania. Zasadniczo powinieneś używać „do asyncsamego końca”; to znaczy nie blokuj asynckodu. Na moim blogu szczegółowo omawiam, w jaki sposób blokowanie kodu asynchronicznego powoduje zakleszczenie .

awaitbędzie asynchronicznie czekać na zakończenie zadania. Oznacza to, że bieżąca metoda jest „wstrzymana” (jej stan jest przechwytywany), a metoda zwraca swojemu rozmówcy niepełne zadanie. Później, gdy awaitwyrażenie zostanie zakończone, pozostała część metody jest planowana jako kontynuacja.

Wspomniałeś także o „bloku kooperacyjnym”, przez który zakładam, że masz na myśli zadanie, które Waitwykonujesz w oczekującym wątku. Są sytuacje, w których może się to zdarzyć, ale jest to optymalizacja. Istnieje wiele sytuacji, w których nie może się to zdarzyć, na przykład jeśli zadanie jest dla innego programu planującego lub jeśli zostało już uruchomione lub jeśli nie jest to zadanie kodowe (na przykład w twoim kodzie: Waitnie można wykonać Delayzadania bezpośrednio, ponieważ nie ma kodu dla tego).

Pomocne może okazać się moje async/ awaitwprowadzenie .

Stephen Cleary
źródło
5
Myślę, że istnieje nieporozumienie, Waitdziała dobrze awaitimpas.
ronag
1
Wyraźnie: Tak, jeśli mogę zrezygnować await Task.Delay(1)z Task.Delay(1).Wait()usługi działa dobrze, w przeciwnym razie zakleszczenia.
ronag
5
Nie, harmonogram zadań tego nie zrobi. Waitblokuje wątek i nie można go używać do innych celów.
Stephen Cleary
8
@ronag Domyślam się, że po prostu pomieszałeś nazwy metod i twój impas został spowodowany przez kod blokujący i działał z awaitkodem. Albo to, albo impas nie był związany z żadnym z nich, a ty źle zdiagnozowałeś problem.
Servy
3
@hexterminator: Jest to zgodne z projektem - działa świetnie w aplikacjach interfejsu użytkownika, ale często przeszkadza aplikacjom ASP.NET. Program ASP.NET Core naprawił to, usuwając SynchronizationContextblokadę w żądaniu programu ASP.NET Core, aby nie blokować się.
Stephen Cleary,
5

Na podstawie tego, co przeczytałem z różnych źródeł:

awaitWyrażenie nie blokuje nić, na której jest wykonywany. Zamiast tego powoduje, że kompilator zarejestruje resztę asyncmetody jako kontynuację oczekiwanego zadania. Kontrola następnie powraca do obiektu wywołującego asyncmetodę. Po zakończeniu zadania wywołuje jego kontynuację, a wykonywanie asyncmetody jest wznawiane od miejsca, w którym zostało przerwane.

Aby poczekać na taskukończenie jednego , możesz wywołać jego Task.Waitmetodę. Wywołanie Waitmetody blokuje wątek wywołujący, dopóki instancja jednej klasy nie zakończy wykonywania. Metoda bez parametrów Wait()służy do bezwarunkowego oczekiwania na zakończenie zadania. Zadanie symuluje pracę, wywołując Thread.Sleepmetodę uśpienia na dwie sekundy.

Ten artykuł jest również dobrą lekturą.

Ayushmati
źródło
3
„Czy to nie jest technicznie niepoprawne? Czy ktoś mógłby to wyjaśnić?” - czy mogę wyjaśnić; zadajesz to jako pytanie? (Chcę tylko wyjaśnić, czy pytasz czy odpowiadasz). Jeśli pytasz: może działać lepiej jako osobne pytanie; jest mało prawdopodobne, aby zebrano tu nowe odpowiedzi jako odpowiedzi
Marc Gravell
1
Odpowiedziałem na to pytanie i zadałem osobne pytanie w związku z wątpliwościami, które miałem tutaj stackoverflow.com/questions/53654006/… Dzięki @MarcGravell. Czy możesz teraz usunąć swój głos za usunięciem?
Ayushmati,
„Czy możesz teraz usunąć swój głos za usunięciem?” - to nie jest moje; dzięki ♦ wszelkie takie głosowania odniosłyby skutek natychmiastowy. Nie sądzę jednak, aby stanowiło to odpowiedź na kluczowe pytania, które dotyczą zachowania impasu.
Marc Gravell
To nie jest prawda. Aż do pierwszego oczekiwania na
nieosiągnięcie
-2

Niektóre ważne fakty nie zostały podane w innych odpowiedziach:

„async oczekuje” jest bardziej złożony na poziomie CIL, a zatem kosztuje pamięć i czas procesora.

Każde zadanie można anulować, jeśli czas oczekiwania jest niedopuszczalny.

W przypadku „asynchronicznego oczekiwania” nie mamy modułu obsługującego takie zadanie, aby je anulować lub monitorować.

Korzystanie z zadania jest bardziej elastyczne niż „asynchroniczne czekanie”.

Każda funkcja synchronizacji może być zapakowana przez asynchronię.

public async Task<ActionResult> DoAsync(long id) 
{ 
    return await Task.Run(() => { return DoSync(id); } ); 
} 

„async oczekuje” generuje wiele problemów. Nie oczekujemy teraz, że instrukcja zostanie osiągnięta bez debugowania środowiska wykonawczego i kontekstu. Jeśli pierwsze oczekiwanie nie zostanie osiągnięte, wszystko jest zablokowane . Czasami zdarza się, że zdarzenie, które oczekuje, wciąż się kończy, ale wszystko jest zablokowane:

https://github.com/dotnet/runtime/issues/36063

Nie rozumiem, dlaczego muszę żyć z duplikacją kodu dla metody synchronizacji i asynchronizacji lub używania hacków.

Wniosek: Utwórz zadanie ręcznie i kontroluj je, jest znacznie lepsze. Handler to Task daje większą kontrolę. Możemy monitorować zadania i zarządzać nimi:

https://github.com/lsmolinski/MonitoredQueueBackgroundWorkItem

Przepraszam za mój angielski.

użytkownik1785960
źródło