Prática recomendada para chamar o ConfigureAwait para todo o código do servidor

561

Quando você tem código do lado do servidor (ou seja, alguns ApiController ) e suas funções são assíncronas - então elas retornam Task<SomeObject>-, é considerada uma boa prática que sempre que você aguarde as funções que chama ConfigureAwait(false)?

Eu tinha lido que é mais eficiente, pois não precisa alternar os contextos de thread para o contexto de thread original. No entanto, com o ASP.NET Web Api, se o seu pedido estiver chegando em um thread, e você aguardar alguma função e chamarConfigureAwait(false) que possam potencialmente colocá-lo em um segmento diferente quando você retornar o resultado final da sua ApiControllerfunção.

Eu digitei um exemplo do que estou falando abaixo:

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
fonte

Respostas:

628

Atualização: o ASP.NET Core não possui umSynchronizationContext . Se você estiver no ASP.NET Core, não importa se você usa ConfigureAwait(false)ou não.

Para o ASP.NET "Completo" ou "Clássico" ou o que for, o restante desta resposta ainda se aplica.

Postagem original (para ASP.NET não Core):

Este vídeo da equipe do ASP.NET tem as melhores informações sobre o uso asyncno ASP.NET.

Eu tinha lido que é mais eficiente, pois não precisa alternar os contextos de thread para o contexto de thread original.

Isso ocorre com aplicativos de interface do usuário, onde há apenas um thread da interface do usuário no qual você precisa "sincronizar".

No ASP.NET, a situação é um pouco mais complexa. Quando um asyncmétodo retoma a execução, ele pega um thread do pool de threads do ASP.NET. Se você desativar a captura de contexto usando ConfigureAwait(false), o encadeamento continuará executando o método diretamente. Se você não desativar a captura de contexto, o encadeamento entrará novamente no contexto de solicitação e continuará a executar o método.

Portanto ConfigureAwait(false), você não economiza um salto de thread no ASP.NET; ele economiza a reinserção do contexto da solicitação, mas isso normalmente é muito rápido. ConfigureAwait(false) pode ser útil se você estiver tentando fazer uma pequena quantidade de processamento paralelo de uma solicitação, mas realmente o TPL é mais adequado para a maioria desses cenários.

No entanto, com o ASP.NET Web Api, se sua solicitação estiver chegando em um thread, e você aguardar alguma função e chamar ConfigureAwait (false) que poderia potencialmente colocá-lo em um segmento diferente quando você retornar o resultado final da função ApiController .

Na verdade, apenas fazer um awaitpode fazer isso. Depois que seu asyncmétodo atinge um await, o método é bloqueado, mas o thread retorna ao pool de threads. Quando o método está pronto para continuar, qualquer encadeamento é capturado do conjunto de encadeamentos e usado para retomar o método.

A única diferença ConfigureAwait faz no ASP.NET é se esse thread entra no contexto da solicitação ao retomar o método.

Tenho mais informações básicas no meu artigo do MSDNSynchronizationContext e na asyncpublicação do meu blog de introdução .

Stephen Cleary
fonte
23
O armazenamento local do thread não é transmitido por nenhum contexto. HttpContext.Currenté transmitido pelo ASP.NET SynchronizationContext, que é transmitido por padrão quando você await, mas não é transmitido por ContinueWith. OTOH, o contexto de execução (incluindo restrições de segurança) é o contexto mencionado no CLR via C # e é transmitido por ambos ContinueWithe await(mesmo se você usar ConfigureAwait(false)).
Stephen Cleary
65
Não seria ótimo se o C # tivesse suporte de idioma nativo para o ConfigureAwait (false)? Algo como 'waititnc' (não espera contexto). Digitar uma chamada de método separada em qualquer lugar é bastante irritante. :)
NathanAldenSr
19
@ NathanAldenSr: Foi discutido bastante. O problema com uma nova palavra-chave é que, ConfigureAwaitna verdade, só faz sentido quando você aguarda tarefas , enquanto awaitatua sobre qualquer "aguardável". Outras opções consideradas foram: O comportamento padrão deve descartar o contexto se estiver em uma biblioteca? Ou tem uma configuração de compilador para o comportamento de contexto padrão? Ambos foram rejeitados porque é mais difícil ler o código e dizer o que ele faz.
Stephen Cleary
10
@AnshulNigam: É por isso que as ações do controlador precisam de seu contexto. Mas a maioria dos métodos chamados pelas ações não.
Stephen Cleary
14
@ JonathanRoeder: De um modo geral, você não deve ConfigureAwait(false)evitar um impasse baseado em Result/ Waitporque no ASP.NET você não deveria estar usando Result/ Waitem primeiro lugar.
Stephen Cleary
131

Resposta breve à sua pergunta: Não. Você não deve ligar ConfigureAwait(false)para o nível do aplicativo dessa maneira.

Versão TL; DR da resposta longa: se você estiver escrevendo uma biblioteca em que não conhece seu consumidor e não precisa de um contexto de sincronização (que não deveria em uma biblioteca que acredito), sempre use ConfigureAwait(false). Caso contrário, os consumidores da sua biblioteca podem enfrentar conflitos, consumindo seus métodos assíncronos de maneira bloqueadora. Isso depende da situação.

Aqui está uma explicação um pouco mais detalhada sobre a importância do ConfigureAwaitmétodo (uma citação da minha postagem no blog):

Quando você está aguardando um método com a palavra-chave wait, o compilador gera um monte de código em seu nome. Um dos objetivos desta ação é lidar com a sincronização com o thread da interface do usuário (ou principal). O componente principal desse recurso é oSynchronizationContext.Current que obtém o contexto de sincronização para o encadeamento atual. SynchronizationContext.Currenté preenchido, dependendo do ambiente em que você está. O GetAwaitermétodo de Tarefa procura SynchronizationContext.Current. Se o contexto de sincronização atual não for nulo, a continuação que é transmitida para o garçom será postada novamente nesse contexto de sincronização.

Ao consumir um método, que usa os novos recursos de linguagem assíncrona, de maneira bloqueadora, você terminará com um impasse se tiver um SynchronizationContext disponível. Ao consumir esses métodos de maneira bloqueadora (aguardando o método Tarefa com Espera ou obtendo o resultado diretamente da propriedade Resultado da Tarefa), você bloqueará o encadeamento principal ao mesmo tempo. Quando, eventualmente, a tarefa for concluída dentro desse método no conjunto de encadeamentos, ela invocará a continuação para postar de volta no encadeamento principal, porque SynchronizationContext.Currentestá disponível e capturado. Mas há um problema aqui: o thread da interface do usuário está bloqueado e você tem um impasse!

Além disso, aqui estão dois excelentes artigos para você, exatamente para sua pergunta:

Por fim, há um ótimo vídeo curto de Lucian Wischik exatamente sobre este tópico: Os métodos da biblioteca assíncrona devem considerar o uso de Task.ConfigureAwait (false) .

Espero que isto ajude.

tugberk
fonte
2
"O método GetAwaiter da tarefa procura SynchronizationContext.Current. Se o contexto atual de sincronização não for nulo, a continuação que é transmitida a esse garçom será postada novamente nesse contexto de sincronização." - Estou tendo a impressão de que você está tentando dizer que Taskcaminha na pilha para obter o SynchronizationContextque está errado. O SynchronizationContexté capturado antes da chamada para Taske, em seguida, o restante do código continua no SynchronizationContextif SynchronizationContext.Currentnão for nulo.
usar o seguinte código
1
@casperOne Eu pretendi dizer o mesmo.
precisa saber é o seguinte
8
Não deveria ser responsabilidade do chamador garantir que isso SynchronizationContext.Currentesteja claro / ou que a biblioteca seja chamada dentro de um em Task.Run()vez de precisar escrever em .ConfigureAwait(false)toda a biblioteca de classes?
binki
1
@binki - por outro lado: (1) presumivelmente, uma biblioteca é usada em muitos aplicativos; portanto, fazer um esforço único na biblioteca para facilitar o uso dos aplicativos é econômico; (2) presumivelmente, o autor da biblioteca sabe que ele escreveu um código que não tem motivos para exigir a continuação do contexto original, que ele expressa por esses .ConfigureAwait(false)s. Talvez fosse mais fácil para os autores da biblioteca se esse fosse o comportamento padrão, mas eu presumo que tornar um pouco mais difícil escrever corretamente uma biblioteca é melhor do que tornar um pouco mais difícil escrever um aplicativo corretamente.
Home
4
Por que o autor de uma biblioteca deve mimar o consumidor? Se o consumidor deseja um impasse, por que devo impedi-lo?
Quarkly 25/07/19
25

A maior desvantagem que encontrei ao usar o ConfigureAwait (false) é que a cultura do encadeamento é revertida para o padrão do sistema. Se você configurou uma cultura, por exemplo ...

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

e você estiver hospedando em um servidor cuja cultura está definida como en-US, você encontrará antes que ConfigureAwait (false) seja chamado CultureInfo.CurrentCulture retornará en-AU e depois que você for en-US. ie

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

Se o seu aplicativo estiver fazendo algo que exija formatação de dados específica da cultura, será necessário estar ciente disso ao usar o ConfigureAwait (false).

Mick
fonte
27
Versões modernas do .NET (acho que desde 4.6?) Propagarão a cultura através de threads, mesmo que ConfigureAwait(false)sejam usadas.
Stephen Cleary
1
Obrigado pela informação. Na verdade, estamos usando .net 4.5.2 #
Mick
11

Tenho algumas considerações gerais sobre a implementação de Task:

  1. Tarefa é descartável ainda estamos não deveria usar using.
  2. ConfigureAwaitfoi introduzido no 4.5. Taskfoi introduzido no 4.0.
  3. Os threads do .NET sempre eram usados ​​para fluir o contexto (consulte C # via manual do CLR), mas na implementação padrão Task.ContinueWitheles não são verdadeiros, foi percebido que a troca de contexto é cara e é desativada por padrão.
  4. O problema é que um desenvolvedor de biblioteca não deve se importar se seus clientes precisam ou não de fluxo de contexto; portanto, não deve decidir se deve ou não fluir o contexto.
  5. [Adicionado mais tarde] O fato de não haver resposta autorizada e referência adequada e continuamos lutando com isso significa que alguém não fez seu trabalho corretamente.

Eu tenho alguns posts sobre o assunto, mas minha opinião - além da boa resposta de Tugberk - é que você deve ativar todas as APIs assíncronas e idealmente fluir o contexto.Como você está fazendo a assíncrona, você pode simplesmente usar continuações em vez de esperar, para que nenhum impasse seja causado, já que nenhuma espera é feita na biblioteca e você mantém o fluxo para que o contexto seja preservado (como HttpContext).

O problema é quando uma biblioteca expõe uma API síncrona, mas usa outra API assíncrona - portanto, você precisa usar Wait()/ Resultno seu código.

Aliostad
fonte
6
1) Você pode ligar Task.Disposese quiser; você simplesmente não precisa na grande maioria das vezes. 2) Taskfoi introduzido no .NET 4.0 como parte do TPL, o que não era necessário ConfigureAwait; quando asyncfoi adicionado, eles reutilizaram o Tasktipo existente em vez de inventar um novo Future.
Stephen Cleary
6
3) Você está confundindo dois tipos diferentes de "contexto". O "contexto" mencionado em C # via CLR é sempre transmitido, mesmo em Tasks; o "contexto" controlado por ContinueWithé um SynchronizationContextou TaskScheduler. Esses diferentes contextos são explicados em detalhes no blog de Stephen Toub .
Stephen Cleary
21
4) O autor da biblioteca não precisa se preocupar se seus chamadores precisam do fluxo de contexto, porque cada método assíncrono é retomado independentemente. Portanto, se os chamadores precisarem do fluxo de contexto, eles poderão fazê-lo, independentemente do autor da biblioteca ou não.
Stephen Cleary
1
No começo, você parece estar reclamando em vez de responder à pergunta. E então você está falando sobre “o contexto”, exceto que existem vários tipos de contexto no .Net e realmente não está claro de qual deles (ou quais?) Você está falando. E mesmo que você não esteja confuso (mas acho que sim, acredito que não exista um contexto que flua com Threads, mas que não flua mais com ContinueWith()), isso torna sua resposta confusa de ler.
svick
1
@StephenCleary sim, o lib dev não precisa saber, é responsabilidade do cliente. Eu pensei que tinha deixado claro, mas meu fraseado não estava claro.
Aliostad 22/11/2012