No hace mucho tiempo, nos pasó un buen artículo sobre el terrible estado de rendimiento del software moderno (
original en inglés ,
traducción en Habré ). Este artículo me recordó a un antipatrón del código, que es muy común y generalmente funciona de alguna manera, pero conduce a pequeñas pérdidas de rendimiento aquí y allá. Bueno, ya sabes, un poco, qué manos no alcanzarán de ninguna manera. El único problema es que una docena de estas "bagatelas" dispersas en diferentes lugares del código comienzan a generar problemas como "Tengo el último Intel Core i7 y las contracciones de desplazamiento".

Estoy hablando del uso incorrecto de la función de
suspensión (el caso puede variar según el lenguaje de programación y la plataforma). Entonces, ¿qué es el sueño? La documentación responde a esta pregunta de manera muy simple: esta es una pausa en la ejecución del subproceso actual para el número especificado de milisegundos. Cabe destacar la belleza estética del prototipo de esta función:
void Sleep(DWORD dwMilliseconds);
Solo un parámetro (extremadamente claro), sin códigos de error ni excepciones: siempre funciona. ¡Hay muy pocas funciones agradables y comprensibles!
Obtiene aún más respeto por esta función cuando lee cómo funciona.La función va al programador de subprocesos del sistema operativo y le dice: “Mi subproceso y yo quisiéramos rechazar el tiempo de CPU asignado a nosotros, ahora y durante tantos milisegundos en el futuro. ¡Dale a los pobres! El planificador, un poco sorprendido por tal generosidad, realiza las funciones de gratitud en nombre del procesador, le da el tiempo restante a la siguiente persona (y siempre hay esas) y no incluye el hilo que hizo que se pretendiera que Sleep le transmitiera el contexto de ejecución durante el número especificado de milisegundos. Belleza!
¿Qué pudo haber salido mal? El hecho de que los programadores usen esta maravillosa característica no es para lo que está destinado.
Y está destinado a la simulación de software de algún proceso externo, definido por algo real, pausa.
Ejemplo correcto número 1
Estamos escribiendo la aplicación "reloj", en la que una vez por segundo debe cambiar el número en la pantalla (o la posición de la flecha). La función Sleep aquí se adapta perfectamente: realmente no tenemos nada que hacer durante un período de tiempo claramente definido (exactamente un segundo). ¿Por qué no dormir?
Ejemplo correcto número 2
Estamos escribiendo un controlador de
luz de luna para una máquina de pan. El algoritmo de operación lo establece uno de los programas y se ve más o menos así:
- Ir al modo 1.
- Trabajar en él durante 20 minutos.
- Ir al modo 2.
- Trabaja en él por 10 minutos
- Apagar
Todo aquí también está claro: trabajamos con el tiempo, lo establece el proceso tecnológico. Usar el sueño es aceptable.
Ahora veamos ejemplos del mal uso del sueño.
Cuando necesito algún ejemplo de código C ++ incorrecto, voy al
repositorio de código del editor de texto Notepad ++. Su código es tan terrible que cualquier antipatrón definitivamente está allí, incluso escribí
un artículo sobre esto una vez. ¡Notepad ++ tampoco me decepcionó esta vez! Veamos cómo usa el sueño.
Mal ejemplo número 1
Al inicio, Notepad ++ verifica si ya se está ejecutando otra instancia de su proceso y, de ser así, busca su ventana y le envía un mensaje, y se cierra. Para detectar otro proceso, se utiliza un método estándar: el mutex global llamado. Pero el siguiente código fue escrito para buscar ventanas:
if ((!isMultiInst) && (!TheFirstOne)) { HWND hNotepad_plus = ::FindWindow(Notepad_plus_Window::getClassName(), NULL); for (int i = 0 ;!hNotepad_plus && i < 5 ; ++i) { Sleep(100); hNotepad_plus = ::FindWindow(Notepad_plus_Window::getClassName(), NULL); } if (hNotepad_plus) { ... } ... }
El programador que escribió este código intentó encontrar una ventana para Notepad ++ que ya se había lanzado e incluso imaginó una situación en la que dos procesos se iniciaron literalmente al mismo tiempo, por lo que el primero de ellos ya creó un mutex global, pero aún no creó una ventana de editor. En este caso, el segundo proceso esperará la creación de la ventana "5 veces en 100 ms". Como resultado, no esperaremos en absoluto o perderemos hasta 100 ms entre el momento de la creación real de la ventana y la salida de la suspensión.
Este es el primer (y uno de los principales) antipatrones del uso del sueño. No estamos esperando que ocurra el evento, pero "por unos pocos milisegundos, de repente tendrá suerte". Estamos esperando tanto que, por un lado, realmente no molestamos al usuario, y por otro lado, tenemos la oportunidad de esperar el evento que necesitamos. Sí, el usuario puede no notar una pausa de 100 ms al iniciar la aplicación. Pero si tal práctica de "esperar un poco desde la excavadora" es aceptada y aceptable en el proyecto, puede terminar con el hecho de que esperaremos en cada paso por las razones más insignificantes. Aquí 100 ms, hay otros 50 ms, y aquí 200 ms, y aquí nuestro programa ya "de alguna manera se está desacelerando durante unos segundos".
Además, es simplemente estéticamente desagradable ver código que se ejecuta durante mucho tiempo y que podría funcionar rápidamente. En este caso particular, uno podría usar la función
SetWindowsHookEx , suscribiéndose al evento HSHELL_WINDOWCREATED, y recibir una notificación de la creación de ventanas al instante. Sí, el código se vuelve un poco más complicado, pero literalmente 3-4 líneas. ¡Y ganamos hasta 100 ms! Y lo más importante: ya no usamos las funciones de expectativa incondicional cuando la expectativa no es incondicional.
Mal ejemplo número 2
HANDLE hThread = ::CreateThread(NULL, 0, threadTextTroller, &trollerParams, 0, NULL); int sleepTime = 1000 / x * y; ::Sleep(sleepTime);
Realmente no entendí exactamente qué y durante cuánto tiempo estuvo esperando este código en Notepad ++, pero a menudo vi el antipatrón general "iniciar la transmisión y esperar". La gente espera cosas diferentes: el inicio de otra secuencia, la recepción de algunos datos de ella, el final de su trabajo. Dos cosas son malas aquí de inmediato:
- La programación multiproceso es necesaria para hacer algo multiproceso. Es decir el lanzamiento del segundo subproceso supone que continuaremos haciendo algo en el primero, en este momento el segundo subproceso realizará otro trabajo y el primero, una vez que haya terminado su trabajo (y, tal vez, haya esperado un poco más), obtendrá su resultado y lo usará de alguna manera. Si comenzamos a "dormir" inmediatamente después de comenzar el segundo hilo, ¿por qué es necesario?
- Es necesario esperar correctamente. Para una expectativa adecuada, existen prácticas comprobadas: el uso de eventos, funciones de espera, llamadas de devolución de llamada. Si estamos esperando que el código comience a funcionar en el segundo hilo, configure un evento para esto y señaléelo en el segundo hilo. Si estamos esperando que el segundo subproceso termine de funcionar, en C ++ hay una maravillosa clase de subproceso y su método de unión (bueno, o, nuevamente, métodos específicos de la plataforma como WaitForSingleObject y HANDLE en Windows). Esperar a que el trabajo se complete en otro subproceso "durante unos pocos milisegundos" es simplemente estúpido, porque si no tenemos un sistema operativo en tiempo real, nadie le dará ninguna garantía de cuánto tiempo comenzará ese segundo subproceso o llegará a alguna etapa de su trabajo.
Mal ejemplo número 3
Aquí vemos un hilo de fondo que está durmiendo esperando algunos eventos.
class CReadChangesServer { ... void Run() { while (m_nOutstandingRequests || !m_bTerminate) { ::SleepEx(INFINITE, true); } } ... void RequestTermination() { m_bTerminate = true; ... } ... bool m_bTerminate; };
Debo admitir que no se usa Sleep, sino
SleepEx , que es más inteligente y puede interrumpir la espera de algunos eventos (como la finalización de operaciones asincrónicas). ¡Pero esto no ayuda en absoluto! El hecho es que el ciclo while (! M_bTerminate) tiene todo el derecho de trabajar sin cesar, ignorando el método RequestTermination () llamado desde otro hilo, restableciendo la variable m_bTerminate a verdadero. Escribí sobre las causas y consecuencias de esto en un
artículo anterior . Para evitar esto, debe usar algo garantizado para funcionar correctamente entre hilos: atómico, evento o algo similar.
Sí, formalmente SleepEx no tiene la culpa del problema de usar la variable booleana habitual para sincronizar hilos, este es un error separado de otra clase. Pero, ¿por qué se hizo posible en este código? Porque al principio el programador pensó "necesitas dormir aquí", y luego pensó cuánto tiempo y bajo qué condiciones dejar de hacerlo. Y en el escenario correcto, ni siquiera debería tener un primer pensamiento. El pensamiento debería haber ocurrido en mi cabeza "aquí deberíamos esperar un evento", y desde ese momento en adelante el pensamiento habría funcionado para elegir el mecanismo correcto para sincronizar datos entre flujos, lo que excluiría tanto la variable booleana como el uso de SleepEx.
Mal ejemplo número 4
En este ejemplo, veremos la función backupDocument, que actúa como un "autoguardado", útil en caso de un bloqueo inesperado del editor. Por defecto, duerme durante 7 segundos, luego le da el comando para guardar los cambios (si lo fueran).
DWORD WINAPI Notepad_plus::backupDocument(void * ) { ... while (isSnapshotMode) { ... ::Sleep(DWORD(timer)); ... ::PostMessage(Notepad_plus_Window::gNppHWND, NPPM_INTERNAL_SAVEBACKUP, 0, 0); } return TRUE; }
El intervalo se puede cambiar, pero este no es el problema. Cualquier intervalo será demasiado largo y demasiado corto al mismo tiempo. Si escribimos una letra por minuto, no tiene sentido dormir solo por 7 segundos. Si copiamos y pegamos 10 megabytes de texto de algún lugar, no tenemos que esperar otros 7 segundos después de eso, es un volumen lo suficientemente grande como para iniciar la copia de seguridad de inmediato (de repente, lo cortamos de algún lado y desaparece allí, y el editor se bloquea después de un segundo).
Es decir con una simple expectativa, reemplazamos el algoritmo más inteligente que falta aquí.
Mal ejemplo número 5
Notepad ++ puede "escribir texto", es decir emule el ingreso de texto humano haciendo una pausa entre la inserción de la letra. Parece estar escrito como un "huevo de Pascua", pero puedes encontrar algún tipo de aplicación funcional de esta función (
engañar a Upwork, sí ).
int pauseTimeArray[nbPauseTime] = {200,400,600}; const int maxRange = 200; ... int ranNum = getRandomNumber(maxRange); ::Sleep(ranNum + pauseTimeArray[ranNum%nbPauseTime]); ::SendMessage(pCurrentView->getHSelf(), SCI_DELETEBACK, 0, 0);
El problema aquí es que el código tiene una idea de algún tipo de "persona promedio" haciendo una pausa de 400-800 ms entre cada tecla presionada. Ok, tal vez sea "promedio" y normal. Pero sabes, si el programa que utilizo hace algunas pausas en mi trabajo simplemente porque parecen hermosas y adecuadas para ella, esto no significa en absoluto que comparta su opinión. Me gustaría poder ajustar la duración de los datos de pausa. Y, si en el caso de Notepad ++ esto no es muy crítico, en otros programas a veces me encontré con configuraciones como "actualizar datos: a menudo, normalmente, rara vez", donde "a menudo" no era para mí con la frecuencia suficiente, y "rara vez" no era suficiente raramente Sí, y "normal" no era normal. Dicha funcionalidad debería permitir al usuario indicar con precisión el número de milisegundos que le gustaría esperar hasta que se complete la acción deseada. Con la opción obligatoria de ingresar "0". Además, 0 en este caso ni siquiera debería pasarse como un argumento para la función Sleep, sino que simplemente excluye su llamada (Sleep (0) en realidad no regresa instantáneamente, sino que da el resto del intervalo de tiempo dado por el programador a otro hilo).
Conclusiones
Con la ayuda de Sleep, uno puede y debe cumplir una expectativa cuando es una expectativa asignada incondicionalmente por un período de tiempo específico y hay alguna explicación lógica de por qué es así: "según el proceso tecnológico", "el tiempo se calcula según esta fórmula", "espera mucho dijo el cliente ". La espera de algunos eventos o la sincronización de subprocesos no se debe implementar con la función de suspensión.