Nota
O acceso a esta páxina require autorización. Pode tentar iniciar sesión ou modificar os directorios.
O acceso a esta páxina require autorización. Pode tentar modificar os directorios.
Cuando tiene un método sincrónico en una biblioteca, es posible que tenga la tentación de exponer un homólogo asincrónico que lo encapsula en Task.Run:
public T Foo() { /* synchronous work */ }
// Don't do this in a library:
public Task<T> FooAsync()
{
return Task.Run(() => Foo());
}
En este artículo se explica por qué ese enfoque es casi siempre incorrecto para las bibliotecas y cómo pensar en los inconvenientes.
Escalabilidad frente a descarga
La programación asincrónica proporciona dos ventajas distintas:
- Escalabilidad : reduzca el consumo de recursos liberando subprocesos durante las esperas de E/S.
- Descargo: mueva el trabajo a otro subproceso para mantener la capacidad de respuesta (por ejemplo, mantener libre un subproceso de interfaz de usuario) o conseguir paralelismo.
Estas ventajas requieren enfoques diferentes. La distinción crítica: encapsular un método sincrónico en Task.Run ayuda con la descarga, pero no hace nada para la escalabilidad.
¿Por qué Task.Run no mejora la escalabilidad?
Una implementación verdaderamente asincrónica reduce el número de subprocesos consumidos durante una operación de larga duración. Un Task.Run envoltorio sigue bloqueando un hilo, simplemente mueve el bloqueo de un hilo a otro.
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 ese enfoque con una implementación verdaderamente asincrónica que no consume ningún subproceso mientras 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 implementaciones se completan después del retraso especificado, pero la segunda implementación no bloquea ningún subproceso mientras espera. En el caso de las aplicaciones de servidor que controlan muchas solicitudes simultáneas, esa diferencia afecta directamente al número de solicitudes que un servidor puede procesar simultáneamente.
La descarga es responsabilidad del consumidor
El ajuste de llamadas sincrónicas en Task.Run es útil para descargar el trabajo desde un subproceso de interfaz de usuario. Sin embargo, el consumidor, no la biblioteca, debe manejar este envoltorio.
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
El consumidor conoce su contexto: si están en un hilo de interfaz de usuario, la granularidad que necesitan y si la delegación de tareas agrega valor. La biblioteca no lo hace.
¿Por qué las bibliotecas no deben exponer contenedores asincrónicos a través de sincronización?
Cuando una biblioteca expone solo el método sincrónico (y no un contenedor asincrónico), los consumidores se benefician de varias maneras:
- Área expuesta de API reducida: menos métodos para aprender, probar y mantener.
- No hay expectativas de escalabilidad engañosas: los usuarios saben que solo los métodos expuestos como asincrónicos proporcionan realmente ventajas de escalabilidad.
-
Control de consumidor: los autores de llamadas eligen si y cómo descargar, en el nivel correcto de granularidad. Una aplicación de servidor de alto rendimiento puede llamar directamente al método sincrónico, evitando una sobrecarga innecesaria de
Task.Run. - Mejor rendimiento: las envolturas asincrónicas agregan sobrecarga a través de asignaciones, cambios de contexto y programación del pool de subprocesos. Para las operaciones específicas, esa sobrecarga puede ser significativa.
Excepciones a la regla
Algunas clases base exponen métodos asincrónicos para que las clases derivadas puedan invalidarlas con implementaciones verdaderamente asincrónicas. La clase base proporciona un valor predeterminado de asincrónico sobre sincrónico.
Por ejemplo, Stream expone ReadAsync y WriteAsync. Las implementaciones base envuelven los métodos sincrónicos Read y Write. Las clases derivadas como FileStream e NetworkStream invalidan estos métodos con implementaciones de E/S asincrónicas que proporcionan ventajas de escalabilidad reales.
Del mismo modo, TextReader ofrece ReadToEndAsync en la clase base como un envoltorio, y StreamReader lo sobrescribe con una implementación realmente asincrónica que llama a ReadAsync internamente.
Estas excepciones son válidas porque:
- El patrón está diseñado para el polimorfismo. Los llamadores interactúan con el tipo base.
- Los tipos derivados proporcionan invalidaciones verdaderamente asincrónicas.
Directrices
Exponga métodos asincrónicos de una biblioteca solo cuando la implementación proporciona ventajas de escalabilidad reales sobre su homólogo sincrónico. No exponga métodos asincrónicos exclusivamente para la descarga. Deje esa opción para el consumidor.