Con varios hilos por aquí de aplicaciones que se quedan «colgadas» sin excepción visible, un aviso sobre uno de los errores más comunes con async/await en C#, que sigue apareciendo aunque el lenguaje ya lleva muchos años con esto: mezclar código asíncrono con .Result o .Wait() para «forzar» que se comporte como síncrono.
Esto es lo que causa el cuelgue clásico, sobre todo en aplicaciones de escritorio (WinForms/WPF) o en el hilo principal de ASP.NET clásico:
// Esto puede colgar la aplicacion:
var resultado = ObtenerDatosAsync().Result;
El motivo es un interbloqueo (deadlock) por el contexto de sincronización: .Result bloquea el hilo actual esperando a que la tarea termine, pero cuando la tarea asíncrona interna termina, intenta volver a ese mismo hilo para continuar (por defecto, await captura el contexto de sincronización original), y ese hilo está bloqueado esperándola a ella. Los dos se quedan esperándose mutuamente para siempre.
La solución correcta es dejar que el async/await se propague hacia arriba en toda la cadena de llamadas, en vez de cortarlo en algún punto intermedio con .Result:
// Correcto: async hasta el final
var resultado = await ObtenerDatosAsync();
Si de verdad necesitas llamar código asíncrono desde un contexto síncrono que no puedes convertir a async (por ejemplo, un constructor, que no puede ser async en C#), la vía menos peligrosa es ConfigureAwait(false) en la llamada interna, que evita que intente volver al contexto original y reduce (aunque no elimina del todo) el riesgo de deadlock:
var resultado = ObtenerDatosAsync().ConfigureAwait(false).GetAwaiter().GetResult();
Pero la recomendación real, siempre que se pueda, es la primera: hacer async toda la cadena, desde el punto de entrada (el método del botón, el controlador, lo que sea) hasta la llamada final. Cortar la cadena a medias con .Result/.Wait() es donde casi siempre empieza el problema.
David Carrero
El motivo técnico exacto de por qué se cuelga (no siempre, lo cual lo hace peor) es la captura del SynchronizationContext. Cuando haces await dentro de un método async, por defecto el await "recuerda" el contexto de sincronización desde el que se llamó (el hilo de UI en WinForms/WPF, o el contexto de request en ASP.NET clásico) para volver a él cuando la tarea termina.
El problema aparece cuando, desde ese mismo contexto (por ejemplo el hilo de UI), llamas de forma síncrona con .Result o .Wait() a un método async: el hilo de UI se queda bloqueado esperando a que termine la tarea, pero la tarea, al completarse, necesita volver a ese mismo hilo de UI para continuar (porque capturó ese contexto), y ese hilo está ocupado esperando. Resultado: deadlock, cada uno esperando al otro indefinidamente.
Dos formas de evitarlo:
1. La más recomendada siempre que puedas: ser async de principio a fin, await en toda la cadena de llamadas hasta arriba, sin ningún .Result/.Wait() síncrono mezclado en medio.
2. Si estás escribiendo una librería y no controlas quién te va a llamar (síncrono o async), usar ConfigureAwait(false) en los await internos de tu librería. Esto le dice a la tarea que no necesita volver al contexto original al terminar, puede continuar en cualquier hilo del pool, lo que rompe la dependencia circular que causa el deadlock.
Ivan