Procedura consigliata per chiamare ConfigureAwait per tutto il codice lato server


561

Quando si dispone di un codice lato server (ovvero alcuni ApiController) e le funzioni sono asincrone - quindi ritornano Task<SomeObject>- è considerata la migliore pratica ogni volta che si aspettano funzioni che si chiamano ConfigureAwait(false)?

Avevo letto che è più performante in quanto non deve riportare i contesti di thread al contesto di thread originale. Tuttavia, con ASP.NET Web Api, se la tua richiesta arriva su un thread e attendi qualche funzione e chiamata ConfigureAwait(false)che potrebbero potenzialmente metterti su un thread diverso quando stai restituendo il risultato finale della tua ApiControllerfunzione.

Ho scritto un esempio di ciò di cui sto parlando di seguito:

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;
    }
}

Risposte:


628

Aggiornamento: ASP.NET Core non ha aSynchronizationContext . Se sei su ASP.NET Core, non importa se lo usi ConfigureAwait(false)o meno.

Per ASP.NET "Completo" o "Classico" o qualsiasi altra cosa, il resto di questa risposta si applica ancora.

Post originale (per ASP.NET non Core):

Questo video del team ASP.NET contiene le migliori informazioni sull'uso di asyncASP.NET.

Avevo letto che è più performante in quanto non deve riportare i contesti di thread al contesto di thread originale.

Questo è vero con le applicazioni UI, dove c'è solo un thread UI che devi "sincronizzare" di nuovo.

In ASP.NET, la situazione è un po 'più complessa. Quando un asyncmetodo riprende l'esecuzione, prende un thread dal pool di thread ASP.NET. Se si disabilita l'acquisizione del contesto tramite ConfigureAwait(false), il thread continua a eseguire direttamente il metodo. Se non si disabilita l'acquisizione del contesto, il thread rientrerà nuovamente nel contesto della richiesta e continuerà a eseguire il metodo.

Quindi ConfigureAwait(false)non ti salva un salto di thread in ASP.NET; ti salva il rientro del contesto della richiesta, ma normalmente è molto veloce. ConfigureAwait(false) potrebbe essere utile se stai tentando di eseguire una piccola quantità di elaborazione parallela di una richiesta, ma in realtà TPL si adatta meglio alla maggior parte di quegli scenari.

Tuttavia, con ASP.NET Web Api, se la richiesta arriva su un thread e si attende una funzione e si chiama ConfigureAwait (false) che potrebbe potenzialmente inserirti in un thread diverso quando si restituisce il risultato finale della funzione ApiController .

In realtà, basta farlo awaite farlo. Una volta che il asyncmetodo ha raggiunto un await, il metodo viene bloccato ma il thread ritorna al pool di thread. Quando il metodo è pronto per continuare, qualsiasi thread viene strappato dal pool di thread e utilizzato per riprendere il metodo.

L'unica differenza ConfigureAwaitin ASP.NET è se quel thread entra nel contesto della richiesta quando si riprende il metodo.

Ho ulteriori informazioni di base nel mio articolo su MSDNSynchronizationContext e nel mio asyncpost sul blog di introduzione .


23
L'archiviazione locale di thread non scorre in nessun contesto. HttpContext.Currentviene trasmesso da ASP.NET SynchronizationContext, che viene trasmesso per impostazione predefinita quando l'utente await, ma non scorre ContinueWith. OTOH, il contesto di esecuzione (comprese le restrizioni di sicurezza) è il contesto menzionato in CLR tramite C # ed è gestito da entrambi ContinueWithe await(anche se si utilizza ConfigureAwait(false)).
Stephen Cleary,

65
Non sarebbe bello se C # avesse il supporto della lingua nativa per ConfigureAwait (false)? Qualcosa come 'awaitnc' (non aspetta alcun contesto). Digitare una chiamata di metodo separata ovunque è piuttosto fastidioso. :)
NathanAldenSr

19
@NathanAldenSr: è stato discusso parecchio. Il problema con una nuova parola chiave è che in ConfigureAwaitrealtà ha senso solo quando si attendono compiti , mentre awaitagisce su qualsiasi "aspettabile". Altre opzioni considerate erano: Il comportamento predefinito dovrebbe scartare il contesto se in una libreria? O hai un'impostazione del compilatore per il comportamento di contesto predefinito? Entrambi sono stati respinti perché è più difficile leggere il codice e dire cosa fa.
Stephen Cleary,

10
@AnshulNigam: ecco perché le azioni del controller hanno bisogno del loro contesto. Ma la maggior parte dei metodi che le azioni chiamano no.
Stephen Cleary,

14
@JonathanRoeder: in generale, non dovresti ConfigureAwait(false)evitare un deadlock basato su Result/ Waitperché in ASP.NET non dovresti usare Result/ Waitin primo luogo.
Stephen Cleary,

131

Breve risposta alla tua domanda: No. Non dovresti chiamare ConfigureAwait(false)a livello di applicazione in questo modo.

TL; versione DR della lunga risposta: se stai scrivendo una libreria in cui non conosci il tuo consumatore e non hai bisogno di un contesto di sincronizzazione (cosa che non dovresti trovare in una libreria, credo), dovresti sempre usare ConfigureAwait(false). Altrimenti, i consumatori della tua libreria potrebbero affrontare deadlock consumando i tuoi metodi asincroni in modo bloccante. Questo dipende dalla situazione.

Ecco una spiegazione un po 'più dettagliata sull'importanza del ConfigureAwaitmetodo (una citazione dal mio post sul blog):

Quando aspetti un metodo con la parola chiave wait, il compilatore genera un sacco di codice per conto tuo. Uno degli scopi di questa azione è gestire la sincronizzazione con il thread dell'interfaccia utente (o principale). Il componente chiave di questa funzione è quello SynchronizationContext.Currentche ottiene il contesto di sincronizzazione per il thread corrente. SynchronizationContext.Currentviene popolato in base all'ambiente in cui ci si trova. Il GetAwaitermetodo di Task cerca SynchronizationContext.Current. Se il contesto di sincronizzazione corrente non è nullo, la continuazione che viene passata a quel cameriere verrà registrata nel contesto di sincronizzazione.

Quando si utilizza un metodo, che utilizza le nuove funzionalità del linguaggio asincrono, in modo bloccante, si otterrà un deadlock se si dispone di un SynchronizationContext disponibile. Quando si utilizzano tali metodi in modo bloccante (in attesa del metodo Task with Wait o prendendo il risultato direttamente dalla proprietà Result dell'attività), si bloccherà contemporaneamente il thread principale. Quando alla fine l'attività si completa all'interno di quel metodo nel threadpool, invocherà la continuazione per postare di nuovo sul thread principale perché SynchronizationContext.Currentè disponibile e acquisito. Ma c'è un problema qui: il thread dell'interfaccia utente è bloccato e hai un deadlock!

Inoltre, ecco due grandi articoli per te che sono esattamente per la tua domanda:

Infine, c'è un bellissimo breve video di Lucian Wischik esattamente su questo argomento: i metodi della libreria asincrona dovrebbero considerare l'utilizzo di Task.ConfigureAwait (false) .

Spero che sia di aiuto.


2
"Il metodo di attività GetAwaiter cerca SynchronizationContext.Current. Se il contesto di sincronizzazione corrente non è nullo, la continuazione che viene passata a quel cameriere verrà registrata in quel contesto di sincronizzazione." - Ho l'impressione che stai cercando di dire che Taskcammina nello stack per ottenere il SynchronizationContext, il che è sbagliato. Il SynchronizationContextviene catturato prima della chiamata al Taske quindi il resto del codice viene continuato sul SynchronizationContextse SynchronizationContext.Currentnon è nullo.
casper:

1
@casperOne Ho intenzione di dire lo stesso.
Tugberk,

8
Non dovrebbe essere responsabilità del chiamante assicurarsi che SynchronizationContext.Currentsia chiaro / o che la libreria sia chiamata all'interno di un Task.Run()invece di dover scrivere in .ConfigureAwait(false)tutta la libreria di classi?
binki,

1
@binki - d'altra parte: (1) presumibilmente una libreria viene utilizzata in molte applicazioni, quindi fare uno sforzo una volta nella libreria per renderlo più semplice sulle applicazioni è conveniente; (2) presumibilmente l'autore della biblioteca sa di avere un codice scritto che non ha motivo di richiedere di continuare sul contesto originale, che egli esprime da questi .ConfigureAwait(false). Forse sarebbe più facile per gli autori delle biblioteche se quello fosse il comportamento predefinito, ma presumo che rendere un po 'più difficile scrivere una biblioteca correttamente sia meglio che rendere un po' più difficile scrivere un'app correttamente.
ToolmakerSteve

4
Perché l'autore di una biblioteca dovrebbe ingannare il consumatore? Se il consumatore desidera un deadlock, perché dovrei prevenirlo?
Quarkly,

25

Il più grande svantaggio che ho riscontrato usando ConfigureAwait (false) è che la cultura del thread è ripristinata al valore predefinito di sistema. Se hai configurato una cultura, ad esempio ...

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

e stai ospitando su un server la cui cultura è impostata su en-US, quindi troverai prima che ConfigureAwait (false) sia chiamato CultureInfo.CurrentCulture restituirà en-AU e dopo che otterrai en-US. vale a dire

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

Se l'applicazione sta eseguendo operazioni che richiedono una formattazione dei dati specifica per la cultura, è necessario prestare attenzione a ciò quando si utilizza ConfigureAwait (false).


27
Le versioni moderne di .NET (credo dalla 4.6?) Propagheranno la cultura tra i thread, anche se ConfigureAwait(false)viene utilizzata.
Stephen Cleary,

1
Grazie per le informazioni. Stiamo effettivamente usando .net 4.5.2
Mick il

11

Ho alcune considerazioni generali sull'implementazione di Task:

  1. L'attività è usa e getta ma non dovremmo usarla using.
  2. ConfigureAwaitè stato introdotto in 4.5. Taskè stato introdotto in 4.0.
  3. I thread .NET sono sempre stati utilizzati per far fluire il contesto (vedi C # tramite il libro CLR) ma nell'implementazione di default Task.ContinueWithnon lo fanno b / c è stato realizzato che il cambio di contesto è costoso ed è disattivato per impostazione predefinita.
  4. Il problema è che uno sviluppatore di librerie non dovrebbe preoccuparsi se i suoi client hanno bisogno del flusso di contesto o no, quindi non dovrebbe decidere se scorrere il contesto o meno.
  5. [Aggiunto più tardi] Il fatto che non ci sia una risposta autorevole e un riferimento adeguato e continuiamo a combattere su questo significa che qualcuno non ha fatto bene il proprio lavoro.

Ho alcuni post sull'argomento ma la mia opinione - oltre alla bella risposta di Tugberk - è che dovresti rendere asincrone tutte le API e idealmente scorrere il contesto. Dato che stai facendo un asincrono, puoi semplicemente usare le continuazioni invece di aspettare, quindi non ci sarà alcun deadlock poiché nessuna attesa viene eseguita nella libreria e mantieni il flusso in modo che il contesto sia preservato (come HttpContext).

Il problema è quando una libreria espone un'API sincrona ma utilizza un'altra API asincrona, quindi è necessario utilizzare Wait()/ Resultnel codice.


6
1) Puoi chiamare Task.Disposese vuoi; semplicemente non hai bisogno per la stragrande maggioranza delle volte. 2) è Taskstato introdotto in .NET 4.0 come parte del TPL, che non era necessario ConfigureAwait; quando è asyncstato aggiunto, hanno riutilizzato il Tasktipo esistente invece di inventarne uno nuovo Future.
Stephen Cleary,

6
3) Stai confondendo due diversi tipi di "contesto". Il "contesto" menzionato in C # tramite CLR scorre sempre, anche in Tasks; il "contesto" controllato da ContinueWithè un SynchronizationContexto TaskScheduler. Questi diversi contesti sono spiegati in dettaglio nel blog di Stephen Toub .
Stephen Cleary,

21
4) L'autore della biblioteca non ha bisogno di preoccuparsi se i suoi chiamanti hanno bisogno del flusso di contesto, perché ogni metodo asincrono riprende in modo indipendente. Pertanto, se i chiamanti hanno bisogno del flusso di contesto, possono farlo scorrere, indipendentemente dal fatto che l'autore della libreria lo abbia fatto scorrere o meno.
Stephen Cleary,

1
All'inizio sembra che ti lamenti invece di rispondere alla domanda. E poi stai parlando di "contesto", tranne che ci sono diversi tipi di contesto in .Net e non è davvero chiaro di quale (o di quelli?) Stai parlando. E anche se non sei confuso (ma penso di esserlo, credo che non vi sia alcun contesto che scorreva con Threads, ma non più con ContinueWith()), questo rende la tua risposta confusa da leggere.
svick,

1
@StephenCleary sì, non è necessario che lo sviluppatore di librerie lo sappia, dipende dal client. Pensavo di averlo chiarito, ma il mio fraseggio non era chiaro.
Aliostad,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.