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 você tiver um método síncrono em uma biblioteca, talvez sinta vontade de expor um equivalente assíncrono para encapsulá-lo em Task.Run:
public T Foo() { /* synchronous work */ }
// Don't do this in a library:
public Task<T> FooAsync()
{
return Task.Run(() => Foo());
}
Este artigo explica por que essa abordagem é quase sempre errada para bibliotecas e como pensar sobre as compensações.
Escalabilidade versus descarregamento
A programação assíncrona oferece dois benefícios distintos:
- Escalabilidade – reduza o consumo de recursos liberando threads durante as esperas de E/S.
- Descarregamento — Transferir o trabalho para um thread diferente para manter a capacidade de resposta (por exemplo, manter livre o thread de interface do usuário) ou alcançar paralelismo.
Esses benefícios exigem abordagens diferentes. A distinção crítica: encapsular um método síncrono ajuda no Task.Run descarregamento, mas não faz nada para escalabilidade.
Por que Task.Run não melhora a escalabilidade
Uma implementação verdadeiramente assíncrona reduz o número de threads consumidos durante uma operação de longa execução. Um Task.Run wrapper ainda bloqueia um thread – ele apenas move o bloqueio de um thread para outro:
public static class TimerExampleWrong
{
public static Task SleepAsync(int millisecondsTimeout)
{
return Task.Run(() => Thread.Sleep(millisecondsTimeout));
}
}
Public Module TimerExampleWrong
Public Function SleepAsync(millisecondsTimeout As Integer) As Task
Return Task.Run(Sub() Thread.Sleep(millisecondsTimeout))
End Function
End Module
Compare essa abordagem com uma implementação verdadeiramente assíncrona que não consome threads enquanto aguarda:
public static class TimerExampleRight
{
public static Task SleepAsync(int millisecondsTimeout)
{
var tcs = new TaskCompletionSource<bool>();
var timer = new Timer(
_ => tcs.TrySetResult(true), null, millisecondsTimeout, Timeout.Infinite);
tcs.Task.ContinueWith(
_ => timer.Dispose(), TaskScheduler.Default);
return tcs.Task;
}
}
Public Module TimerExampleRight
Public Function SleepAsync(millisecondsTimeout As Integer) As Task
Dim tcs As New TaskCompletionSource(Of Boolean)()
Dim tmr As New Timer(
Sub(state) tcs.TrySetResult(True), Nothing, millisecondsTimeout, Timeout.Infinite)
tcs.Task.ContinueWith(
Sub(t) tmr.Dispose(), TaskScheduler.Default)
Return tcs.Task
End Function
End Module
Ambas as implementações são concluídas após o atraso especificado, mas a segunda implementação não bloqueia nenhum thread enquanto aguarda. Para aplicativos de servidor que lidam com muitas solicitações simultâneas, essa diferença afeta diretamente quantas solicitações um servidor pode processar simultaneamente.
O descarregamento é responsabilidade do consumidor
Encapsular chamadas síncronas em Task.Run é útil para descarregar o trabalho de uma thread de interface do usuário. No entanto, o consumidor, não a biblioteca, deve lidar com esse encapsulamento:
public static class UIOffloadExample
{
public static int ComputeIntensive(int input)
{
int result = 0;
for (int i = 0; i < input; i++)
{
result += i;
}
return result;
}
public static async Task ConsumeFromUIThreadAsync()
{
int result = await Task.Run(() => ComputeIntensive(10_000));
Console.WriteLine($"Result: {result}");
}
}
Public Module UIOffloadExample
Public Function ComputeIntensive(input As Integer) As Integer
Dim result As Integer = 0
For i As Integer = 0 To input - 1
result += i
Next
Return result
End Function
Public Async Function ConsumeFromUIThreadAsync() As Task
Dim result As Integer = Await Task.Run(Function() ComputeIntensive(10_000))
Console.WriteLine($"Result: {result}")
End Function
End Module
O consumidor conhece seu contexto: se está em um thread da interface do usuário, quanta granularidade precisa e se a distribuição de carga traz valor. A biblioteca não.
Por que as bibliotecas não devem expor wrappers assíncronos sobre sincronização
Quando uma biblioteca expõe apenas o método síncrono (e não um wrapper assíncrono), os consumidores se beneficiam de várias maneiras:
- Área de superfície de API reduzida: menos métodos para aprender, testar e manter.
- Sem expectativas de escalabilidade enganosas: os usuários sabem que apenas os métodos expostos como assíncronos realmente fornecem benefícios de escalabilidade.
-
Controle do consumidor: os chamadores escolhem se e como descarregar, no nível certo de granularidade. Um aplicativo de servidor de alta taxa de transferência pode chamar o método síncrono diretamente, evitando sobrecarga desnecessária de
Task.Run. - Melhor desempenho: Envoltórios assíncronos adicionam sobrecarga por meio de alocações, comutação de contexto e agendamento do thread pool. Para operações refinadas, essa sobrecarga pode ser significativa.
Exceções à regra
Algumas classes base expõem métodos assíncronos para que as classes derivadas possam substituí-las por implementações verdadeiramente assíncronas. A classe base fornece um padrão assíncrono-sobre-síncrono padrão.
Por exemplo, Stream expõe ReadAsync e WriteAsync. As implementações base encapsulam os métodos Read e Write síncronos. Classes derivadas como FileStream e NetworkStream sobrescrevem esses métodos com implementações de E/S assíncronas que fornecem benefícios reais de escalabilidade.
Da mesma forma, TextReader fornece ReadToEndAsync na classe base como um wrapper e StreamReader substitui-a por uma implementação verdadeiramente assíncrona que chama ReadAsync internamente.
Essas exceções são válidas porque:
- O padrão foi projetado para polimorfismo. Os chamadores interagem com o tipo base.
- Os tipos derivados fornecem substituições verdadeiramente assíncronas.
Diretriz
Exponha métodos assíncronos de uma biblioteca somente quando a implementação fornece benefícios reais de escalabilidade em relação ao seu equivalente síncrono. Não exponha métodos assíncronos somente para descarregamento. Deixe essa escolha para o consumidor.