Menú de navegaciónMenú
Categorías

La mejor forma de Aprender Programación online y en español www.campusmvp.es

Thread.Sleep vs Task.Delay en .NET: cómo introducir retardos en tu código correctamente

Imagen ornamental, un logo de .NET con un letrero electrónico gigante que dice "EXpect Delays"

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):

Resultado de la ejecución, que muestra dos ids de hilo diferentes al elecutar Task.Delay

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!

José M. Alarcón Aguín Fundador de campusMVP, es ingeniero industrial y especialista en consultoría de empresa. Ha escrito diversos libros, habiendo publicado hasta la fecha cientos de artículos sobre informática e ingeniería en publicaciones especializadas. Microsoft lo ha reconocido como MVP (Most Valuable Professional) en desarrollo web desde el año 2004 hasta la actualidad. Puedes seguirlo en Twitter en @jm_alarcon o leer sus blog técnico o personal. Ver todos los posts de José M. Alarcón Aguín
Archivado en: Lenguajes y plataformas

Boletín campusMVP.es

Solo cosas útiles. Una vez al mes.

🚀 Únete a miles de desarrolladores

DATE DE ALTA

x No me interesa | x Ya soy suscriptor

La mejor formación online para desarrolladores como tú

Agregar comentario

Los datos anteriores se utilizarán exclusivamente para permitirte hacer el comentario y, si lo seleccionas, notificarte de nuevos comentarios en este artículo, pero no se procesarán ni se utilizarán para ningún otro propósito. Lee nuestra política de privacidad.