Yeda AI Tips · #107

English

Result y Wait son deadlocks en espera

Este hábito de C# congela apps web. Un solo .Result en medio de una cadena async funciona bien en tu máquina, funciona bien en la demo, y luego congela un hilo de request en producción. Bajo carga es peor: muchos hilos bloqueados a la vez agotan el thread pool y el servicio entero deja de responder. Trata cada .Result y .Wait como un deadlock a punto de ocurrir.

Por qué una línea puede congelar un request

Cuando haces await sobre una task incompleta, por defecto se captura el contexto actual y se usa para reanudar el método. En ASP.NET clásico y en apps con GUI, ese contexto solo permite ejecutar un fragmento de código a la vez. Ahora bloquea ese mismo hilo de contexto con .Result: la continuación del await queda encolada en un contexto que el hilo bloqueado posee, y ese hilo no lo va a soltar hasta que la task termine. Cada lado espera al otro — un deadlock clásico. Stephen Cleary dice que es la pregunta más frecuente de quienes empiezan con async: "¿Por qué mi código parcialmente async hace deadlock?"

El mismo código corre bien en una app de consola (contexto de thread pool, sin deadlock), y justo por eso sobrevive las pruebas locales y muere en la app web.

ASP.NET Core eliminó ese contexto por request, así que el deadlock de libro es más raro — pero bloquear sigue siendo veneno ahí. La guía de Microsoft es directa: "Muchas llamadas bloqueantes síncronas llevan a la inanición del Thread Pool y a tiempos de respuesta degradados." Un pool dimensionado para miles de requests async concurrentes colapsa cuando cada request retiene un hilo esperando .Result.

La solución: async de entrada, async de salida

La asincronía es viral. Cuando una llamada es async, todos los callers por encima también deberían serlo — "los esfuerzos por ser async no sirven de nada a menos que toda la pila de llamadas sea async" (guía de David Fowler para ASP.NET Core). La tabla de traducción es corta:

En lugar de…Usa
task.Result / task.Wait()await task
Task.WaitAny(...)await Task.WhenAny(...)
Task.WaitAll(...)await Task.WhenAll(...)
Thread.Sleep(1000)await Task.Delay(1000)

En concreto:

// Deadlock (or starved thread) waiting to happen:
public IActionResult Get()
{
    var data = _service.LoadAsync().Result;   // blocks the request thread
    return Ok(data);
}

// Async all the way down:
public async Task<IActionResult> Get(CancellationToken ct)
{
    var data = await _service.LoadAsync(ct);
    return Ok(data);
}

Fíjate en el CancellationToken: el modelo de cancelación cooperativa de .NET espera que aceptes un token y lo pases por toda la cadena de llamadas, para que un cliente que se desconecta (o un timeout) realmente detenga el trabajo en vez de quemar un hilo en una respuesta que nadie va a leer.

Notas para usuarios avanzados

Recursos

Read in English

¿Construyendo una funcionalidad con AI? Yeda AI diseña, audita y entrega sistemas LLM de producción.

Habla con nosotros · Lee el blog