Olá Habr! Apresento a você a tradução do artigo de
perguntas frequentes do ConfigureAwait de Stephen Taub.

Async
/
await
adicionado ao .NET há mais de sete anos. Essa decisão teve um impacto significativo, não apenas no ecossistema .NET - ela também se reflete em muitos outros idiomas e estruturas. Atualmente, muitas melhorias no .NET foram implementadas em termos de construções de linguagem adicionais usando assincronia, APIs com suporte a assincronia foram implementadas, melhorias fundamentais foram feitas na infraestrutura, devido à qual
async
/
await
funciona como um relógio (em particular, os recursos de desempenho e diagnóstico foram aprimorados no .NET Core).
ConfigureAwait
é um aspecto do
async
/
await
que continua levantando questões. Espero poder responder a muitos deles. Tentarei tornar este artigo legível do começo ao fim e, ao mesmo tempo, executá-lo no estilo de respostas a perguntas frequentes (FAQ), para que possa ser referenciado no futuro.
Para realmente lidar com o
ConfigureAwait
, voltaremos um pouco.
O que é um SynchronizationContext?
De acordo com a documentação de
System.Threading.SynchronizationContext "Fornece funcionalidade básica para distribuir o contexto de sincronização em vários modelos de sincronização". Esta definição não é totalmente óbvia.
Em 99,9% dos casos, o
SynchronizationContext
usado simplesmente como um tipo com o método virtual
Post
, que aceita o delegado para execução assíncrona (há outros membros virtuais no
SynchronizationContext
, mas eles são menos comuns e não serão discutidos neste artigo). O método
Post
do tipo base literalmente
simplesmente chama ThreadPool.QueueUserWorkItem
para executar de forma assíncrona o delegado fornecido. Os tipos derivados substituem a
Post
para que o delegado possa executar no lugar certo, na hora certa.
Por exemplo, o Windows Forms possui um
tipo derivado de SynchronizationContext que redefine
Post
para tornar o equivalente a
Control.BeginInvoke
. Isso significa que qualquer chamada para esse método
Post
resultará em uma chamada para o delegado posteriormente no thread associado ao Control correspondente - o chamado thread da interface do usuário. No coração do Windows Forms está o processamento de mensagens do Win32. O loop da mensagem é executado em um thread da interface do usuário que apenas aguarda o processamento de novas mensagens. Essas mensagens são acionadas pelo movimento do mouse, cliques, entrada do teclado, eventos do sistema disponíveis para execução pelos delegados, etc. Portanto, se você tiver uma instância
SynchronizationContext
para um thread da interface do usuário em um aplicativo Windows Forms, deve passar o delegado para o método
Post
para executar uma operação nele.
O Windows Presentation Foundation (WPF) também possui um
tipo derivado de
SynchronizationContext
com um método
Post
substituído que "direciona" de maneira semelhante o delegado para o fluxo da interface do usuário (usando
Dispatcher.BeginInvoke
), com controle do Dispatcher do WPF, não com o controle de formulários do Windows.
E o Windows RunTime (WinRT) possui seu próprio
tipo derivado de
SynchronizationContext
, que também coloca o delegado na
CoreDispatcher
threads da interface do usuário usando o
CoreDispatcher
.
É isso que está por trás da frase "executar delegado no thread da interface do usuário". Você também pode implementar seu
SynchronizationContext
com o método
Post
e alguma implementação. Por exemplo, não preciso me preocupar com qual thread o delegado está executando, mas quero ter certeza de que qualquer método
Post
delegado no meu
SynchronizationContext
executado com algum grau de paralelismo limitado. Você pode implementar um
SynchronizationContext
personalizado da seguinte maneira:
internal sealed class MaxConcurrencySynchronizationContext : SynchronizationContext { private readonly SemaphoreSlim _semaphore; public MaxConcurrencySynchronizationContext(int maxConcurrencyLevel) => _semaphore = new SemaphoreSlim(maxConcurrencyLevel); public override void Post(SendOrPostCallback d, object state) => _semaphore.WaitAsync().ContinueWith(delegate { try { d(state); } finally { _semaphore.Release(); } }, default, TaskContinuationOptions.None, TaskScheduler.Default); public override void Send(SendOrPostCallback d, object state) { _semaphore.Wait(); try { d(state); } finally { _semaphore.Release(); } } }
A estrutura xUnit tem uma
implementação semelhante
do SynchronizationContext. Aqui é usado para reduzir a quantidade de código associado a testes paralelos.
As vantagens aqui são as mesmas de qualquer abstração: é fornecida uma única API que pode ser usada para enfileirar o delegado para execução da maneira que o programador desejar, sem precisar conhecer os detalhes da implementação. Suponha que eu escreva uma biblioteca na qual precise trabalhar e, em seguida, coloque um delegado na fila de volta ao contexto original. Para fazer isso, preciso capturar seu
SynchronizationContext
e, quando concluir o necessário, terei que chamar o método
Post
desse contexto e passá-lo para um delegado para execução. Não preciso saber que, para o Windows Forms, você precisa assumir o
Control
e usar o
BeginInvoke
, pois o WPF usa o
BeginInvoke
do
Dispatcher
ou, de alguma forma, obtém o contexto e a fila do xUnit. Tudo o que preciso fazer é pegar o
SynchronizationContext
atual e usá-lo mais tarde. Para fazer isso, o
SynchronizationContext
tem uma propriedade
Current
. Isso pode ser implementado da seguinte maneira:
public void DoWork(Action worker, Action completion) { SynchronizationContext sc = SynchronizationContext.Current; ThreadPool.QueueUserWorkItem(_ => { try { worker(); } finally { sc.Post(_ => completion(), null); } }); }
Você pode definir um contexto especial na propriedade
Current
usando o método
SynchronizationContext.SetSynchronizationContext
.
O que é um Agendador de Tarefas?
SynchronizationContext
é uma abstração comum para o "planejador". Algumas estruturas usam suas próprias abstrações para isso, e
System.Threading.Tasks
não
System.Threading.Tasks
exceção. Quando há delegados na
Task
que podem ser enfileirados e executados, eles são associados ao
System.Threading.Tasks.TaskScheduler
. Também existe um método
Post
virtual para enfileirar um delegado (uma chamada de delegado é implementada usando mecanismos padrão),
TaskScheduler
fornece um método abstrato de
QueueTask
(uma chamada de tarefa é implementada usando o método
ExecuteTask
).
O planejador padrão que retorna
TaskScheduler.Default
é um pool de encadeamentos. No
TaskScheduler
, também é possível obter e substituir métodos para definir a hora e o local da chamada de
Task
. Por exemplo, as bibliotecas principais incluem o tipo
System.Threading.Tasks.ConcurrentExclusiveSchedulerPair
. Uma instância dessa classe fornece duas propriedades de
TaskScheduler
:
ExclusiveScheduler
e
ConcurrentScheduler
. As tarefas agendadas no
ConcurrentScheduler
podem ser executadas em paralelo, mas levando em consideração a restrição definida pelo
ConcurrentExclusiveSchedulerPair
quando ele é criado (semelhante ao
MaxConcurrencySynchronizationContext
). Nenhuma tarefa
ConcurrentScheduler
será executada se a tarefa for executada no
ExclusiveScheduler
e apenas uma tarefa exclusiva tiver permissão para executar por vez. Esse comportamento é muito semelhante a um bloqueio de leitura / gravação.
Como
SynchronizationContext
,
TaskScheduler
tem uma propriedade
Current
que retorna o atual
TaskScheduler
. No entanto, diferentemente de
SynchronizationContext
, ele não possui um método para definir o agendador atual. Em vez disso, o planejador está associado à tarefa atual. Portanto, por exemplo, este programa exibirá
True
, pois o lambda usado em
StartNew
é executado na instância
ExclusiveScheduler
de
ConcurrentExclusiveSchedulerPair
e
TaskScheduler.Current
instalado neste agendador:
using System; using System.Threading.Tasks; class Program { static void Main() { var cesp = new ConcurrentExclusiveSchedulerPair(); Task.Factory.StartNew(() => { Console.WriteLine(TaskScheduler.Current == cesp.ExclusiveScheduler); }, default, TaskCreationOptions.None, cesp.ExclusiveScheduler).Wait(); } }
Curiosamente,
TaskScheduler
fornece um método estático
FromCurrentSynchronizationContext
. O método cria um novo
TaskScheduler
e
TaskScheduler
as tarefas para execução no contexto
SynchronizationContext.Current
retornado usando o método
Post
.
Como o SynchronizationContext e o TaskScheduler estão relacionados a aguardar?
Digamos que você precise escrever um aplicativo de interface do usuário com um botão. Pressionar o botão inicia o download de texto do site e o define para o botão
Content
. O botão deve estar acessível apenas na interface do usuário do fluxo em que está localizado; portanto, quando carregamos a data e a hora com sucesso e queremos colocá-las no
Content
do botão, precisamos fazer isso a partir do fluxo que tem controle sobre ele. Se essa condição não for atendida, obteremos uma exceção:
System.InvalidOperationException: ' , .'
Podemos usar manualmente o
SynchronizationContext
para definir o
Content
no contexto de origem, por exemplo, através do
TaskScheduler
:
private static readonly HttpClient s_httpClient = new HttpClient(); private void downloadBtn_Click(object sender, RoutedEventArgs e) { s_httpClient.GetStringAsync("http://example.com/currenttime").ContinueWith(downloadTask => { downloadBtn.Content = downloadTask.Result; }, TaskScheduler.FromCurrentSynchronizationContext()); }
E podemos usar o
SynchronizationContext
diretamente:
private static readonly HttpClient s_httpClient = new HttpClient(); private void downloadBtn_Click(object sender, RoutedEventArgs e) { SynchronizationContext sc = SynchronizationContext.Current; s_httpClient.GetStringAsync("http://example.com/currenttime").ContinueWith(downloadTask => { sc.Post(delegate { downloadBtn.Content = downloadTask.Result; }, null); }); }
No entanto, essas duas opções explicitamente usam um retorno de chamada. Em vez disso, podemos usar
async
/
await
:
private static readonly HttpClient s_httpClient = new HttpClient(); private async void downloadBtn_Click(object sender, RoutedEventArgs e) { string text = await s_httpClient.GetStringAsync("http://example.com/currenttime"); downloadBtn.Content = text; }
Tudo isso "simplesmente funciona" e configura o
Content
com êxito no thread da interface do usuário, pois no caso da versão implementada manualmente acima, por padrão, aguardar uma tarefa refere-se a
SynchronizationContext.Current
e
TaskScheduler.Current
. Quando você "espera" algo em C #, o compilador converte o código para pesquisa (chamando
GetAwaiter
) o "esperado" (neste caso, Tarefa) em "aguardando" (
TaskAwaiter
). A "espera" é responsável por anexar um retorno de chamada (geralmente chamado de "continuação") que retorna à máquina de estado quando a espera é concluída. Ele implementa isso usando o contexto / agendador que ele capturou durante o registro de retorno de chamada. Vamos otimizar e configurar um pouco, é algo como isto:
object scheduler = SynchronizationContext.Current; if (scheduler is null && TaskScheduler.Current != TaskScheduler.Default) { scheduler = TaskScheduler.Current; }
Aqui, primeiro é verificado se o
SynchronizationContext
e, se não, se
TaskScheduler
não padrão. Se houver, quando o retorno de chamada estiver pronto para a chamada, o agendador capturado será usado; caso contrário, o retorno de chamada será executado como parte da operação que conclui a tarefa esperada.
O que o ConfigureAwait faz (false)
O método
ConfigureAwait
não é especial: não é reconhecido de nenhuma maneira específica pelo compilador ou pelo tempo de execução. Este é um método normal que retorna uma estrutura (
ConfiguredTaskAwaitable
- quebra a tarefa original) e aceita um valor booleano. Lembre-se de que
await
pode ser usado com qualquer tipo que implemente o padrão correto. Se outro tipo for retornado, isso significa que quando o compilador obtém acesso ao método
GetAwaiter
(parte do padrão) das instâncias, mas o faz do tipo retornado do
ConfigureAwait
, e não da tarefa diretamente. Isso permite alterar o comportamento de
await
deste garçom especial.
Aguardar o tipo retornado pelo
ConfigureAwait(continueOnCapturedContext: false)
vez de aguardar a
Task
afeta diretamente a implementação de captura de contexto / planejador discutida acima. A lógica se torna algo como isto:
object scheduler = null; if (continueOnCapturedContext) { scheduler = SynchronizationContext.Current; if (scheduler is null && TaskScheduler.Current != TaskScheduler.Default) { scheduler = TaskScheduler.Current; } }
Em outras palavras, especificar
false
, mesmo se houver um contexto ou planejador atual para o retorno de chamada, implica que ele está ausente.
Por que preciso usar o ConfigureAwait (false)?
ConfigureAwait(continueOnCapturedContext: false)
usado para impedir que o retorno de chamada seja forçado a chamar no contexto ou planejador de origem. Isso nos dá várias vantagens:
Melhoria de desempenho. Há uma sobrecarga na fila de um retorno de chamada, diferente da chamada, pois isso requer trabalho adicional (e geralmente alocação adicional). Além disso, não podemos usar a otimização em tempo de execução (podemos otimizar mais quando sabemos exatamente como o retorno de chamada será chamado, mas se for passado para uma implementação arbitrária da abstração, às vezes isso impõe restrições). Para seções muito carregadas, mesmo os custos adicionais da verificação do
SynchronizationContext
atual e do
TaskScheduler
atual (os quais implicam acesso aos fluxos estáticos) podem aumentar significativamente a sobrecarga. Se o código após
await
não exigir execução no contexto original, usando o
ConfigureAwait(false)
, todas essas despesas poderão ser evitadas, pois não precisam ser enfileiradas desnecessariamente, podem usar todas as otimizações disponíveis e também evitar o acesso desnecessário à estática do fluxo.
Prevenção de deadlock. Considere o método de biblioteca que
await
usos para baixar algo da rede. Você chama esse método e bloqueia de forma síncrona, aguardando a conclusão da tarefa, por exemplo, usando
.Wait()
ou
.Result
ou
.GetAwaiter()
.GetResult()
. Agora considere o que acontece se a chamada ocorrer quando o
SynchronizationContext
atual limitar o número de operações a 1 usando explicitamente
MaxConcurrencySynchronizationContext
, ou implicitamente, se for um contexto com um único encadeamento a ser usado (por exemplo, um encadeamento da interface do usuário). Assim, você chama o método em um único encadeamento e o bloqueia, aguardando a conclusão da operação. O download começa na rede e aguarda sua conclusão. Por padrão, aguardar uma
Task
capturará o
SynchronizationContext
atual (e, nesse caso) e, quando o download da rede for concluído, ele será colocado na fila de volta para o retorno de chamada
SynchronizationContext
, que chamará o restante da operação. Mas o único segmento que pode lidar com o retorno de chamada na fila está bloqueado no momento enquanto aguarda a conclusão da operação. E essa operação não será concluída até que o retorno de chamada seja processado. Impasse! Pode ocorrer mesmo quando o contexto não limita a simultaneidade a 1, mas os recursos são limitados de alguma forma. Imagine a mesma situação, apenas com o valor 4 para
MaxConcurrencySynchronizationContext
. Em vez de executar a operação uma vez, colocamos na fila 4 chamadas para o contexto. Cada chamada é feita e bloqueada antes de sua conclusão. Todos os recursos agora estão bloqueados aguardando a conclusão dos métodos assíncronos, e a única coisa que lhes permitirá concluir é se os retornos de chamada são processados por esse contexto. No entanto, ele já está totalmente ocupado. Impasse novamente. Se o método da biblioteca usasse o
ConfigureAwait(false)
, ele não colocaria na fila o retorno de chamada no contexto original, o que evitaria scripts de conflito.
Preciso usar o ConfigureAwait (true)?
Não, a menos que você precise indicar explicitamente que não está usando o
ConfigureAwait(false)
(por exemplo, para ocultar avisos de análise estática, etc.).
ConfigureAwait(true)
não faz nada significativo. Se você comparar a
await task
e
await task.ConfigureAwait(true)
, eles serão funcionalmente idênticos. Portanto, se
ConfigureAwait(true)
presente no código, ele poderá ser excluído sem consequências negativas.
O método
ConfigureAwait
assume um valor booleano, pois em algumas situações pode ser necessário passar uma variável para controlar a configuração. Mas em 99% dos casos, o valor está definido como false,
ConfigureAwait(false)
.
Quando usar o ConfigureAwait (false)?
Depende se você implementa o código no nível do aplicativo ou o código da biblioteca de uso geral.
Ao escrever aplicativos, geralmente é necessário algum comportamento padrão. Se o modelo / ambiente do aplicativo (por exemplo, Windows Forms, WPF, ASP.NET Core) publica um
SynchronizationContext
especial, quase certamente há uma boa razão para isso: significa que o código permite que você cuide do contexto de sincronização para interagir adequadamente com o modelo / ambiente do aplicativo. Por exemplo, se você gravar um manipulador de eventos em um aplicativo Windows Forms, um teste no xUnit ou um código em um controlador ASP.NET MVC, independentemente de o modelo do aplicativo ter publicado um
SynchronizationContext
, será necessário usar
SynchronizationContext
se houver. Isso significa que, se o
ConfigureAwait(true)
e o
await
, retornos de chamada / continuações são enviados de volta ao contexto original - tudo corre como deveria. A partir daqui, você pode formular uma regra geral:
se você escrever código no nível do aplicativo, não use o ConfigureAwait(false)
. Vamos voltar ao manipulador de cliques:
private static readonly HttpClient s_httpClient = new HttpClient(); private async void downloadBtn_Click(object sender, RoutedEventArgs e) { string text = await s_httpClient.GetStringAsync("http://example.com/currenttime"); downloadBtn.Content = text; }
downloadBtn.Content = text
deve ser executado no contexto original. Se o código violou essa regra e usou
ConfigureAwait (false)
, não será usado no contexto original:
private static readonly HttpClient s_httpClient = new HttpClient(); private async void downloadBtn_Click(object sender, RoutedEventArgs e) { string text = await s_httpClient.GetStringAsync("http://example.com/currenttime").ConfigureAwait(false);
isso levará a um comportamento inadequado. O mesmo se aplica ao código em um aplicativo ASP.NET clássico que depende do
HttpContext.Current
. Ao usar o
ConfigureAwait(false)
tentativa subseqüente de usar a função
Context.Current
provavelmente
Context.Current
problemas.
É isso que distingue as bibliotecas de uso geral. Eles são universais em parte porque não se importam com o ambiente em que são usados. Você pode usá-los a partir de um aplicativo Web, de um aplicativo cliente ou de um teste - não importa, pois o código da biblioteca é independente do modelo de aplicativo no qual ele pode ser usado. Agnóstico também significa que a biblioteca não fará nada para interagir com o modelo de aplicativo, por exemplo, não terá acesso aos controles da interface do usuário, porque a biblioteca de uso geral não sabe nada sobre eles. Como não há necessidade de executar o código em nenhum ambiente específico, podemos evitar forçar continuações / retornos de chamada a serem forçados ao contexto original, e fazemos isso usando o
ConfigureAwait(false)
, que nos oferece vantagens de desempenho e aumenta a confiabilidade. Isso nos leva ao seguinte:
se você estiver escrevendo um código de biblioteca de uso geral, use ConfigureAwait(false)
. É por isso que todos (ou quase todos) aguardam nas bibliotecas de tempo de execução do .NET Core usam o ConfigureAwait (false); Com algumas exceções, que são os erros mais prováveis, eles serão corrigidos.
Por exemplo, o PR corrigido nenhuma chamada ConfigureAwait(false)
em HttpClient
.Obviamente, isso não faz sentido em todos os lugares. Por exemplo, uma das grandes exceções (ou pelo menos casos em que você precisa pensar sobre isso) nas bibliotecas de uso geral é quando essas bibliotecas possuem APIs que aceitam delegados a uma chamada. Em tais casos, a biblioteca recebe um código potencial da camada de aplicação do chamador, o que torna estes pressupostos para a biblioteca de "propósito geral" altamente controversa Imagine, por exemplo, uma versão assíncrona de Onde método LINQ :. public static async IAsyncEnumerable<T> WhereAsync(this IAsyncEnumerable<T> source, Func<T, bool> predicate)
. Deve predicate
ser chamado o original SynchronizationContext
chamador Depende da implementação? WhereAsync
, e essa é a razão pela qual ele pode decidir não usar ConfigureAwait(false)
.Mesmo em casos especiais, siga a recomendação geral: use ConfigureAwait(false)
se estiver escrevendo um código de biblioteca de uso geral / independente de modelo de aplicativo.ConfigureAwait (false) garante que o retorno de chamada não será executado no contexto original?
Não, isso garante que não será colocado em fila no contexto original. Mas isso não significa que o código depois await
não será executado no contexto original. Isso se deve ao fato de as operações já concluídas serem retornadas de forma síncrona e não forçadas de volta à fila. Portanto, se você espera uma tarefa que já foi concluída pelo tempo que você espera, independentemente de ser usada ConfigureAwait(false)
, o código continuará sendo executado imediatamente no thread atual em um contexto que ainda é válido.ConfigureAwait (false) , — ?
Em geral, não. Lembre-se do FAQ anterior. Se await task.ConfigureAwait(false)
incluir uma tarefa que já foi concluída no momento da espera (o que realmente acontece com bastante frequência), o uso ConfigureAwait(false)
será inútil, pois o encadeamento continua a executar o código a seguir no método e ainda está no mesmo contexto de antes.Uma exceção digna de nota é que a primeira await
sempre terminará de forma assíncrona e a operação esperada retornará em um ambiente livre de especial SynchronizationContext
ou TaskScheduler
. Por exemplo, CryptoStream
nas bibliotecas de tempo de execução, o .NET verifica se seu código potencialmente intensivo em computação não é executado como parte de uma chamada síncrona do código de chamada. Para fazer isso, ele usa um especialawaiter
para garantir que o código após a primeira espera seja executado no thread pool de threads. No entanto, mesmo nesse caso, você notará que a próxima espera ainda está sendo usada ConfigureAwait(false)
; Tecnicamente, isso não é necessário, mas simplifica bastante a revisão do código, pois não há necessidade de entender por que ele não foi usado ConfigureAwait(false)
.É possível usar o Task.Run para evitar o uso de ConfigureAwait (false)?
Sim, se você escrever: Task.Run(async delegate { await SomethingAsync();
em seguida, ConfigureAwait(false)
no SomethingAsync()
seria supérfluo, como o delegado passou a Task.Run
ser executado em um pool de threads fluxo, de modo que nenhuma alteração no código acima, SynchronizationContext.Current
retorna o valor null
. Além disso, ele o Task.Run
utiliza implicitamente TaskScheduler.Default
, para TaskScheduler.Current
que também retorne um valor dentro do delegado Default
. Isso significa que ele await
terá o mesmo comportamento, independentemente de ter sido usado ConfigureAwait(false)
. Também não pode garantir o que o código dentro deste lambda pode fazer. Se você tem um código: Task.Run(async delegate { SynchronizationContext.SetSynchronizationContext(new SomeCoolSyncCtx()); await SomethingAsync();
então o código dentro SomethingAsync
realmente verá a SynchronizationContext.Current
instância SomeCoolSyncCtx
. e isso await
e quaisquer expectativas não configuradas dentro do SomethingAsync serão retornadas para esse contexto. Portanto, para usar essa abordagem, é necessário entender o que todo o código que você coloca na fila pode ou não fazer e se suas ações podem se tornar um obstáculo.Essa abordagem também ocorre devido à necessidade de criar / enfileirar um objeto de tarefa adicional. Isso pode ou não ser importante para o aplicativo / biblioteca, dependendo dos requisitos de desempenho.Lembre-se também de que essas soluções alternativas podem causar mais problemas do que benefícios e ter diferentes consequências indesejadas. Por exemplo, algumas ferramentas de análise estática sinalizam expectativas que não usam o ConfigureAwait(false)
CA2007 . Se você ligar o analisador e usar esse truque para evitar o uso ConfigureAwait
, há uma alta probabilidade de que o analisador o marque. Isso pode implicar ainda mais trabalho, por exemplo, convém desativar o analisador devido à sua imunidade, além de ignorar outros locais na base de código onde você realmente precisa usá-lo ConfigureAwait(false)
.É possível usar SynchronizationContext.SetSynchronizationContext para evitar o uso de ConfigureAwait (false)?
Não.
Embora seja possível. Depende da implementação usada.Alguns desenvolvedores fazem isso: Task t; SynchronizationContext old = SynchronizationContext.Current; SynchronizationContext.SetSynchronizationContext(null); try { t = CallCodeThatUsesAwaitAsync();
na esperança de que isso force o código a CallCodeThatUsesAwaitAsync
exibir o contexto atual como null
. Assim será. No entanto, esta opção não afetará qual deles será await
exibido TaskScheduler.Current
. Portanto, se o código for executado em um especial TaskScheduler
, seu await
interior CallCodeThatUsesAwaitAsync
verá e fará fila para esse especial TaskScheduler
.Como no Task.Run
FAQ, as mesmas advertências se aplicam aqui: existem certas conseqüências dessa abordagem, e o código dentro do bloco try
também pode interferir nessas tentativas, definindo um contexto diferente (ou chamando o código usando um agendador de tarefas não padrão).Com este modelo, você também precisa ter cuidado com pequenas alterações: SynchronizationContext old = SynchronizationContext.Current; SynchronizationContext.SetSynchronizationContext(null); try { await t; } finally { SynchronizationContext.SetSynchronizationContext(old); }
Veja qual é o problema? Um pouco difícil de notar, mas é impressionante. Não há garantia de que a espera eventualmente cause um retorno de chamada / continue no encadeamento original. Isso significa que o retorno SynchronizationContext
ao original pode não ocorrer no encadeamento original, o que pode levar ao fato de que os itens de trabalho subseqüentes nesse encadeamento verão o contexto errado. Para combater isso, modelos de aplicativos bem escritos que definem um contexto especial geralmente adicionam código para redefini-lo manualmente antes de chamar qualquer código personalizado adicional. E mesmo que isso aconteça em um encadeamento, pode levar algum tempo durante o qual o contexto pode não ser restaurado corretamente. E se funcionar em um encadeamento diferente, isso pode levar à instalação do contexto errado. E assim por diante
Muito longe do ideal.Preciso usar o ConfigureAwait (false) se usar GetAwaiter () .GetResult ()?
Não.
ConfigureAwait
afeta apenas retornos de chamada. Em particular, o modelo awaiter
exige que você awaiter
forneça a propriedade IsCompleted
, os métodos GetResult
e OnCompleted
(opcionalmente com o método UnsafeOnCompleted). ConfigureAwait
afeta apenas o comportamento {Unsafe}OnCompleted
; portanto, se você ligar diretamente GetResult()
, independentemente de fazê-lo TaskAwaiter
ou não, ConfiguredTaskAwaitable.ConfiguredTaskAwaiter
não há diferença no comportamento. Portanto, se você vir task.ConfigureAwait(false).GetAwaiter().GetResult()
que pode substituí-lo por task.GetAwaiter().GetResult()
(além disso, pense se você realmente precisa dessa implementação).Eu sei que o código é executado em um ambiente no qual nunca haverá um SynchronizationContext especial ou um TaskScheduler especial. Não consigo usar o ConfigureAwait (false)?
Possivelmente.
Depende de como você está confiante sobre "nunca". Conforme mencionado nas perguntas anteriores, apenas porque o modelo do aplicativo em que você está trabalhando não especifica um especial SynchronizationContext
e não chama seu código em um especial TaskScheduler
não significa que o código de outro usuário ou biblioteca não os utilize. Portanto, você precisa ter certeza disso ou, pelo menos, reconhecer o risco de que essa opção seja possível.Ouvi dizer que no .NET Core não há necessidade de aplicar o ConfigureAwait (false). É isso mesmo?
Não é assim. É necessário ao trabalhar no .NET Core pelos mesmos motivos que quando trabalha no .NET Framework. Nada mudou a esse respeito.Foi alterado se determinados ambientes publicam seus próprios SynchronizationContext
. Em particular, enquanto o ASP.NET clássico no .NET Framework tem o seu SynchronizationContext
, o ASP.NET Core não. Isso significa que o código em execução no aplicativo ASP.NET Core não verá código especial por padrão SynchronizationContext
, o que reduz a necessidade ConfigureAwait(false)
desse ambiente.No entanto, isso não significa que nunca haverá um costume SynchronizationContext
ouTaskScheduler
. Se algum código de usuário (ou outro código de biblioteca usado pelo aplicativo) definir o contexto do usuário e chamar seu código ou chamar na Tarefa agendada no agendador de tarefas especial, o await
ASP.NET Core verá um contexto ou agendador não padrão, o que pode exigir uso ConfigureAwait(false)
. Obviamente, em situações em que você evita bloqueios síncronos (o que você precisa fazer em aplicativos da Web de qualquer maneira) e, se não for contra a pequena sobrecarga de desempenho em alguns casos, poderá fazê-lo sem usar ConfigureAwait(false)
.Posso usar o ConfigureAwait ao "aguardar a conclusão de cada" no IAsyncEnumerable?
Sim
Veja o artigo do MSDN para um exemplo .Await foreach
corresponde ao padrão e, portanto, pode ser usado para listar IAsyncEnumerable<T>
. Também pode ser usado para listar elementos que representam o escopo correto da API. bibliotecas de execução NET incluem um método de expansão ConfigureAwait
para a IAsyncEnumerable<T>
qual devolve um tipo especial, que envolve IAsyncEnumerable<T>
e Boolean
corresponde ao modelo correcto. Quando o compilador gera chamadas para MoveNextAsync
e o DisposeAsync
enumerador. Essas chamadas estão relacionadas ao tipo configurado retornado de estrutura de enumerador, que, por sua vez, atende às expectativas, conforme necessário.Posso usar o ConfigureAwait com 'aguardar usando' IAsyncDisposable?
Sim, embora com um pouco de complicação.Tal como acontece com IAsyncEnumerable<T>
, biblioteca .NET de tempo de execução fornece um método de extensão ConfigureAwait
para IAsyncDisposable
, e await using
vai funcionar muito bem, porque ele implementa o modelo apropriado (ou seja, fornece um método correspondente DisposeAsync
): await using (var c = new MyAsyncDisposableClass().ConfigureAwait(false)) { ... }
O problema aqui é que o tipo c
agora não é MyAsyncDisposableClass
, mas sim System.Runtime.CompilerServices.ConfiguredAsyncDisposable
, que retornou do método de extensão ConfigureAwait
para IAsyncDisposable
.Para contornar isso, adicione a linha: var c = new MyAsyncDisposableClass(); await using (c.ConfigureAwait(false)) { ... }
Agora o tipo é c
novamente desejado MyAsyncDisposableClass
. O que também tem o efeito de aumentar o escopo para c
; se necessário, você pode colocar tudo entre chaves.Usei o ConfigureAwait (false), mas meu AsyncLocal ainda fluiu para o código depois de esperar. Isso é um bug?
Não, isso é bastante esperado. O fluxo de dados AsyncLocal<T>
é parte ExecutionContext
separada de SynchronizationContext
. Se você não fizer isso explicitamente fluxo desconectado ExecutionContext
usando ExecutionContext.SuppressFlow()
, ExecutionContext
(e, portanto, de dados AsyncLocal <T>
) sempre passam por awaits
, independentemente de utilizado ConfigureAwait
, a fim de evitar a captura a fonte SynchronizationContext
. Mais detalhes são discutidos neste artigo .As ferramentas de idiomas podem me ajudar a evitar a necessidade de usar explicitamente o ConfigureAwait (false) na minha biblioteca?
Às vezes, os desenvolvedores de bibliotecas reclamam da necessidade de usar ConfigureAwait(false)
e pedem alternativas menos invasivas.Atualmente, eles não são, pelo menos não são construídos no idioma / compilador / tempo de execução. No entanto, existem muitas sugestões sobre como isso pode ser implementado, por exemplo: 1 , 2 , 3 , 4 .Se você estiver interessado no tópico, se tiver idéias novas e interessantes, o autor do artigo original o convidará para uma discussão.