Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
Quando uma biblioteca expõe apenas APIs assíncronas, os consumidores às vezes os encapsulam em chamadas síncronas para satisfazer uma interface ou contrato síncrono. Esse padrão "sync-over-async" pode parecer simples, mas é uma fonte comum de deadlocks e problemas de desempenho.
Padrões básicos de encapsulamento
Um wrapper síncrono em torno de um método TAP (Padrão Assíncrono baseado em tarefa) acessa a propriedade tarefa Result, o que bloqueia o thread de chamada.
public class TapWrapper
{
public static int Foo(Func<Task<int>> fooAsync)
{
return fooAsync().Result;
}
}
Public Module TapWrapper
Public Function Foo(fooAsync As Func(Of Task(Of Integer))) As Integer
Return fooAsync().Result
End Function
End Module
Essa abordagem parece simples, mas pode causar sérios problemas dependendo do ambiente em que ela é executada.
Deadlocks com contextos de thread único
O cenário mais perigoso ocorre quando você chama um wrapper síncrono de um thread que tem um thread SynchronizationContextúnico. Esse cenário normalmente é um thread de interface do usuário em aplicativos WPF, Windows Forms ou MAUI.
public static class DeadlockExample
{
private static void Delay(int milliseconds)
{
DelayAsync(milliseconds).Wait();
}
private static async Task DelayAsync(int milliseconds)
{
await Task.Delay(milliseconds);
}
}
Public Module DeadlockExample
Private Sub Delay(milliseconds As Integer)
DelayAsync(milliseconds).Wait()
End Sub
Private Async Function DelayAsync(milliseconds As Integer) As Task
Await Task.Delay(milliseconds)
End Function
End Module
Veja o que acontece passo a passo:
- A thread da interface do usuário chama
Delay, que chamaDelayAsync(milliseconds).Wait(). -
DelayAsyncé executado de forma síncrona até atingirawait Task.Delay(milliseconds). - Como o atraso ainda não foi concluído,
awaitcaptura o atual SynchronizationContext e suspende.DelayAsyncretorna um Task para o chamador. - O thread da interface do usuário bloqueia
.Wait(), aguardando a conclusão dessa tarefa. - Quando o atraso for concluído, a continuação precisará ser executada no
SynchronizationContextoriginal, que é a thread de UI. - A thread de interface do usuário não pode processar a continuação porque está bloqueada em
.Wait(). - Deadlock.
Importante
O êxito ou falha do código sincronizado sobre assíncrono depende do ambiente em que ele é executado. O código que funciona em um aplicativo de console pode ficar em deadlock em um thread de interface do usuário ou em ASP.NET (no .NET Framework). Essa dependência ambiental é um motivo central para evitar expor wrappers síncronos.
Esgotamento do pool de threads
Deadlocks não se limitam a threads da interface do usuário. Se um método assíncrono depender do pool de threads para concluir seu trabalho, por exemplo, enfileirando uma etapa de processamento final, bloquear muitos threads do pool com wrappers síncronos pode esgotar os recursos do pool.
public static class ThreadPoolDeadlockExample
{
public static int Foo(Func<Task<int>> fooAsync)
{
return fooAsync().Result;
}
public static async Task DemonstrateDeadlockRiskAsync()
{
var tasks = Enumerable.Range(0, 25)
.Select(_ => Task.Run(() => Foo(() => SomeIOOperationAsync())));
await Task.WhenAll(tasks);
}
private static async Task<int> SomeIOOperationAsync()
{
await Task.Delay(100);
return 42;
}
}
Neste cenário:
- Muitas threads do pool fazem chamadas para
Foo, que bloqueiam em.Result. - Cada operação assíncrona conclui sua E/S e precisa de uma thread do pool para executar seu retorno de chamada de conclusão.
- Como as chamadas bloqueadas ocupam os threads de trabalho disponíveis, as conclusões podem esperar muito tempo até que um thread fique disponível.
- O .NET moderno pode adicionar mais threads ao pool de threads ao longo do tempo, mas o aplicativo ainda pode sofrer esgotamento severo de threads do pool, baixa taxa de transferência, longos atrasos ou um bloqueio aparente.
Esse padrão afetou HttpWebRequest.GetResponse no .NET Framework 1.x, em que o método síncrono foi implementado como um encapsulamento do método assíncrono BeginGetResponse/EndGetResponse.
Diretriz: evite expor os wrappers síncronos
Não exponha um método síncrono que encapsula uma implementação assíncrona. Em vez disso, deixe a decisão de bloquear ao consumidor. O consumidor conhece seu ambiente de threading e pode fazer uma escolha informada.
Se você precisar chamar um método assíncrono de forma síncrona, considere primeiro se pode reestruturar o código para ser "assíncrono até o fim". A refatoração geralmente é a melhor solução de longo prazo.
Estratégias de mitigação quando a sincronização sobre assíncrono é inevitável
Às vezes, a sincronização sobre assíncrono é verdadeiramente inevitável. Por exemplo, é inevitável quando você implementa uma interface que requer um método síncrono e a única implementação disponível é assíncrona. Nesses casos, aplique as estratégias a seguir para reduzir o risco.
Usar ConfigureAwait(false) na implementação assíncrona
Se você controlar o método assíncrono, use Task.ConfigureAwait, com false em cada await, para impedir que a continuação realize marshaling de volta para o original SynchronizationContext:
public static class ConfigureAwaitMitigation
{
public static async Task<int> LibraryMethodAsync()
{
await Task.Delay(100).ConfigureAwait(false);
return 42;
}
public static int Sync()
{
return LibraryMethodAsync().GetAwaiter().GetResult();
}
}
Public Module ConfigureAwaitMitigation
Public Async Function LibraryMethodAsync() As Task(Of Integer)
Await Task.Delay(100).ConfigureAwait(False)
Return 42
End Function
Public Function Sync() As Integer
Return LibraryMethodAsync().Result
End Function
End Module
Como autor de biblioteca, use ConfigureAwait(false) em todos os awaits, a menos que seu código precise continuar execuções especificamente no contexto capturado. O uso de ConfigureAwait(false) é uma prática recomendada para melhorar o desempenho e ajuda a prevenir deadlocks quando os consumidores estão bloqueados.
Descarregar no pool de threads
Se você não controlar a implementação assíncrona (e ela pode não usar ConfigureAwait(false)), delegue a chamada para o pool de threads. O pool de threads não tem um SynchronizationContext, portanto, o await não tentará fazer marshalback para um thread bloqueado:
public int Sync()
{
return Task.Run(() => Library.FooAsync()).Result;
}
Public Function Sync() As Integer
Return Task.Run(Function() Library.FooAsync()).Result
End Function
Testar em vários ambientes
Se você precisar enviar um wrapper síncrono, teste-o a partir de:
- Um thread de interface do usuário (WPF, Windows Forms).
- O pool de threads em condição de carga.
- O pool de threads com uma contagem máxima baixa de threads.
- Um aplicativo de console.
O comportamento que funciona em um ambiente pode entrar em deadlock em outro.