Llamada asíncrona desde código síncrono en C#: batalla de enfoques

Entrenador de Python en Línea para Principiantes

Aprende Python fácilmente sin sobrecargarte de teoría. Resuelve tareas prácticas con verificación automática, recibe pistas y escribe código directamente en el navegador, sin instalar nada.

Start Course

¿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 en InnerException.
  • 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:

                

Blogs

Book Recommendations