¿Cómo llamar a un método asíncrono desde uno síncrono en C#? Carne, no agua
Imagina: estás por la noche frente al código, terminando una vieja utilidad de consola en C#. Todo funciona de forma síncrona, como un reloj. Y de repente — ¡pum! — necesitas llamar a una API que solo está hecha con async Task. ¿No vas a reescribir todo el programa? Un dolor familiar, ¿verdad?
La pregunta «How to call asynchronous method from synchronous method in C#» es una de las más populares en Stack Overflow. Y no es para menos. Allí, en los comentarios, hay toneladas de consejos desde «haz .Result y no te preocupes» hasta «¿estás loco? ¡Vas a tener un deadlock!». Vamos a desglosarlo: qué enfoques realmente funcionan y cuáles son un camino al infierno.
¿Por qué simplemente «await» no funciona?
Si intentas escribir:
public void SyncMethod(){ await DoSomethingAsync(); // Error CS4032}El compilador simplemente se negará a compilarlo. await solo se puede usar dentro de un método marcado con el modificador async. Y async void es otro cantar que también analizaremos.
Entonces, tenemos un método asíncrono:
public async Task<string> GetDataAsync(){ using var httpClient = new HttpClient(); return await httpClient.GetStringAsync("https://api.example.com/data");}Y tenemos un método síncrono desde el cual queremos llamarlo. Vamos por las opciones.
Enfoque 1: GetAwaiter().GetResult() — estilo hacker
La forma más popular y, quizás, la más elegante:
public string CallSync(){ return GetDataAsync().GetAwaiter().GetResult();}Por qué es genial:
- No genera una excepción
AggregateException(a diferencia de.Result). Si dentro del método asíncrono ocurre un error, obtienes su tipo original, no un envoltorio. - El código se lee bien, sin try-catch innecesarios.
Pero hay un detalle: si tu método síncrono se ejecuta en un contexto con SynchronizationContext (por ejemplo, en el hilo de UI de WinForms o WPF), corres el riesgo de sufrir un deadlock — un bloqueo mutuo de hilos. El hilo A espera que se complete la tarea asíncrona, y la tarea no puede completarse porque espera que el hilo A se libere. Un desastre.
Cómo evitarlo: Usa .ConfigureAwait(false) dentro del propio método asíncrono. Pero eso ya implica cambiar la API, no solo el código que llama.
Enfoque 2: Task.Run(async () => await ...).GetAwaiter() — salida al pool de hilos
Si no puedes modificar el método asíncrono, pero quieres evitar garantizadamente el deadlock — saca la tarea al pool de hilos:
public string CallSyncSafe(){ return Task.Run(async () => await GetDataAsync()).GetAwaiter().GetResult();}Magia: Task.Run ejecuta el delegado en un hilo del ThreadPool, que no tiene SynchronizationContext. El deadlock es imposible. Incluso si el método original usa ConfigureAwait(true).
Inconveniente: Un cambio de contexto (context switch) adicional. Para cientos de llamadas no es óptimo. Para llamadas puntuales es perfecto.
Enfoque 3: .Result o .Wait() — el método de la abuela
public string CallSyncOldSchool(){ return GetDataAsync().Result; // O .Wait()}¿Por qué es malo?
- En caso de error, lanza
AggregateException. Para llegar al error real, hay que rebuscar enInnerException. - El mismo riesgo de deadlock en contexto de UI.
- El código parece un parche de 2012.
Este enfoque es una bandera roja para las revisiones de código. Si veo algo así en un pull request, lo rechazo.
Enfoque 4: AsyncHelper o RunSync personalizado
Los desarrolladores experimentados a veces escriben sus propios helpers. Por ejemplo, este: