¿Por qué es Thread.Sleep tan dañino?

128

A menudo veo que se menciona que Thread.Sleep();no debería usarse, pero no puedo entender por qué es así. Si Thread.Sleep();puede causar problemas, ¿hay alguna solución alternativa con el mismo resultado que sea segura?

p.ej.

while(true)
{
    doSomework();
    i++;
    Thread.Sleep(5000);
}

otro es:

while (true)
{
    string[] images = Directory.GetFiles(@"C:\Dir", "*.png");

    foreach (string image in images)
    {
        this.Invoke(() => this.Enabled = true);
        pictureBox1.Image = new Bitmap(image);
        Thread.Sleep(1000);
    }
}
Burimi
fuente
3
Un resumen del blog podría ser 'no use mal Thread.sleep ()'.
Martin James
55
No diría que es dañino. Prefiero decir que es como, goto:es decir, que probablemente haya una mejor solución a sus problemas que Sleep.
Predeterminado el
9
No es exactamente lo mismo goto, que se parece más a un olor a código que a un olor de diseño. No hay nada de malo en que un compilador inserte gotos en su código: la computadora no se confunde. Pero Thread.Sleepno es exactamente lo mismo; los compiladores no insertan esa llamada y tiene otras consecuencias negativas. Pero sí, el sentimiento general de que usarlo es incorrecto porque casi siempre hay una mejor solución es ciertamente correcto.
Cody Gray
31
Todos brindan opiniones sobre por qué los ejemplos anteriores son malos, pero nadie ha proporcionado una versión reescrita que no use Thread.Sleep () que aún cumple el objetivo de los ejemplos dados.
StingyJack
3
Cada vez que pones un sleep()código (o pruebas) muere un cachorro
Reza S

Respuestas:

163

Los problemas con las llamadas Thread.Sleepse explican sucintamente aquí :

Thread.Sleeptiene su uso: simula operaciones largas mientras se prueba / depura en un hilo MTA. En .NET no hay otra razón para usarlo.

Thread.Sleep(n)significa bloquear el subproceso actual durante al menos el número de intervalos de tiempo (o cuánticos de subprocesos) que pueden ocurrir en n milisegundos. La longitud de un intervalo de tiempo es diferente en diferentes versiones / tipos de Windows y diferentes procesadores y generalmente varía de 15 a 30 milisegundos. Esto significa que el hilo está casi garantizado para bloquear por más de nmilisegundos. La probabilidad de que su hilo vuelva a despertarse exactamente después de nmilisegundos es tan imposible como imposible. Entonces, no Thread.Sleeptiene sentido el tiempo .

Los subprocesos son un recurso limitado, toman aproximadamente 200,000 ciclos para crear y alrededor de 100,000 ciclos para destruir. De manera predeterminada, reservan 1 megabyte de memoria virtual para su pila y usan 2,000-8,000 ciclos para cada cambio de contexto. Esto hace que cualquier hilo de espera sea un gran desperdicio.

La solución preferida: WaitHandles

El error más cometido es usar Thread.Sleepuna construcción while ( demostración y respuesta , buena entrada de blog )

EDITAR:
Me gustaría mejorar mi respuesta:

Tenemos 2 casos de uso diferentes:

  1. Estamos a la espera porque sabemos un intervalo de tiempo específico cuando debemos seguir (de uso Thread.Sleep, System.Threading.Timero imitaciones)

  2. Estamos esperando porque alguna condición cambia en algún momento ... ¡las palabras clave son / son en algún momento ! si la verificación de condición está en nuestro dominio de código, deberíamos usar WaitHandles; de lo contrario, el componente externo debería proporcionar algún tipo de gancho ... ¡si no es así, su diseño es malo!

Mi respuesta cubre principalmente el caso de uso 2

Andreas Niedermair
fuente
29
No llamaría a 1 MB de memoria un gran desperdicio teniendo en cuenta el hardware de hoy
Predeterminado
14
@Defecto oye, discuta eso con el autor original :) y, siempre depende de su código, o mejor: el factor ... y el tema principal es "Los hilos son un recurso limitado" - los niños de hoy no saben mucho sobre la eficacia y el costo de ciertas implementaciones, porque "el hardware es barato" ... pero a veces es necesario codificar muy bien
Andreas Niedermair
11
'Esto hace que cualquier hilo de espera sea un gran desperdicio' ¿eh? Si algunas especificaciones de protocolo exigen una pausa de un segundo antes de continuar, ¿qué esperará durante 1 segundo? ¡Algún hilo, en algún lugar, tendrá que esperar! La sobrecarga para crear / destruir subprocesos a menudo es irrelevante porque un subproceso tiene que generarse de todos modos por otras razones y se ejecuta durante la vida útil del proceso. Me interesaría ver alguna forma de evitar los cambios de contexto cuando una especificación dice 'después de encender la bomba, espere al menos diez segundos para que la presión se estabilice antes de abrir la válvula de alimentación'.
Martin James
9
@CodyGray - Leí la publicación nuevamente. No veo peces de ningún color en mis comentarios. Andreas se retiró de la web: 'Thread.Sleep tiene su uso: simula operaciones largas mientras prueba / depura en un hilo MTA. En .NET no hay otra razón para usarlo '. Sostengo que hay muchas aplicaciones donde una llamada sleep () es, bueno, justo lo que se requiere. Si leigons de desarrolladores, (porque hay muchos), insista en usar los bucles sleep () como monitores de condición que deben reemplazarse con eventos / condvas / semas / lo que sea, eso no es justificación para una afirmación de que 'no hay otra razón para usarlo '.
Martin James
8
En 30 años de desarrollo de aplicaciones multiproceso (principalmente C ++ / Delphi / Windows), nunca he visto la necesidad de bucles sleep (0) o sleep (1) en ningún código entregable. Ocasionalmente, he introducido ese código con fines de depuración, pero nunca llegó al cliente. 'si está escribiendo algo que no controla completamente cada hilo': la microgestión de hilos es un error tan grande como la microgestión del personal de desarrollo. La gestión de subprocesos es para lo que el sistema operativo está allí: se deben usar las herramientas que proporciona.
Martin James
34

ESCENARIO 1: espere a que finalice la tarea asíncrona: estoy de acuerdo en que WaitHandle / Auto | ManualResetEvent se debe usar en un escenario en el que un subproceso está esperando que se complete la tarea en otro subproceso.

ESCENARIO 2: sincronización mientras que el bucle: Sin embargo, como un mecanismo de sincronización cruda (mientras que + Thread.Sleep) está perfectamente bien para el 99% de las aplicaciones que NO requiere saber exactamente cuándo el hilo bloqueado debería "despertarse *. El argumento que toma 200k ciclos para crear el subproceso también no es válido: el subproceso de bucle de sincronización debe crearse de todos modos y 200k ciclos es solo otro gran número (¿dime cuántos ciclos para abrir un archivo / socket / db llama?).

Entonces, si while + Thread.Sleep funciona, ¿por qué complicar las cosas? ¡Solo los abogados de sintaxis serían prácticos !

Swab.Jat
fuente
Afortunadamente, ahora tenemos TaskCompletionSource para esperar eficientemente un resultado.
Austin Salgat
13

Me gustaría responder a esta pregunta desde una perspectiva de política de codificación, que puede o no ser útil para nadie. Pero particularmente cuando se trata de herramientas destinadas a programadores corporativos de 9 a 5 años, las personas que escriben documentación tienden a usar palabras como "no debería" y "nunca" como "no hagas esto a menos que realmente sepas lo que estás haciendo y por qué ".

Algunos de mis otros favoritos en el mundo de C # son que le dicen que "nunca llame a lock (this)" o "nunca llame a GC.Collect ()". Estos dos se declaran enérgicamente en muchos blogs y documentación oficial, y la OMI son información completa. En cierto nivel, esta información errónea cumple su propósito, ya que mantiene a los principiantes lejos de hacer cosas que no entienden antes de investigar completamente las alternativas, pero al mismo tiempo, dificulta encontrar información REAL a través de motores de búsqueda que parece apuntar a artículos que le dicen que no haga algo sin ofrecer respuesta a la pregunta "¿por qué no?"

Políticamente, se reduce a lo que la gente considera "buen diseño" o "mal diseño". La documentación oficial no debe dictar el diseño de mi aplicación. Si realmente hay una razón técnica por la que no debería llamar a sleep (), entonces la documentación de la OMI debe indicar que está totalmente bien llamarlo bajo escenarios específicos, pero tal vez ofrecer algunas soluciones alternativas que sean independientes del escenario o más apropiadas para el otro escenarios

Claramente, llamar a "dormir ()" es útil en muchas situaciones cuando los plazos están claramente definidos en términos del tiempo real, sin embargo, hay sistemas más sofisticados para esperar y señalar hilos que deben considerarse y entenderse antes de comenzar a tirar sueño ( ) en su código, y arrojar declaraciones innecesarias sleep () en su código generalmente se considera una táctica para principiantes.

Jason Nelson
fuente
5

Es el bucle 1) .spinning y 2) .polling de sus ejemplos que la gente advierte contra , no la parte Thread.Sleep (). Creo que Thread.Sleep () generalmente se agrega para mejorar fácilmente el código que está girando o en un bucle de sondeo, por lo que solo está asociado con el código "incorrecto".

Además, las personas hacen cosas como:

while(inWait)Thread.Sleep(5000); 

donde no se accede a la variable inWait de manera segura para subprocesos, lo que también causa problemas.

Lo que los programadores quieren ver son los subprocesos controlados por eventos y construcciones de señalización y bloqueo, y cuando lo haga, no necesitará Thread.Sleep (), y las preocupaciones sobre el acceso variable seguro para subprocesos también se eliminan. Como ejemplo, ¿podría crear un controlador de eventos asociado con la clase FileSystemWatcher y usar un evento para activar su segundo ejemplo en lugar de bucle?

Como Andreas N. mencionó, leer Threading en C #, por Joe Albahari , es realmente muy bueno.

Miguel
fuente
Entonces, ¿cuál es la alternativa a hacer lo que está en su ejemplo de código?
shinzou
kuhaku: Sí, buena pregunta. Obviamente Thread.Sleep () es un atajo, y a veces un atajo grande. Para el ejemplo anterior, el resto del código en la función debajo del bucle de sondeo "while (inWait) Thread.Sleep (5000);" es una nueva función. Esa nueva función es un delegado (función de devolución de llamada) y la pasa a lo que esté configurando el indicador "inWait", y en lugar de cambiar el indicador "inWait", se invoca la devolución de llamada. Este fue el ejemplo más corto que pude encontrar: myelin.co.nz/notes/callbacks/cs-delegates.html
mike
Solo para asegurarme de que lo tengo, ¿te refieres a envolver Thread.Sleep()con otra función y llamar a eso en el ciclo while?
shinzou
5

El modo de suspensión se usa en los casos en que los programas independientes sobre los que no tiene control a veces pueden usar un recurso de uso común (por ejemplo, un archivo), al que su programa necesita acceder cuando se ejecuta, y cuando estos usan el recurso otros programas que su programa no puede usar. En este caso, cuando accede al recurso en su código, coloca su acceso al recurso en un try-catch (para detectar la excepción cuando no puede acceder al recurso), y lo coloca en un ciclo while. Si el recurso es gratuito, el sueño nunca se llama. Pero si el recurso está bloqueado, duerme durante un período de tiempo apropiado e intenta acceder al recurso nuevamente (por eso está haciendo un bucle). Sin embargo, tenga en cuenta que debe poner algún tipo de limitador en el bucle, por lo que no es un bucle potencialmente infinito.

Steve Greene
fuente
3

Tengo un caso de uso que no veo cubierto aquí, y argumentaré que esta es una razón válida para usar Thread.Sleep ():

En una aplicación de consola que ejecuta trabajos de limpieza, necesito hacer una gran cantidad de llamadas a bases de datos bastante caras, a una base de datos compartida por miles de usuarios concurrentes. Para no dañar el DB y excluir a otros durante horas, necesitaré una pausa entre llamadas, del orden de 100 ms. Esto no está relacionado con el tiempo, solo con el acceso a la base de datos para otros subprocesos.

Gastar 2000-8000 ciclos en el cambio de contexto entre llamadas que pueden tardar 500 ms en ejecutarse es benigno, al igual que tener 1 MB de pila para el hilo, que se ejecuta como una sola instancia en un servidor.

Henrik R Clausen
fuente
Yeap, usar Thread.Sleepuna aplicación de consola de un solo hilo y un solo propósito como la que usted describe está perfectamente bien.
Theodor Zoulias
-7

Estoy de acuerdo con muchos aquí, pero también creo que depende.

Recientemente hice este código:

private void animate(FlowLayoutPanel element, int start, int end)
{
    bool asc = end > start;
    element.Show();
    while (start != end) {
        start += asc ? 1 : -1;
        element.Height = start;
        Thread.Sleep(1);
    }
    if (!asc)
    {
        element.Hide();
    }
    element.Focus();
}

Era una simple función animada, y solía Thread.Sleep .

Mi conclusión, si hace el trabajo, úsalo.


fuente
-8

Para aquellos de ustedes que no han visto un argumento válido contra el uso de Thread.Sleep en SCENARIO 2, realmente hay uno: la salida de la aplicación se mantiene por el ciclo while (SCENARIO 1/3 es simplemente estúpido, así que no merece más mencionando)

Muchos de los que fingen estar al tanto, gritando Thread.Sleep is evil no mencionaron una sola razón válida para aquellos de nosotros que exigimos una razón práctica para no usarla, pero aquí está, gracias a Pete - Thread.Sleep es malvado (se puede evitar fácilmente con un temporizador / controlador)

    static void Main(string[] args)
    {
        Thread t = new Thread(new ThreadStart(ThreadFunc));
        t.Start();

        Console.WriteLine("Hit any key to exit.");
        Console.ReadLine();

        Console.WriteLine("App exiting");
        return;
    }

    static void ThreadFunc()
    {
        int i=0;
        try
        {
            while (true)
            {
                Console.WriteLine(Thread.CurrentThread.ThreadState.ToString() + " " + i);

                Thread.Sleep(1000 * 10);
                i++;
            }
        }
        finally
        {
            Console.WriteLine("Exiting while loop");
        }
        return;
    }
Swab.Jat
fuente
9
¡No, Thread.Sleepno es la razón de esto (el nuevo hilo y el continuo while-loop es)! puede eliminar la línea Thread.Sleep- et voila: el programa no se cerrará también ...
Andreas Niedermair