En programación, los retrasos son a menudo inevitables: las esperas a que termine una operación de entrada/salida, la llamada a un servicio externo o una operación costosa en una base de datos.
Pero también, en ocasiones, es al revés: somos nosotros los que queremos introducir una pausa en el código, de modo que espere un tiempo sin hacer nada. Esto puede tener varias utilidades prácticas, como por ejemplo:
- Sincronizar procesos que dependen de otros recursos externos, como archivos, bases de datos, redes, etc.
- Evitar sobrecargar el sistema con demasiadas peticiones o operaciones simultáneas, lo que podría provocar errores o ralentizar el rendimiento.
- Simular situaciones reales que implican tiempos de espera. Esto lo hacemos mucho en formación, por ejemplo, pero también en testing.
Aquí es donde entran en juego dos métodos comunes en el arsenal de todo desarrollador de .NET: Thread.Sleep()
y Task.Delay()
.
Vamos a explorarlos para ver sus similitudes y diferencias, cómo se comportan en diferentes situaciones y cuándo es más apropiado utilizar o el otro.
Comencemos con Thread.Sleep(), un método que existe desde la primera versión de .NET hace décadas, y que pertenece al espacio de nombres System.Threading
. Este método tiene una forma bastante sencilla de introducir un retraso: bloquea el hilo actual. Esto significa que el hilo completo no responde mientras dura el estado de "sueño".
Por otro lado tenemos Task.Delay(), una herramienta más moderna en el mundo de .NET, diseñada específicamente para la programación asíncrona. Forma parte de la Biblioteca de procesamiento paralelo basado en tareas (o TPL, de Task Parallel Library en el casi siempre más sucinto idioma inglés), y está en el espacio de nombres System.Threading.Tasks
. Nos permite introducir un retraso sin bloquear el hilo que realiza la llamada. Esto es especialmente crucial en escenarios donde la capacidad de respuesta es importante, como en aplicaciones con interfaces gráficas (GUI) o en operaciones del lado del servidor.
Para entender mejor la diferencia principal entre estos dos métodos, veamos cómo se comportan en el contexto de una aplicación simple, en la que tan solo vamos a mostrar por consola el identificador del hilo actual antes y después de llamar a estos dos métodos.
Fíjate en el siguiente fragmento en que el método principal está marcado como asíncrono, y que debemos usar await
a la hora de llamar a Task.Delay()
:
private static async Task Main()
{
Console.WriteLine($"ID del hilo ANTES de dormir: {Environment.CurrentManagedThreadId}");
Thread.Sleep(1000);
Console.WriteLine($"ID del hilo DESPUÉS de dormir: {Environment.CurrentManagedThreadId}");
Console.WriteLine($"ID del hilo ANTES de dormir: {Environment.CurrentManagedThreadId}");
await Task.Delay(1000);
Console.WriteLine($"ID del hilo DESPUÉS de dormir: {Environment.CurrentManagedThreadId}");
}
Este es el resultado de ejecutar este código (en este caso lo estoy ejecutando en LinqPad por comodidad):
Al verlo en ejecución es fácil darse cuenta de la gran diferencia que existe entre ambos métodos de detener la ejecución: Thread.Sleep()
hace que se conserve el identificador del hilo, mientras que tras ejecutar Task.Delay()
el código se sigue ejecutando en un hilo diferente.
Nota: la captura anterior está hecha con LinqPad y verás lo mismo en una app de consola. Sin embargo, si ejecutas el mismo código en un entorno como ASP.NET Core es posible que veas que, tras Task.Delay
, se recupera la ejecución en el mismo hilo. Esto es así porque ASP.NET Core es muy selectivo reutilizando los subprocesos de su pool de hilos, y en un entorno local de pruebas es muy probable que use siempre el mismo, puesto que éste queda liberado (vuelve al pool durante la operación asíncrona para poder atender otras posibles peticiones, junto a los demás hilos del pool), y es habitual en este entorno que ese mismo hilo se utilice también para retomar la respuesta. Sin embargo en aplicaciones reales con más carga generalmente no será así. Lo importante de la asincronía en ASP.NET (y en las apps Web en general) no es el rendimiento, sino la escalabilidad que permiten.
Y es que el método tradicional es síncrono, por lo que realmente detiene le ejecución del hilo actual. Al bloquear el hilo nada más puede funcionar mientras no regrese el Sleep()
, y por lo tanto no se puede refrescar o manipular la interfaz de usuario, no se pueden recibir notificaciones o peticiones, etc...
Sin embargo, el método de la TPL es asíncrono (por eso hemos tenido que marcar la función como async
y usar un await
, evitando así la "fontanería" necesaria para hacerlo funcionar sin ese azúcar sintáctico). Lo que hace cuando lo llamamos es liberar el hilo actual, y cuando termina la espera, introduce el código en otro hilo que esté libre para que se siga ejecutando. De ahí los dos identificadores diferentes. Este método no bloquea el hilo principal, y la aplicación puede seguir trabajando con normalidad, por lo que funcionará la interfaz de usuario y todo lo demás. De este modo introduces una espera o retardo en tu código sin afectar a nada más.
Pero entonces, ¿es malo utilizar Thread.Sleep()
? ¿Deberíamos evitarlo?
No necesariamente. Si estás trabajando en un proceso en segundo plano que no recibe interacción del usuario ni externa, no pasa nada por bloquear el hilo y puedes usarlo ahí sin problema. La excepción sería si vas a repetir el proceso muchos cientos de veces en poco tiempo en hilos diferentes, ya que el número de hilos del sistema es limitado. Pero para pruebas de desarrollo, testing unitario, una aplicación de consola, un proceso periódico... no hay problema ninguno en utilizarlo.
En el resto de los casos es mejor utilizar el retardo asíncrono de verdad que nos proporciona Task.Delay()
, especialmente en aplicaciones de escritorio en las que el usuario puede interaccionar con la interfaz mientras esperamos (o lanzamos una tarea en segundo plano, que en el fondo es lo mismo). Pero también en aplicaciones web, ya que los hilos del servidor Web son limitados y si los bloqueamos estamos disminuyendo su disponibilidad y por lo tanto la capacidad de respuesta del servidor.
¡Espero que te resulte útil!