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 una biblioteca expone solo API asincrónicas, los consumidores a veces los encapsulan en llamadas sincrónicas para satisfacer una interfaz o contrato sincrónicos. Este patrón de "sincronización sobre asincronía" puede parecer sencillo, pero es una fuente común de interbloqueos y problemas de rendimiento.
Patrones de envoltura básicos
Un contenedor sincrónico alrededor de un método basado en el patrón asincrónico de tareas (TAP) accede a la propiedad Result de la tarea, lo que bloquea el hilo que realiza la llamada.
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
Este enfoque es sencillo, pero puede causar problemas graves en función del entorno en el que se ejecute.
Interbloqueos con contextos de un solo hilo
El escenario más peligroso se produce cuando se llama a un contenedor sincrónico desde un subproceso que tiene un único subproceso SynchronizationContext. Este escenario suele ser un subproceso de interfaz de usuario en aplicaciones de WPF, Windows Forms o 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
Esto es lo que sucede paso a paso:
- El subproceso de interfaz de usuario llama a
Delay, que llama aDelayAsync(milliseconds).Wait(). -
DelayAsyncse ejecuta sincrónicamente hasta que alcanzaawait Task.Delay(milliseconds). - Dado que el retraso aún no se ha completado,
awaitcaptura el estado actual de SynchronizationContext y se suspende.DelayAsyncdevuelve un Task al autor de la llamada. - El subproceso de interfaz de usuario se bloquea en
.Wait(), esperando a que se complete esa tarea. - Cuando finalice el retraso, la continuación debe ejecutarse en el hilo original
SynchronizationContext, que es el hilo de la interfaz de usuario. - El subproceso de interfaz de usuario no puede procesar la continuación porque está bloqueado en
.Wait(). - Interbloqueo.
Importante
El éxito o error del código sincronizado a través de async depende del entorno en el que se ejecute. El código que funciona en una aplicación de consola podría bloquearse en un hilo de interfaz de usuario o en ASP.NET (en .NET Framework). Esta dependencia del entorno es una razón fundamental para evitar exponer envoltorios sincrónicos.
Agotamiento del grupo de hilos
Los interbloqueos no están limitados a los subprocesos de la interfaz de usuario. Si un método asincrónico depende del grupo de subprocesos para completar su trabajo, por ejemplo, mediante la puesta en cola de un paso de procesamiento final, el bloqueo de muchos subprocesos del grupo con envoltorios síncronos puede dejar sin recursos al grupo.
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;
}
}
En este escenario:
- Muchos hilos del grupo de hilos llaman a
Foo, que bloquea en.Result. - Cada operación asincrónica completa su E/S y necesita un hilo del pool de hilos para ejecutar su callback de finalización.
- Dado que las llamadas bloqueadas ocupan subprocesos de trabajo disponibles, las finalizaciones pueden esperar mucho tiempo para que un subproceso esté disponible.
- La versión moderna de .NET puede agregar más subprocesos de grupo con el tiempo, pero la aplicación todavía puede sufrir un grave agotamiento del grupo de subprocesos, un rendimiento deficiente, retrasos prolongados o un bloqueo aparente.
Este patrón afectó a HttpWebRequest.GetResponse en .NET Framework 1.x, donde el método sincrónico se implementó como un envoltorio alrededor del método asincrónico BeginGetResponse/EndGetResponse.
Directriz: Evite exponer envoltorios sincrónicos
No exponga un método sincrónico que encapsula una implementación asincrónica. En cambio, deje al consumidor la decisión de bloquear o no. El consumidor conoce su entorno de subprocesos y puede tomar una decisión informada.
Si necesita llamar a un método asincrónico de forma sincrónica, considere primero si puede reestructurar el código para que sea "asincrónico todo el camino hacia abajo". La refactorización suele ser la mejor solución a largo plazo.
Estrategias de mitigación cuando la sincronización sobre asincronía es inevitable
A veces, la sincronización sobre asincronía es realmente inevitable. Por ejemplo, es inevitable cuando se implementa una interfaz que requiere un método sincrónico y la única implementación disponible es asincrónica. En esos casos, aplique las siguientes estrategias para reducir el riesgo.
Uso ConfigureAwait(false) en la implementación asincrónica
Si controla el método asincrónico, use Task.ConfigureAwait con false en cada await para evitar que la continuación vuelva a descargarse en el 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 desarrollador de bibliotecas, use ConfigureAwait(false) en todos los awaits a menos que el código necesite reanudarse específicamente en el contexto capturado.
ConfigureAwait(false) es una práctica recomendada para el rendimiento y ayuda a evitar bloqueos cuando los consumidores bloquean.
Descarga en el grupo de subprocesos
Si no controla la implementación asincrónica (y es posible que no use ConfigureAwait(false)), delegue la llamada al grupo de subprocesos. El grupo de subprocesos no tiene un SynchronizationContext, por lo que await no intentará retomar en un subproceso bloqueado.
public int Sync()
{
return Task.Run(() => Library.FooAsync()).Result;
}
Public Function Sync() As Integer
Return Task.Run(Function() Library.FooAsync()).Result
End Function
Prueba en varios entornos
Si debe enviar un contenedor sincrónico, pruébelo desde:
- Un subproceso de interfaz de usuario (WPF, Windows Forms).
- Grupo de subprocesos bajo carga.
- El grupo de hilos con un número máximo de hilos bajo.
- Una aplicación de consola.
El comportamiento que funciona en un entorno podría llegar a interbloquearse en otro.