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 tem um método síncrono numa biblioteca, pode sentir-se tentado a expor um equivalente assíncrono que o envolva 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 porque é que essa abordagem é quase sempre errada para as bibliotecas e como pensar nas trocas.
Escalabilidade vs. descarregamento
A programação assíncrona oferece dois benefícios distintos:
- Escalabilidade — Reduza o consumo de recursos libertando threads durante as esperas de I/O.
- Offloading — Mover o trabalho para outro thread para manter a responsividade (por exemplo, manter um thread da interface do utilizador livre) ou alcançar paralelismo.
Estes benefícios exigem abordagens diferentes. A distinção crítica: envolver um método síncrono em Task.Run ajuda a descarregar, mas não contribui para a escalabilidade.
Porque 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 duração. Um Task.Run wrapper continua a bloquear um thread — 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 espera:
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 está à espera. Para aplicações servidor que lidam com muitos pedidos simultâneos, essa diferença afeta diretamente quantos pedidos um servidor pode processar simultaneamente.
O descarregamento é responsabilidade do consumidor
Envolver chamadas síncronas em Task.Run é útil para descarregar trabalho de um thread de interface de utilizador. No entanto, o consumidor, e não a biblioteca, deve tratar deste 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 o seu contexto: se está num tópico de interface, quanta granularidade precisa e se o offloading acrescenta valor. A biblioteca não.
Porque é que as bibliotecas não deveriam expor wrappers de sincronização assíncrona
Quando uma biblioteca expõe apenas o método síncrono (e não um wrapper assíncrono), os consumidores beneficiam de várias formas:
- Área de superfície reduzida da API: Menos métodos para aprender, testar e manter.
- Sem expectativas enganosas de escalabilidade: Os utilizadores sabem que apenas os métodos expostos como assíncronos proporcionam benefícios de escalabilidade.
-
Controlo do consumidor: Os chamadores escolhem se e como descarregam, com o nível certo de granularidade. Uma aplicação de servidor de elevada capacidade pode chamar diretamente o método síncrono, evitando sobrecarga desnecessária de
Task.Run. - Melhor desempenho: Os wrappers assíncronos adicionam sobrecarga através de alocações, trocas de contexto e agendamento de pool de threads. Para operações de granulação fina, essa sobrecarga pode ser significativa.
Exceções à regra
Algumas classes base expõem métodos assíncronos para que classes derivadas possam sobrepê-los com implementações verdadeiramente assíncronas. A classe base fornece um comportamento padrão de async-over-sync.
Por exemplo, Stream expõe ReadAsync e WriteAsync. As implementações base envolvem os métodos síncronos Read e Write . Classes derivadas como FileStream e NetworkStream sobrepõem estes métodos com implementações de E/S assíncronas que proporcionam benefícios reais de escalabilidade.
De forma semelhante, TextReader fornece ReadToEndAsync na classe base como um encapsulador, e StreamReader sobrescreve-o com uma implementação verdadeiramente assíncrona que chama ReadAsync internamente.
Estas exceções são válidas porque:
- O padrão foi concebido com foco em polimorfismo. Os chamadores interagem com o tipo base.
- Os tipos derivados fornecem substituições verdadeiramente assíncronas.
Diretriz
Expõe métodos assíncronos de uma biblioteca apenas quando a implementação oferece benefícios reais de escalabilidade em relação ao seu equivalente síncrono. Não exponhas métodos assíncronos apenas para descarregar. Deixe essa escolha ao consumidor.