Najlepsza praktyka, aby wywołać ConfigureAwait dla całego kodu po stronie serwera

561

Jeśli masz kod po stronie serwera (np. Niektóre ApiController), a twoje funkcje są asynchroniczne - więc zwracają się Task<SomeObject>- czy jest najlepszą praktyką, że za każdym razem czekasz na funkcje, które wywołujesz ConfigureAwait(false)?

Przeczytałem, że jest bardziej wydajny, ponieważ nie musi przełączać kontekstów wątków z powrotem do oryginalnego kontekstu wątków. Jednak w przypadku interfejsu ASP.NET Web Api, jeśli twoje zapytanie przychodzi do jednego wątku, a ty czekasz na jakąś funkcję i wywołanie, ConfigureAwait(false)które potencjalnie mogą umieścić cię w innym wątku, gdy zwracasz końcowy wynik działania tej ApiControllerfunkcji.

Poniżej podałem przykład tego, o czym mówię:

public class CustomerController : ApiController
{
    public async Task<Customer> Get(int id)
    {
        // you are on a particular thread here
        var customer = await SomeAsyncFunctionThatGetsCustomer(id).ConfigureAwait(false);

        // now you are on a different thread!  will that cause problems?
        return customer;
    }
}
Arash Emami
źródło

Odpowiedzi:

628

Aktualizacja: ASP.NET Core nie maSynchronizationContext . Jeśli korzystasz z ASP.NET Core, nie ma znaczenia, czy używasz, ConfigureAwait(false)czy nie.

W przypadku ASP.NET „Full”, „Classic” lub cokolwiek, reszta tej odpowiedzi nadal obowiązuje.

Oryginalny post (dla nie-Core ASP.NET):

Ten film wideo zespołu ASP.NET zawiera najlepsze informacje na temat korzystania asyncz ASP.NET.

Przeczytałem, że jest bardziej wydajny, ponieważ nie musi przełączać kontekstów wątków z powrotem do oryginalnego kontekstu wątków.

Dotyczy to aplikacji interfejsu użytkownika, w których istnieje tylko jeden wątek interfejsu użytkownika, do którego należy „zsynchronizować”.

W ASP.NET sytuacja jest nieco bardziej złożona. Gdy asyncmetoda wznawia wykonywanie, pobiera wątek z puli wątków ASP.NET. Jeśli wyłączysz przechwytywanie kontekstu za pomocą ConfigureAwait(false), wątek będzie kontynuował bezpośrednie wykonywanie metody. Jeśli nie wyłączysz przechwytywania kontekstu, wątek ponownie wejdzie w kontekst żądania, a następnie będzie kontynuował wykonywanie metody.

Więc ConfigureAwait(false)nie oszczędza ci skoku wątku w ASP.NET; oszczędza ci to ponownego wprowadzania kontekstu żądania, ale zwykle jest to bardzo szybkie. ConfigureAwait(false) może być przydatne, jeśli próbujesz wykonać niewielką ilość równoległego przetwarzania żądania, ale tak naprawdę TPL lepiej pasuje do większości tych scenariuszy.

Jednak w przypadku interfejsu ASP.NET Web Api, jeśli twoje zapytanie przychodzi do jednego wątku, a ty czekasz na jakąś funkcję i wywołujesz ConfigureAwait (false), który może potencjalnie umieścić cię w innym wątku, gdy zwracasz końcowy wynik funkcji ApiController .

Właściwie po prostu robienie i awaitmoże to zrobić. Gdy twoja asyncmetoda trafi na await, metoda jest blokowana, ale wątek wraca do puli wątków. Gdy metoda jest gotowa do kontynuacji, dowolny wątek jest pobierany z puli wątków i używany do wznowienia metody.

Jedyną różnicą ConfigureAwaitw ASP.NET jest to, czy wątek wchodzi w kontekst żądania podczas wznawiania metody.

Mam więcej podstawowych informacji w moim artykule MSDNSynchronizationContext i moim asyncblogu wprowadzającym .

Stephen Cleary
źródło
23
Lokalny magazyn wątków nie przepływa przez żaden kontekst. HttpContext.Currentprzepływa przez ASP.NET SynchronizationContext, który przepływa domyślnie, gdy Ty await, ale nie przepływa przez ContinueWith. OTOH, kontekst wykonania (w tym ograniczenia bezpieczeństwa) to kontekst wymieniony w CLR za pośrednictwem C #, i jest przepuszczany przez oba ContinueWithi await(nawet jeśli używasz ConfigureAwait(false)).
Stephen Cleary
65
Czy nie byłoby wspaniale, gdyby C # miał obsługę języka ojczystego dla ConfigureAwait (false)? Coś w rodzaju „awaitnc” (nie oczekuje kontekstu). Wpisanie wszędzie osobnego wywołania metody jest dość denerwujące. :)
NathanAldenSr
19
@NathanAldenSr: Dyskutowano sporo. Problem z nowym słowem kluczowym polega na tym, że w ConfigureAwaitrzeczywistości ma on sens tylko wtedy , gdy czekasz na zadanie , podczas gdy awaitdziała na „oczekiwane”. Inne rozważane opcje to: Czy domyślne zachowanie powinno odrzucić kontekst, jeśli znajduje się w bibliotece? A może masz ustawienia kompilatora dla domyślnego zachowania kontekstowego? Oba zostały odrzucone, ponieważ trudniej jest po prostu przeczytać kod i powiedzieć, co robi.
Stephen Cleary
10
@AnshulNigam: Właśnie dlatego działania kontrolera potrzebują kontekstu. Ale większość metod, które wywołują te działania, tego nie robi.
Stephen Cleary
14
@JonathanRoeder: Ogólnie rzecz biorąc, nie powinieneś ConfigureAwait(false)unikać zakleszczenia opartego na Result/ Wait, ponieważ w ASP.NET nie powinieneś używać Result/ Wait.
Stephen Cleary
131

Krótka odpowiedź na twoje pytanie: Nie. Nie powinieneś dzwonić ConfigureAwait(false)na takim poziomie aplikacji.

Długa wersja TL; DR długiej odpowiedzi: jeśli piszesz bibliotekę, w której nie znasz konsumenta i nie potrzebujesz kontekstu synchronizacji (którego, jak sądzę, nie powinieneś mieć w bibliotece), zawsze powinieneś jej używać ConfigureAwait(false). W przeciwnym razie konsumenci Twojej biblioteki mogą napotkać impas, wykorzystując metody asynchroniczne w sposób blokujący. To zależy od sytuacji.

Oto nieco bardziej szczegółowe wyjaśnienie znaczenia ConfigureAwaitmetody (cytat z mojego posta na blogu):

Gdy czekasz na metodę ze słowem kluczowym Oczekiwanie, kompilator generuje w twoim imieniu sporo kodu. Jednym z celów tej akcji jest obsługa synchronizacji z wątkiem interfejsu użytkownika (lub głównego). Kluczowym elementem tej funkcji jest SynchronizationContext.Currentkontekst synchronizacji dla bieżącego wątku. SynchronizationContext.Currentjest wypełniany w zależności od środowiska, w którym się znajdujesz. GetAwaiterMetoda Zadania wyszukuje SynchronizationContext.Current. Jeśli bieżący kontekst synchronizacji nie ma wartości zerowej, kontynuacja przekazywana do tego oczekującego zostanie wysłana z powrotem do tego kontekstu synchronizacji.

Podczas korzystania z metody, która korzysta z nowych funkcji języka asynchronicznego, w sposób blokujący, zakończy się impasem, jeśli masz dostępny kontekst synchronizacji. Gdy używasz takich metod w sposób blokujący (czekając na metodę Zadanie z Czekaj lub pobierając wynik bezpośrednio z właściwości Wynik zadania), blokujesz jednocześnie główny wątek. Kiedy ostatecznie zadanie zakończy się wewnątrz tej metody w puli wątków, uruchomi kontynuację, aby wysłać wiadomość z powrotem do głównego wątku, ponieważ SynchronizationContext.Currentjest ona dostępna i przechwycona. Ale jest tutaj problem: wątek interfejsu użytkownika jest zablokowany i masz impas!

Oto dwa świetne artykuły, które są dokładnie na twoje pytanie:

Na koniec jest świetny krótki film od Luciana Wischika na ten temat: Metody bibliotek asynchronicznych powinny rozważyć użycie Task.ConfigureAwait (false) .

Mam nadzieję że to pomoże.

tugberk
źródło
2
„Metoda GetAwaiter zadania wyszukuje SynchronizationContext.Current. Jeśli bieżący kontekst synchronizacji nie ma wartości null, kontynuacja przekazywana do tego oczekiwania zostanie wysłana z powrotem do tego kontekstu synchronizacji.” - Mam wrażenie, że próbujesz powiedzieć, że Taskchodzi o stos, aby uzyskać SynchronizationContext, co jest złe. Jest SynchronizationContexton pobierany przed wywołaniem, Taska następnie reszta kodu jest kontynuowana, SynchronizationContextjeśli if SynchronizationContext.Currentnie jest zerowy.
casperOne
1
@casperOne Chciałem powiedzieć to samo.
tugberk
8
Czy to nie obowiązkiem osoby dzwoniącej jest upewnienie się, że SynchronizationContext.Currentjest ona czysta / lub że biblioteka jest wywoływana w miejsce Task.Run()zamiast konieczności pisania .ConfigureAwait(false)całej biblioteki klas?
binki
1
@binki - z drugiej strony: (1) przypuszczalnie biblioteka jest używana w wielu aplikacjach, więc jednorazowy wysiłek w bibliotece, aby ułatwić aplikację, jest opłacalny; (2) przypuszczalnie autor biblioteki wie, że napisał kod, który nie ma powodu, aby wymagać kontynuowania oryginalnego kontekstu, który wyraża przez te słowa .ConfigureAwait(false). Być może byłoby to łatwiejsze dla autorów bibliotek, gdyby takie było zachowanie domyślne, ale przypuszczam, że nieco trudniejsze jest poprawne napisanie biblioteki jest lepsze niż utrudnienie prawidłowego napisania aplikacji.
ToolmakerSteve
4
Dlaczego autor biblioteki powinien kodować konsumenta? Jeśli konsument chce impasu, dlaczego miałbym temu zapobiec?
Quarkly,
25

Największą wadą, jaką znalazłem przy użyciu ConfigureAwait (false) jest to, że kultura wątków jest przywracana do wartości domyślnych systemu. Jeśli skonfigurowałeś kulturę np. ...

<system.web>
    <globalization culture="en-AU" uiCulture="en-AU" />    
    ...

i hostujesz na serwerze, którego kultura jest ustawiona na en-US, to przekonasz się, że ConfigureAwait (false) nazywa się CultureInfo. CurrentCulture zwróci en-AU i po uzyskaniu en-US. to znaczy

// CultureInfo.CurrentCulture ~ {en-AU}
await xxxx.ConfigureAwait(false);
// CultureInfo.CurrentCulture ~ {en-US}

Jeśli twoja aplikacja robi coś, co wymaga specyficznego dla kultury formatowania danych, musisz o tym pamiętać podczas korzystania z ConfigureAwait (false).

Mick
źródło
27
Nowoczesne wersje .NET (myślę, że od wersji 4.6?) Będą propagować kulturę wśród wątków, nawet jeśli ConfigureAwait(false)są używane.
Stephen Cleary
1
Dzięki za informację. Rzeczywiście używamy .net 4.5.2
Mick
11

Mam kilka ogólnych przemyśleń na temat wdrożenia Task:

  1. Zadanie jest jednorazowe, ale nie powinniśmy z niego korzystać using.
  2. ConfigureAwaitzostał wprowadzony w 4.5. Taskzostał wprowadzony w wersji 4.0.
  3. Wątki .NET zawsze używane do przepływu kontekstu (patrz C # poprzez książkę CLR), ale w domyślnej implementacji Task.ContinueWithnie mają b / c okazało się, że zmiana kontekstu jest droga i domyślnie jest wyłączona.
  4. Problem polega na tym, że programista biblioteki nie powinien dbać o to, czy jego klienci potrzebują przepływu kontekstu, czy też nie, dlatego nie powinien decydować, czy przepływ kontekstu, czy nie.
  5. [Dodano później] Fakt, że nie ma autorytatywnej odpowiedzi i odpowiedniego odniesienia, a my wciąż o to walczymy, oznacza, że ​​ktoś nie wykonał dobrze swojej pracy.

Mam kilka postów na ten temat, ale moje zdanie - oprócz ładnej odpowiedzi Tugberka - polega na tym, że powinieneś włączyć wszystkie interfejsy API asynchronicznie i idealnie przepłynąć kontekst. Ponieważ wykonujesz asynchronię, możesz po prostu użyć kontynuacji zamiast czekać, więc nie nastąpi zakleszczenie, ponieważ w bibliotece nie zostanie wykonane żadne czekanie, a przepływ zostanie zachowany, więc kontekst zostanie zachowany (na przykład HttpContext).

Problem polega na tym, że biblioteka udostępnia synchroniczny interfejs API, ale korzysta z innego asynchronicznego interfejsu API - dlatego należy użyć Wait()/ Resultw kodzie.

Aliostad
źródło
6
1) Możesz zadzwonić, Task.Disposejeśli chcesz; po prostu nie potrzebujesz przez większość czasu. 2) Taskzostał wprowadzony w .NET 4.0 jako część TPL, co nie wymagało ConfigureAwait; kiedy asynczostał dodany, ponownie wykorzystali istniejący Tasktyp zamiast wynaleźć nowy Future.
Stephen Cleary
6
3) Mylisz dwa różne rodzaje „kontekstu”. „Kontekst” wspomniany w języku C # przez CLR jest zawsze przepływający, nawet w Tasks; „kontekst” kontrolowany przez ContinueWithto SynchronizationContextlub TaskScheduler. Te różne konteksty zostały szczegółowo wyjaśnione na blogu Stephena Touba .
Stephen Cleary
21
4) Autor biblioteki nie musi dbać o to, czy jego wywołujący potrzebują przepływu kontekstu, ponieważ każda metoda asynchroniczna wznawia się niezależnie. Jeśli więc wywołujący potrzebują przepływu kontekstu, mogą go przepływać, niezależnie od tego, czy autor biblioteki go przepuścił, czy nie.
Stephen Cleary
1
Na początku wydajesz się narzekać zamiast odpowiadać na pytanie. A potem mówisz o „kontekście”, z wyjątkiem tego, że w .Net istnieje kilka rodzajów kontekstu i naprawdę nie jest jasne, o którym (lub tych?) Mówisz. I nawet jeśli nie jesteś zdezorientowany (ale myślę, że tak, uważam, że nie ma kontekstu, który płynąłby z Threads, ale już nie z nim ContinueWith()), to sprawia, że ​​twoja odpowiedź jest myląca z czytaniem.
sick
1
@StephenCleary tak, lib dev nie powinien wiedzieć, to zależy od klienta. Myślałem, że to wyjaśniłem, ale moje sformułowanie nie było jasne.
Aliostad