Bewährte Methode zum Aufrufen von ConfigureAwait für den gesamten serverseitigen Code

561

Wenn Sie serverseitigen Code haben (dh einige ApiController) und Ihre Funktionen asynchron sind - sie kehren also zurück Task<SomeObject>-, wird dies als bewährte Methode angesehen, wenn Sie jedes Mal auf Funktionen warten, die Sie aufrufen ConfigureAwait(false)?

Ich hatte gelesen, dass es leistungsfähiger ist, da es nicht die Thread-Kontexte zurück zum ursprünglichen Thread-Kontext wechseln muss. Wenn Ihre Anforderung mit ASP.NET Web Api jedoch in einem Thread eingeht und Sie auf eine Funktion und einen Aufruf warten ConfigureAwait(false), die Sie möglicherweise in einen anderen Thread versetzen könnten, wenn Sie das Endergebnis Ihrer ApiControllerFunktion zurückgeben.

Ich habe ein Beispiel für das geschrieben, worüber ich unten spreche:

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
quelle

Antworten:

628

Update: ASP.NET Core hat keineSynchronizationContext . Wenn Sie mit ASP.NET Core arbeiten, spielt es keine Rolle, ob Sie verwenden ConfigureAwait(false)oder nicht.

Für ASP.NET "Full" oder "Classic" oder was auch immer gilt der Rest dieser Antwort weiterhin.

Ursprünglicher Beitrag (für Nicht-Core-ASP.NET):

Dieses Video des ASP.NET-Teams enthält die besten Informationen zur Verwendung asyncin ASP.NET.

Ich hatte gelesen, dass es leistungsfähiger ist, da es nicht die Thread-Kontexte zurück zum ursprünglichen Thread-Kontext wechseln muss.

Dies gilt für UI-Anwendungen, bei denen es nur einen UI-Thread gibt, mit dem Sie "synchronisieren" müssen.

In ASP.NET ist die Situation etwas komplexer. Wenn eine asyncMethode die Ausführung fortsetzt, wird ein Thread aus dem ASP.NET-Thread-Pool abgerufen. Wenn Sie die Kontexterfassung mit deaktivieren ConfigureAwait(false), führt der Thread die Methode einfach direkt weiter aus. Wenn Sie die Kontexterfassung nicht deaktivieren, tritt der Thread erneut in den Anforderungskontext ein und führt die Methode weiter aus.

So ConfigureAwait(false)sparen Sie nicht einen Thread Sprung in ASP.NET; Es erspart Ihnen die erneute Eingabe des Anforderungskontexts, dies ist jedoch normalerweise sehr schnell. ConfigureAwait(false) Dies kann nützlich sein, wenn Sie versuchen, eine kleine Menge einer Anfrage parallel zu verarbeiten, TPL jedoch für die meisten dieser Szenarien besser geeignet ist.

Bei ASP.NET Web Api jedoch, wenn Ihre Anfrage in einem Thread eingeht und Sie auf eine Funktion warten und ConfigureAwait (false) aufrufen, die Sie möglicherweise in einen anderen Thread versetzen könnte, wenn Sie das Endergebnis Ihrer ApiController-Funktion zurückgeben .

Eigentlich kann man das nur tun await. Sobald Ihre asyncMethode auf trifft await, wird die Methode blockiert, aber der Thread kehrt zum Thread-Pool zurück. Wenn die Methode zum Fortfahren bereit ist, wird jeder Thread aus dem Thread-Pool entnommen und zum Fortsetzen der Methode verwendet.

Der einzige Unterschied ConfigureAwaitin ASP.NET besteht darin, ob dieser Thread beim Fortsetzen der Methode in den Anforderungskontext wechselt.

Ich habe weitere Hintergrundinformationen in meinem MSDN-ArtikelSynchronizationContext und meinem asyncIntro-Blog-Beitrag .

Stephen Cleary
quelle
23
Thread-lokaler Speicher wird von keinem Kontext übertragen. HttpContext.Currentwird von ASP.NET übertragen SynchronizationContext, das standardmäßig von Ihnen übertragen wird await, aber nicht von ContinueWith. OTOH, der Ausführungskontext (einschließlich Sicherheitsbeschränkungen) ist der in CLR über C # erwähnte Kontext und wird von beiden ContinueWithund await(auch wenn Sie ihn verwenden ConfigureAwait(false)) übertragen.
Stephen Cleary
65
Wäre es nicht großartig, wenn C # ConfigureAwait (false) in der Muttersprache unterstützen würde? So etwas wie 'warte auf' (warte auf keinen Kontext). Es ist ziemlich ärgerlich, überall einen separaten Methodenaufruf einzugeben. :)
NathanAldenSr
19
@ NathanAldenSr: Es wurde ziemlich viel diskutiert. Das Problem mit einem neuen Schlüsselwort ist, dass es ConfigureAwaiteigentlich nur dann Sinn macht, wenn Sie auf Aufgaben warten , während es awaitauf alle "erwarteten" Aktionen reagiert. Weitere in Betracht gezogene Optionen waren: Sollte das Standardverhalten den Kontext in einer Bibliothek verwerfen? Oder haben Sie eine Compilereinstellung für das Standardkontextverhalten? Beide wurden abgelehnt, weil es schwieriger ist, nur den Code zu lesen und zu sagen, was er tut.
Stephen Cleary
10
@AnshulNigam: Deshalb benötigen Controller-Aktionen ihren Kontext. Die meisten Methoden, die von den Aktionen aufgerufen werden, tun dies jedoch nicht.
Stephen Cleary
14
@JonathanRoeder: Im Allgemeinen sollten Sie ConfigureAwait(false)einen Result/ Wait-basierten Deadlock nicht vermeiden müssen , da Sie unter ASP.NET Result/ überhaupt nicht verwenden sollten Wait.
Stephen Cleary
131

Kurze Antwort auf Ihre Frage: Nein. Sie sollten nicht ConfigureAwait(false)auf Anwendungsebene anrufen .

TL; DR-Version der langen Antwort: Wenn Sie eine Bibliothek schreiben, in der Sie Ihren Verbraucher nicht kennen und keinen Synchronisationskontext benötigen (den Sie meiner Meinung nach nicht in einer Bibliothek haben sollten), sollten Sie ihn immer verwenden ConfigureAwait(false). Andernfalls können die Konsumenten Ihrer Bibliothek Deadlocks ausgesetzt sein, indem sie Ihre asynchronen Methoden blockierend nutzen. Dies hängt von der Situation ab.

Hier ist eine etwas detailliertere Erklärung zur Wichtigkeit der ConfigureAwaitMethode (ein Zitat aus meinem Blog-Beitrag):

Wenn Sie auf eine Methode mit dem Schlüsselwort await warten, generiert der Compiler für Sie eine Reihe von Code. Einer der Zwecke dieser Aktion besteht darin, die Synchronisation mit dem UI- (oder Haupt-) Thread durchzuführen. Die Schlüsselkomponente dieser Funktion ist die, SynchronizationContext.Currentdie den Synchronisationskontext für den aktuellen Thread abruft. SynchronizationContext.Currentwird abhängig von der Umgebung, in der Sie sich befinden, ausgefüllt. Die GetAwaiterMethode von Task sucht nach SynchronizationContext.Current. Wenn der aktuelle Synchronisationskontext nicht null ist, wird die Fortsetzung, die an diesen Kellner übergeben wird, an diesen Synchronisationskontext zurückgesendet.

Wenn Sie eine Methode, die die neuen asynchronen Sprachfunktionen verwendet, blockierend verwenden, kommt es zu einem Deadlock, wenn Sie über einen verfügbaren SynchronizationContext verfügen. Wenn Sie solche Methoden blockierend verwenden (auf die Task mit Wait-Methode warten oder das Ergebnis direkt aus der Result-Eigenschaft der Task entnehmen), blockieren Sie gleichzeitig den Hauptthread. Wenn die Aufgabe innerhalb dieser Methode im Threadpool abgeschlossen ist, wird die Fortsetzung aufgerufen, um sie an den Hauptthread zurückzusenden, da sie SynchronizationContext.Currentverfügbar und erfasst ist. Aber hier gibt es ein Problem: Der UI-Thread ist blockiert und Sie haben einen Deadlock!

Hier sind auch zwei großartige Artikel für Sie, die genau für Ihre Frage sind:

Schließlich gibt es ein großartiges kurzes Video von Lucian Wischik genau zu diesem Thema: Asynchrone Bibliotheksmethoden sollten die Verwendung von Task.ConfigureAwait (false) in Betracht ziehen .

Hoffe das hilft.

Tugberk
quelle
2
"Die GetAwaiter-Task-Methode sucht nach SynchronizationContext.Current. Wenn der aktuelle Synchronisationskontext nicht null ist, wird die an diesen Kellner übergebene Fortsetzung in diesen Synchronisationskontext zurückgesendet." - Ich habe den Eindruck, dass Sie versuchen zu sagen, dass Taskder Stapel läuft, um das zu bekommen SynchronizationContext, was falsch ist. Das SynchronizationContextwird vor dem Aufruf Taskdes abgerufen und dann wird der Rest des Codes auf dem SynchronizationContextif fortgesetzt, das SynchronizationContext.Currentnicht null ist.
CasperOne
1
@casperOne Ich habe vor, dasselbe zu sagen.
Tugberk
8
Sollte es nicht in der Verantwortung des Aufrufers liegen, sicherzustellen, dass SynchronizationContext.Currentklar ist / oder dass die Bibliothek innerhalb von a aufgerufen wird, Task.Run()anstatt .ConfigureAwait(false)über die gesamte Klassenbibliothek schreiben zu müssen ?
Binki
1
@binki - auf der anderen Seite: (1) Vermutlich wird eine Bibliothek in vielen Anwendungen verwendet, daher ist es kostengünstig, einmalige Anstrengungen in der Bibliothek zu unternehmen, um die Anwendung zu vereinfachen. (2) Vermutlich weiß der Bibliotheksautor, dass er Code geschrieben hat, der keinen Grund hat, den ursprünglichen Kontext fortzusetzen, den er durch diese .ConfigureAwait(false)s ausdrückt . Vielleicht wäre es für Bibliotheksautoren einfacher, wenn dies das Standardverhalten wäre, aber ich würde annehmen, dass es besser ist, es ein bisschen schwieriger zu machen, eine Bibliothek richtig zu schreiben, als es ein bisschen schwieriger zu machen, eine App richtig zu schreiben.
ToolmakerSteve
4
Warum sollte der Autor einer Bibliothek den Verbraucher verwöhnen? Wenn der Verbraucher einen Deadlock durchführen möchte, warum sollte ich ihn verhindern?
Quarkly
25

Der größte Nachteil, den ich bei der Verwendung von ConfigureAwait (false) festgestellt habe, ist, dass die Thread-Kultur auf den Systemstandard zurückgesetzt wird. Wenn Sie eine Kultur konfiguriert haben, z.

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

Wenn Sie auf einem Server hosten, dessen Kultur auf en-US eingestellt ist, werden Sie feststellen, bevor ConfigureAwait (false) CultureInfo heißt. CurrentCulture gibt en-AU zurück und nachdem Sie en-US erhalten. dh

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

Wenn Ihre Anwendung etwas tut, das eine kulturspezifische Formatierung von Daten erfordert, müssen Sie dies bei der Verwendung von ConfigureAwait (false) berücksichtigen.

Mick
quelle
27
Moderne Versionen von .NET (ich denke seit 4.6?) Verbreiten die Kultur über Threads hinweg, selbst wenn sie ConfigureAwait(false)verwendet werden.
Stephen Cleary
1
Danke für die Information. Wir verwenden in der Tat .net 4.5.2
Mick
11

Ich habe einige allgemeine Gedanken zur Implementierung von Task:

  1. Die Aufgabe ist verfügbar, aber wir sollten sie nicht verwenden using.
  2. ConfigureAwaitwurde in 4.5 eingeführt. Taskwurde in 4.0 eingeführt.
  3. .NET-Threads, die immer zum Fließen des Kontexts verwendet werden (siehe C # über das CLR-Buch), aber in der Standardimplementierung Task.ContinueWithnicht b / c wurde erkannt, dass der Kontextwechsel teuer ist und standardmäßig deaktiviert ist.
  4. Das Problem ist, dass es einem Bibliotheksentwickler egal sein sollte, ob seine Clients einen Kontextfluss benötigen oder nicht. Daher sollte er nicht entscheiden, ob der Kontext fließt oder nicht.
  5. [Später hinzugefügt] Die Tatsache, dass es keine maßgebliche Antwort und keinen richtigen Hinweis gibt und wir weiter darum kämpfen, bedeutet, dass jemand seine Arbeit nicht richtig gemacht hat.

Ich habe ein paar Beiträge zu diesem Thema, aber ich gehe davon aus, dass Sie - zusätzlich zu Tugberks netter Antwort - alle APIs asynchron machen und den Kontext idealerweise fließen lassen sollten. Da Sie asynchron arbeiten, können Sie einfach Fortsetzungen verwenden, anstatt zu warten, sodass kein Deadlock verursacht wird, da in der Bibliothek kein Warten erfolgt und Sie den Fluss beibehalten, damit der Kontext erhalten bleibt (z. B. HttpContext).

Problem ist, wenn eine Bibliothek eine synchrone API verfügbar macht, aber eine andere asynchrone API verwendet - daher müssen Sie Wait()/ Resultin Ihrem Code verwenden.

Aliostad
quelle
6
1) Sie können anrufen, Task.Disposewenn Sie möchten; Sie brauchen einfach nicht die meiste Zeit. 2) Taskwurde in .NET 4.0 als Teil der TPL eingeführt, die nicht benötigt wurde ConfigureAwait; Als sie asynchinzugefügt wurden, verwendeten sie den vorhandenen TaskTyp wieder, anstatt einen neuen zu erfinden Future.
Stephen Cleary
6
3) Sie verwechseln zwei verschiedene Arten von "Kontext". Der in C # über CLR erwähnte "Kontext" fließt immer, auch in Tasks; der "Kontext", der von gesteuert wird, ContinueWithist ein SynchronizationContextoder TaskScheduler. Diese unterschiedlichen Kontexte werden im Blog von Stephen Toub ausführlich erläutert .
Stephen Cleary
21
4) Der Bibliotheksautor muss sich nicht darum kümmern, ob seine Aufrufer den Kontextfluss benötigen, da jede asynchrone Methode unabhängig fortgesetzt wird. Wenn die Aufrufer den Kontextfluss benötigen, können sie ihn fließen lassen, unabhängig davon, ob der Bibliotheksautor ihn übertragen hat oder nicht.
Stephen Cleary
1
Zunächst scheinen Sie sich zu beschweren, anstatt die Frage zu beantworten. Und dann sprechen Sie über „den Kontext“, außer dass es in .Net verschiedene Arten von Kontext gibt und es wirklich nicht klar ist, über welchen (oder welche?) Sie sprechen. Und selbst wenn Sie selbst nicht verwirrt sind (aber ich denke, Sie sind es, ich glaube, es gibt keinen Kontext, der früher mit Threads floss , aber nicht mehr mit ContinueWith()), macht dies Ihre Antwort verwirrend zu lesen.
Svick
1
@StephenCleary ja, lib dev sollte es nicht wissen müssen, es liegt am Client. Ich dachte, ich hätte es klar gemacht, aber meine Formulierung war nicht klar.
Aliostad