ConfigureAwait: perguntas freqüentes

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

imagem

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); //  downloadBtn.Content = text; } 

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 predicateser chamado o original SynchronizationContextchamador 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 awaitnã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 awaitsempre terminará de forma assíncrona e a operação esperada retornará em um ambiente livre de especial SynchronizationContextou TaskScheduler. Por exemplo, CryptoStreamnas 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 especialawaiterpara 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.Runser executado em um pool de threads fluxo, de modo que nenhuma alteração no código acima, SynchronizationContext.Currentretorna o valor null. Além disso, ele o Task.Runutiliza implicitamente TaskScheduler.Default, para TaskScheduler.Currentque também retorne um valor dentro do delegado Default. Isso significa que ele awaitterá 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(); //    SomeCoolSyncCtx }); 

então o código dentro SomethingAsyncrealmente verá a SynchronizationContext.Currentinstância SomeCoolSyncCtx. e isso awaite 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(); // await'      } finally { SynchronizationContext.SetSynchronizationContext(old); } await t; //  -     


na esperança de que isso force o código a CallCodeThatUsesAwaitAsyncexibir o contexto atual como null. Assim será. No entanto, esta opção não afetará qual deles será awaitexibido TaskScheduler.Current. Portanto, se o código for executado em um especial TaskScheduler, seu awaitinterior CallCodeThatUsesAwaitAsyncverá e fará fila para esse especial TaskScheduler.

Como no Task.RunFAQ, as mesmas advertências se aplicam aqui: existem certas conseqüências dessa abordagem, e o código dentro do bloco trytambé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 SynchronizationContextao 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. ConfigureAwaitafeta apenas retornos de chamada. Em particular, o modelo awaiterexige que você awaiterforneça a propriedade IsCompleted, os métodos GetResulte OnCompleted(opcionalmente com o método UnsafeOnCompleted). ConfigureAwaitafeta apenas o comportamento {Unsafe}OnCompleted; portanto, se você ligar diretamente GetResult(), independentemente de fazê-lo TaskAwaiterou não, ConfiguredTaskAwaitable.ConfiguredTaskAwaiternã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 SynchronizationContexte não chama seu código em um especial TaskSchedulernã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 SynchronizationContextouTaskScheduler. 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 awaitASP.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?


SimVeja o artigo do MSDN para um exemplo .

Await foreachcorresponde 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 Booleancorresponde ao modelo correcto. Quando o compilador gera chamadas para MoveNextAsynce o DisposeAsyncenumerador. 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 ConfigureAwaitpara IAsyncDisposable, e await usingvai 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 cagora não é MyAsyncDisposableClass, mas sim System.Runtime.CompilerServices.ConfiguredAsyncDisposable, que retornou do método de extensão ConfigureAwaitpara IAsyncDisposable.

Para contornar isso, adicione a linha:

 var c = new MyAsyncDisposableClass(); await using (c.ConfigureAwait(false)) { ... } 

Agora o tipo é cnovamente 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 ExecutionContextseparada de SynchronizationContext. Se você não fizer isso explicitamente fluxo desconectado ExecutionContextusando 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.

Source: https://habr.com/ru/post/pt482354/


All Articles