HttpClient.GetAsync (…) nigdy nie zwraca wartości przy użyciu funkcji Oczekiwania / Asynchronizacji

315

Edit: To pytanie wygląda to może być ten sam problem, ale nie ma odpowiedzi ...

Edycja: W przypadku testowym 5 zadanie wydaje się być zablokowane WaitingForActivation.

Napotkałem pewne dziwne zachowanie podczas używania System.Net.Http.HttpClient w .NET 4.5 - gdzie „oczekiwanie” na wynik wywołania (np.) httpClient.GetAsync(...)Nigdy nie powróci.

Dzieje się tak tylko w niektórych okolicznościach, gdy używana jest nowa funkcja języka async / czekaj i API zadań - kod zawsze wydaje się działać, gdy używa się tylko kontynuacji.

Oto kod, który odtwarza problem - upuść go w nowym „projekcie MVC 4 WebApi” w Visual Studio 11, aby ujawnić następujące punkty końcowe GET:

/api/test1
/api/test2
/api/test3
/api/test4
/api/test5 <--- never completes
/api/test6

Każdy z punktów końcowych tutaj zwraca te same dane (nagłówki odpowiedzi z stackoverflow.com) z wyjątkiem /api/test5 których nigdy się nie kończy.

Czy napotkałem błąd w klasie HttpClient, czy też niewłaściwie korzystam z interfejsu API?

Kod do reprodukcji:

public class BaseApiController : ApiController
{
    /// <summary>
    /// Retrieves data using continuations
    /// </summary>
    protected Task<string> Continuations_GetSomeDataAsync()
    {
        var httpClient = new HttpClient();

        var t = httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead);

        return t.ContinueWith(t1 => t1.Result.Content.Headers.ToString());
    }

    /// <summary>
    /// Retrieves data using async/await
    /// </summary>
    protected async Task<string> AsyncAwait_GetSomeDataAsync()
    {
        var httpClient = new HttpClient();

        var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead);

        return result.Content.Headers.ToString();
    }
}

public class Test1Controller : BaseApiController
{
    /// <summary>
    /// Handles task using Async/Await
    /// </summary>
    public async Task<string> Get()
    {
        var data = await Continuations_GetSomeDataAsync();

        return data;
    }
}

public class Test2Controller : BaseApiController
{
    /// <summary>
    /// Handles task by blocking the thread until the task completes
    /// </summary>
    public string Get()
    {
        var task = Continuations_GetSomeDataAsync();

        var data = task.GetAwaiter().GetResult();

        return data;
    }
}

public class Test3Controller : BaseApiController
{
    /// <summary>
    /// Passes the task back to the controller host
    /// </summary>
    public Task<string> Get()
    {
        return Continuations_GetSomeDataAsync();
    }
}

public class Test4Controller : BaseApiController
{
    /// <summary>
    /// Handles task using Async/Await
    /// </summary>
    public async Task<string> Get()
    {
        var data = await AsyncAwait_GetSomeDataAsync();

        return data;
    }
}

public class Test5Controller : BaseApiController
{
    /// <summary>
    /// Handles task by blocking the thread until the task completes
    /// </summary>
    public string Get()
    {
        var task = AsyncAwait_GetSomeDataAsync();

        var data = task.GetAwaiter().GetResult();

        return data;
    }
}

public class Test6Controller : BaseApiController
{
    /// <summary>
    /// Passes the task back to the controller host
    /// </summary>
    public Task<string> Get()
    {
        return AsyncAwait_GetSomeDataAsync();
    }
}
Benjamin Fox
źródło
2
Wydaje się, że nie jest to ten sam problem, ale dla pewności, że o tym wiesz, w metodach asynchronicznych WRT beta występuje błąd MVC4, który wykonuje się synchronicznie - patrz stackoverflow.com/questions/9627329/…
James Manning
Dzięki - będę na to uważał. W takim przypadku myślę, że metoda powinna być zawsze asynchroniczna z powodu wywołania HttpClient.GetAsync(...)?
Benjamin Fox

Odpowiedzi:

468

Nadużywasz interfejsu API.

Oto sytuacja: w ASP.NET tylko jeden wątek może obsłużyć żądanie jednocześnie. W razie potrzeby możesz wykonać równoległe przetwarzanie (pożyczanie dodatkowych wątków z puli wątków), ale tylko jeden wątek będzie miał kontekst żądania (dodatkowe wątki nie mają kontekstu żądania).

Jest zarządzany przez ASP.NETSynchronizationContext .

Domyślnie, gdy jesteś awaita Task, metoda wznawia przechwycone SynchronizationContext(lub przechwycone TaskScheduler, jeśli nie ma SynchronizationContext). Zwykle jest to dokładnie to, czego chcesz: asynchroniczna czynność kontrolera awaitcoś zrobi , a po wznowieniu wznawia z kontekstem żądania.

Oto dlaczego test5zawodzi:

  • Test5Controller.Getwykonuje AsyncAwait_GetSomeDataAsync(w kontekście żądania ASP.NET).
  • AsyncAwait_GetSomeDataAsyncwykonuje HttpClient.GetAsync(w kontekście żądania ASP.NET).
  • Żądanie HTTP jest wysyłane i HttpClient.GetAsync zwraca niezakończone Task.
  • AsyncAwait_GetSomeDataAsyncczeka na Task; ponieważ nie jest kompletny, AsyncAwait_GetSomeDataAsynczwraca niedokończone Task.
  • Test5Controller.Get blokuje bieżący wątek do tego czasuTask zakończy.
  • Odpowiedź HTTP przychodzi, a Taskzwracana jest przezHttpClient.GetAsync jest zakończone.
  • AsyncAwait_GetSomeDataAsyncpróbuje wznowić w kontekście żądania ASP.NET. Jednak w tym kontekście jest już wątek: wątek został zablokowanyTest5Controller.Get .
  • Impas.

Oto dlaczego pozostałe działają:

  • ( test1, test2i test3): Continuations_GetSomeDataAsyncplanuje kontynuację puli wątków poza kontekstem żądania ASP.NET. To pozwalaTask zwrotu przez Continuations_GetSomeDataAsyncbez konieczności ponownego wprowadzania kontekstu żądania.
  • ( test4i test6): Ponieważ oczekiwanyTask jest , wątek żądania ASP.NET nie jest blokowany. To pozwalaAsyncAwait_GetSomeDataAsync to użycie kontekstu żądania ASP.NET, gdy jest on gotowy do kontynuowania.

A oto najlepsze praktyki:

  1. W asyncmetodach „bibliotecznych” używaj, ConfigureAwait(false)gdy tylko jest to możliwe. W twoim przypadku to się zmieniAsyncAwait_GetSomeDataAsync navar result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
  2. Nie blokuj na Tasks; jest już asyncna dole. Innymi słowy, użyj awaitzamiast GetResult( Task.Resulti Task.Waitnależy je również zastąpić await).

W ten sposób uzyskuje się obie korzyści: kontynuacja (pozostała część AsyncAwait_GetSomeDataAsyncmetody) jest uruchamiana w podstawowym wątku puli wątków, który nie musi wchodzić w kontekst żądania ASP.NET; a sam kontroler toasync (który nie blokuje wątku żądania).

Więcej informacji:

Aktualizacja 2012-07-13: Włączono tę odpowiedź do postu na blogu .

Stephen Cleary
źródło
2
Czy jest jakaś dokumentacja dla ASP.NET, SynchroniztaionContextktóra wyjaśnia, że dla danego żądania może istnieć tylko jeden wątek? Jeśli nie, myślę, że tak powinno być.
svick
8
Nigdzie nie jest to udokumentowane AFAIK.
Stephen Cleary
10
Dzięki - niesamowita odpowiedź. Różnica w zachowaniu między (pozornie) funkcjonalnie identycznym kodem jest frustrująca, ale ma sens z twoim wyjaśnieniem. Byłoby użyteczne, gdyby środowisko było w stanie wykryć takie zakleszczenia i gdzieś zgłosić wyjątek.
Benjamin Fox
3
Czy istnieją sytuacje, w których użycie polecenia .ConfigureAwait (false) w kontekście asp.net NIE jest zalecane? Wydaje mi się, że należy go zawsze używać i że jest to tylko w kontekście interfejsu użytkownika, że ​​nie należy go używać, ponieważ musisz zsynchronizować się z interfejsem użytkownika. A może brakuje mi sensu?
AlexGad
3
ASP.NET SynchronizationContextzapewnia pewne ważne funkcje: przepływa kontekst żądania. Obejmuje to wszelkiego rodzaju rzeczy, od uwierzytelniania przez pliki cookie po kulturę. W ASP.NET zamiast synchronizować z powrotem do interfejsu użytkownika, synchronizujesz z powrotem w kontekście żądania. Może się to wkrótce zmienić: nowy ApiControllerma HttpRequestMessagekontekst jako właściwość - więc przepuszczanie kontekstu może nie być wymagane SynchronizationContext- ale jeszcze tego nie wiem.
Stephen Cleary
61

Edycja: Generalnie staraj się unikać wykonywania poniższych czynności, z wyjątkiem ostatniego wysiłku w celu uniknięcia impasu. Przeczytaj pierwszy komentarz Stephena Cleary'ego.

Szybka naprawa stąd . Zamiast pisać:

Task tsk = AsyncOperation();
tsk.Wait();

Próbować:

Task.Run(() => AsyncOperation()).Wait();

Lub jeśli potrzebujesz wyniku:

var result = Task.Run(() => AsyncOperation()).Result;

Ze źródła (edytowanego w celu dopasowania do powyższego przykładu):

AsyncOperation będzie teraz wywoływane w ThreadPool, gdzie nie będzie SynchronizationContext, a kontynuacje używane wewnątrz AsyncOperation nie będą zmuszane do powrotu do wątku wywołującego.

Dla mnie wygląda to na użyteczną opcję, ponieważ nie mam opcji, aby całkowicie ją asynchronizować (co wolałbym).

Ze źródła:

Upewnij się, że oczekiwanie w metodzie FooAsync nie znajduje kontekstu, z którego mógłby powrócić. Najprostszym sposobem na to jest wywołanie pracy asynchronicznej z ThreadPool, na przykład poprzez zawinięcie wywołania w Task.Run, np.

int Sync () {return Task.Run (() => Library.FooAsync ()). Wynik; }

FooAsync będzie teraz wywoływane w ThreadPool, gdzie nie będzie SynchronizationContext, a kontynuacje używane w FooAsync nie będą zmuszane z powrotem do wątku wywołującego Sync ().

Ykok
źródło
7
Może chcesz ponownie przeczytać link źródłowy; autor nie zaleca tego. Czy to działa? Tak, ale tylko w tym sensie, że unikasz impasu. To rozwiązanie neguje wszystkie zalety asynckodu w ASP.NET i w rzeczywistości może powodować problemy na dużą skalę. BTW, ConfigureAwaitnie „przerywa właściwego zachowania asynchronicznego” w żadnym scenariuszu; dokładnie tego powinieneś używać w kodzie biblioteki.
Stephen Cleary
2
To cała pierwsza sekcja, pogrubiona Avoid Exposing Synchronous Wrappers for Asynchronous Implementations. Cała reszta postu wyjaśnia kilka różnych sposobów na zrobienie tego, jeśli jest to absolutnie konieczne .
Stephen Cleary
1
Dodano sekcję, którą znalazłem w źródle - pozostawiam ją przyszłym czytelnikom do podjęcia decyzji. Zauważ, że zazwyczaj powinieneś unikać tego i robić to tylko jako ostatnią szansę (tzn. Gdy używasz kodu asynchronicznego, nad którym nie masz kontroli).
Ykok
3
Lubię wszystkie odpowiedzi tutaj i jak zawsze ... wszystkie są oparte na kontekście (pun zamierzony lol). Otaczam wywołania Async HttpClienta wersją synchroniczną, więc nie mogę zmienić tego kodu, aby dodać ConfigureAwait do tej biblioteki. Aby zapobiec zakleszczeniom w produkcji, zamykam wywołania Async w Task.Run. Jak rozumiem, użyje 1 dodatkowego wątku na żądanie i pozwoli uniknąć impasu. Zakładam, że aby być w pełni zgodnym, muszę użyć metod synchronizacji WebClient. Jest to dużo pracy do uzasadnienia, więc potrzebuję ważnego powodu, aby nie trzymać się mojego obecnego podejścia.
samneric
1
W końcu stworzyłem metodę rozszerzenia do konwersji Async na synchronizację. Czytam tutaj gdzieś w ten sam sposób, w jaki robi to .NET Framework: public static TResult RunSync <TResult> (this Func <Task <TResult>> func) {return _taskFactory .StartNew (func) .Unwrap () .GetAwaiter () .GetResult (); }
samneric
10

Ponieważ używasz .Resultlub .Waitlub awaitspowoduje to zakleszczenie w kodzie.

można użyć ConfigureAwait(false)w asyncmetodach zapobiegania impasu

lubię to:

var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead)
                             .ConfigureAwait(false);

w ConfigureAwait(false)miarę możliwości możesz używać opcji Nie blokuj kodu asynchronicznego.

Hasan Fathi
źródło
2

Te dwie szkoły tak naprawdę nie wykluczają.

Oto scenariusz, w którym musisz po prostu użyć

   Task.Run(() => AsyncOperation()).Wait(); 

lub coś w tym rodzaju

   AsyncContext.Run(AsyncOperation);

Mam działanie MVC, które jest objęte atrybutem transakcji bazy danych. Pomysł polegał (prawdopodobnie) na wycofaniu wszystkiego, co zrobiono w akcji, jeśli coś pójdzie nie tak. Nie pozwala to na przełączanie kontekstu, w przeciwnym razie wycofanie transakcji lub zatwierdzenie się nie powiedzie.

Potrzebna mi biblioteka jest asynchroniczna, ponieważ oczekuje się, że będzie działać asynchronicznie.

Jedyna opcja. Uruchom jako zwykłe połączenie synchronizacji.

Po prostu mówię każdemu.

alex.peter
źródło
więc sugerujesz pierwszą opcję w swojej odpowiedzi?
Don Cheadle
1

Zamieszczę to tutaj bardziej dla kompletności niż dla bezpośredniego związku z PO. Spędziłem prawie dzień debugując HttpClientzapytanie, zastanawiając się, dlaczego nigdy nie otrzymałem odpowiedzi.

W końcu okazało się, że zapomniał awaito asynczaproszeniu dalej w dół stosu wywołań.

Czuje się tak dobrze, jak brak średnika.

Bondolin
źródło
-1

Szukam tutaj:

http://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.taskawaiter(v=vs.110).aspx

I tu:

http://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.taskawaiter.getresult(v=vs.110).aspx

I widząc:

Ten typ i jego elementy są przeznaczone do użytku przez kompilator.

Biorąc pod uwagę, że awaitwersja działa i czy jest to „właściwy” sposób robienia rzeczy, czy naprawdę potrzebujesz odpowiedzi na to pytanie?

Mój głos brzmi: nadużycie interfejsu API .

yamen
źródło
Nie zauważyłem tego, chociaż widziałem inny język, który wskazuje, że używanie GetResult () API jest obsługiwanym (i oczekiwanym) przypadkiem użycia.
Benjamin Fox
1
Co więcej, jeśli refaktoryzujesz, Test5Controller.Get()aby wyeliminować oczekującego, wykonując następujące czynności: var task = AsyncAwait_GetSomeDataAsync(); return task.Result;To samo zachowanie można zaobserwować.
Benjamin Fox