Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
Quando uma biblioteca expõe apenas APIs assíncronas, os consumidores por vezes envolvem-nas em chamadas síncronas para satisfazer uma interface ou contrato síncrono. Este padrão de "sincronização sobre assíncrono" pode parecer simples, mas é uma fonte comum de bloqueios e problemas de desempenho.
Padrões básicos de empacotamento
Um wrapper síncrono em torno de um método Task-Based Asynchronous Pattern (TAP) acede à propriedade Result da tarefa, que bloqueia o thread chamador:
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
Esta abordagem parece simples, mas pode causar problemas sérios dependendo do ambiente em que decorre.
Deadlocks com contextos monofilados
O cenário mais perigoso ocorre quando se chama um wrapper síncrono a partir de um thread único que possui um SynchronizationContext. Este cenário é tipicamente um tópico de interface em aplicações 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
Eis o que acontece passo a passo:
- O thread da interface chama
Delay, que chamaDelayAsync(milliseconds).Wait(). -
DelayAsyncExecuta-se de forma síncrona até atingirawait Task.Delay(milliseconds). - Como o atraso ainda não está completo,
awaitcaptura o estado atual SynchronizationContext e suspende.DelayAsyncretorna um Task para quem chamou. - O thread da interface bloqueia
.Wait(), aguardando a conclusão dessa tarefa. - Quando o atraso termina, a continuação tem de correr no original
SynchronizationContext, que é o thread da interface. - O thread da interface não consegue processar a continuação porque está bloqueada em
.Wait(). - Impasse.
Importante
O sucesso ou fracasso do código síncrono sobre assíncrono depende do ambiente em que é executado. O código que funciona numa aplicação de consola pode bloquear num thread de interface ou no ASP.NET (no .NET Framework). Esta dependência ambiental é uma razão fundamental para evitar expor wrappers síncronos.
Exaustão do pool de threads
Os deadlocks não se limitam aos tópicos da interface. Se um método assíncrono depende do pool de threads para completar o seu trabalho, por exemplo, ao enfileirar um passo final de processamento, bloquear muitos threads de pool com wrappers síncronos pode privar o 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:
- Muitos threads do pool de threads chamam
Foo, que bloqueia em.Result. - Cada operação assíncrona completa a sua I/O e necessita de um thread pool thread para executar o seu callback de completão.
- Como as chamadas bloqueadas ocupam threads de trabalho disponíveis, as conclusões podem esperar muito tempo até que um thread fique disponível.
- A versão moderna do .NET pode adicionar mais threads no pool de threads ao longo do tempo, mas a aplicação ainda pode sofrer de esgotamento severo do pool de threads, baixo rendimento, longos atrasos ou um aparente bloqueio.
Este padrão afetava HttpWebRequest.GetResponse no .NET Framework 1.x, onde o método síncrono era implementado como um wrapper em torno do assíncrono BeginGetResponse/EndGetResponse.
Orientação: Evite expor wrappers síncronos
Não exponhas um método síncrono que envolve uma implementação assíncrona. Em vez disso, deixe a decisão de bloquear ou não ao consumidor. O consumidor conhece o seu ambiente de execução de threads e pode tomar uma decisão informada.
Se precisares de chamar um método assíncrono de forma síncrona, considera primeiro se podes reestruturar o código para ser "assíncrono até ao fim". A refatoração é frequentemente a melhor solução a longo prazo.
Estratégias de mitigação quando o sync-over-async é inevitável
Por vezes, sincronizar sobre assíncrono é mesmo inevitável. Por exemplo, é inevitável quando se implementa uma interface que requer um método síncrono, e a única implementação disponível é assíncrona. Nesses casos, aplique as seguintes estratégias para reduzir o risco.
Utilização ConfigureAwait(false) na implementação assíncrona
Se controlares o método assíncrono, usa Task.ConfigureAwait com false em cada await para evitar que a continuação seja remetida ao 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 todas as operações de espera, a menos que o teu código precise especificamente de executar novamente no contexto capturado. O uso de ConfigureAwait(false) é a melhor prática em termos de desempenho e ajuda a evitar deadlocks quando os consumidores estão bloqueados.
Transferir para o pool de threads
Se não controlares a implementação assíncrona (e pode não ser usada ConfigureAwait(false)), descarrega a chamada para o pool de threads. O pool de threads não tem um SynchronizationContext, por isso o await não tentará redirecionar para uma thread bloqueada:
public int Sync()
{
return Task.Run(() => Library.FooAsync()).Result;
}
Public Function Sync() As Integer
Return Task.Run(Function() Library.FooAsync()).Result
End Function
Teste em múltiplos ambientes
Se tiver de enviar um wrapper síncrono, teste-o a partir de:
- Um tópico de interface (WPF, Windows Forms).
- O pool de threads em carga.
- O pool de threads com um número máximo de threads baixo.
- Uma aplicação de consola.
Comportamento que funciona num ambiente pode entravar noutro.