Я говорю о неверном использовании функции Sleep (регистр может отличаться в зависимости от языка программирования и платформы). Итак, что же такое Sleep? Документация отвечает на этот вопрос предельно просто: это пауза в выполнении текущего потока на указанное количество миллисекунд. Нельзя не отметить эстетическую красоту прототипа данной функции:
void Sleep(DWORD dwMilliseconds);
Всего один параметр (предельно понятный), никаких кодов ошибок или исключений — работает всегда. Таких приятных и понятных функций очень мало!
Что же могло пойти не так? То, что программисты используют эту замечательную функцию не для того, для чего она предназначена.
А предназначена она для программной симуляции какого-то внешнего, определённого чем-то реальным, процесса паузы.
Корректный пример №1
Мы пишем приложение «часы», в котором раз в секунду нужно менять цифру на экране (или положение стрелки). Функция Sleep здесь подходит как нельзя лучше: нам реально нечего делать чётко определённый промежуток времени (ровно одну секунду). Почему бы и не поспать?
Корректный пример №2
Мы пишем контроллер самогонного аппарата хлебопечки. Алгоритм работы задаётся одной из программ и выглядит примерно так:
- Перейти в режим 1.
- Проработать в нём 20 минут
- Перейти в режим 2.
- Проработать в нём 10 минут
- Выключиться.
Здесь тоже всё чётко: мы работаем со временем, оно задано технологическим процессом. Использование Sleep — приемлемо.
А теперь посмотрим на примеры неверного использования Sleep.
Когда мне нужен какой-то пример некорректного кода на С++ — я иду в репозиторий кода текстового редактора Notepad++. Его код ужасен настолько, что любой антипаттерн там точно найдётся, я об этом даже статью когда-то писал. Не подвёл меня ноутпадик++ и в этот раз! Давайте посмотрим, как в нём используется Sleep.
Плохой пример №1
При старте Notepad++ проверяет, не запущен ли уже другой экземпляр его процесса и, если это так, ищет его окно и отправляет ему сообщение, а сам закрывается. Для детектирования другого своего процесса используется стандартный способ — глобальный именованный мьютекс. Но вот для поиска окон написан следующий код:
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) { ... } ... }
Программист, писавший этот код, попытался найти окно уже запущенного Notepad++ и даже предусмотрел ситуацию, когда два процесса были запущены буквально одновременно, так что первый из них уже создал глобальный мьютекс, но ещё не создал окно редактора. В этом случае второй процесс будет ждать создания окна «5 раз по 100 мс». В итоге мы или не дождёмся вообще, или потеряем до 100 мс между моментом реального создания окна и выходом из Sleep.
Это и есть первый (и один из главных) антипаттернов использования Sleep. Мы ждём не наступление события, а «сколько-то миллисекунд, вдруг повезёт». Ждём столько, чтобы с одной стороны не очень раздражать пользователя, а с другой стороны — иметь шанс дождаться нужного нам события. Да, пользователь может не заметить паузы в 100 мс при старте приложения. Но если подобная практика «ждать сколько-нибудь от балды» будет принята и допустима в проекте — закончиться это может тем, что ждать мы будем на каждом шагу по самым мелочным причинам. Здесь 100 мс, там ещё 50 мс, а здесь вот 200 мс — и вот у нас программа уже «почему-то тормозит несколько секунд».
Кроме того, просто эстетически неприятно видеть код, работающий долго в то время, как он мог бы работать быстро. В данном конкретном случае можно было бы использовать функцию SetWindowsHookEx, подписавшись на событие HSHELL_WINDOWCREATED — и получить нотификацию о создании окна мгновенно. Да, код становиться чуть сложнее, но буквально на 3-4 строки. И мы выигрываем до 100 мс! А самое главное — мы больше не используем функции безусловного ожидания там, где ожидание не является безусловным.
Плохой пример №2
HANDLE hThread = ::CreateThread(NULL, 0, threadTextTroller, &trollerParams, 0, NULL); int sleepTime = 1000 / x * y; ::Sleep(sleepTime);
Я не очень разбирался, чего конкретно и как долго ждёт этот код в Notepad++, но общий антипаттерн «запустить поток и подождать» я видел часто. Люди ждут разного: начала работы другого потока, получения из него каких-то данных, окончания его работы. Плохо здесь сразу два момента:
- Многопоточное программирование нужно для того, чтобы делать что-то многопоточно. Т.е. запуск второго потока предполагает, что мы что-то продолжим делать в первом, в это время второй поток выполнит другую работу, а первый, закончив свои дела (и, возможно, ещё немного подождав), получит её результат и как-то его использует. Если мы начинаем «спать» сразу же после запуска второго потока — зачем он вообще нужен?
- Ожидать нужно правильно. Для правильного ожидания существуют проверенные практики: использование событий, wait-функций, вызов колбеков. Если мы ждём начала работы кода во втором потоке — заведите для этого событие и сигнальте его во втором потоке. Если мы ждём завершения работы второго потока — в С++ есть замечательный класс thread и его метод join (ну или опять-таки платформенно-зависимые способы типа WaitForSingleObject и HANDLE в Windows). Ждать выполнения работы в другом потоке «сколько-то миллисекунд» попросту глупо, поскольку если у нас не ОС реального времени — никто вам не даст никакой гарантии за сколько времени тот второй поток запустится или дойдёт до какого-то этапа своей работы.
Плохой пример №3
Здесь мы видим фоновый поток, который спит в ожидании каких-то событий.
class CReadChangesServer { ... void Run() { while (m_nOutstandingRequests || !m_bTerminate) { ::SleepEx(INFINITE, true); } } ... void RequestTermination() { m_bTerminate = true; ... } ... bool m_bTerminate; };
Нужно признать, что здесь используется не Sleep, а SleepEx, который более интеллектуален и может прерывать ожидание при некоторых событиях (типа завершения асинхронных операций). Но это нисколько не помогает! Дело в том, что цикл while (!m_bTerminate) имеет полное право работать бесконечно, игнорируя вызванный из другого потока метод RequestTermination(), сбрасывающий переменную m_bTerminate в true. О причинах и следствия этого я писал в предыдущей статье. Для избегания этого следовало бы использовать что-то, гарантированно правильно работающее между потоками: atomic, event или что-то подобное.
Да, формально SleepEx не виноват в проблеме использования обычной булевой переменной для синхронизации потоков, это отдельная ошибка другого класса. Но почему она стала возможной в этом коде? Потому, что сначала программист подумал «тут надо спать», а затем задумался как долго и по какому условию прекратить это делать. А в правильном сценарии у него даже и не должно было бы возникнуть первой мысли. В голове должно была бы возникнуть мысль «тут надо ожидать события» — и вот с этого момента мысль уже бы работала в сторону выбора правильного механизма синхронизации данных между потоками, который исключил бы как булевскую переменную, так и использование SleepEx.
Некоректный пример №4
В этом примере мы посмотрим на функцию backupDocument, которая выполняет роль «автосохранялки», полезной на случай непредвиденного падения редактора. По-умолчанию она спит 7 секунд, затем даёт команду сохранить изменения (если они были).
DWORD WINAPI Notepad_plus::backupDocument(void * /*param*/) { ... while (isSnapshotMode) { ... ::Sleep(DWORD(timer)); ... ::PostMessage(Notepad_plus_Window::gNppHWND, NPPM_INTERNAL_SAVEBACKUP, 0, 0); } return TRUE; }
Интервал поддаётся изменению, но не в этом беда. Любой интервал будет одновременно слишком большим и слишком малым. Если мы набираем одну букву в минуту — нет никакого смысла спать всего 7 секунд. Если мы откуда-то копипастим 10 мегабайт текста — не нужно ждать после этого ещё 7 секунд, это достаточно большой объём, чтобы инициировать бекап немедленно (вдруг мы его откуда-то вырезали и там его не осталось, а редактор через секунду крешнется).
Т.е. простым ожиданием мы здесь заменяем отсутствующий более интеллектуальный алгоритм.
Плохой пример №5
Notepad++ умеет «набирать текст» — т.е. эмулировать ввод текста человеком, делая паузы между вставкой букв. Вроде бы писалось это как «пасхальное яйцо», но можно придумать и какое-нибудь рабочее применение этой фиче (дурить Upwork, ага).
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);
Беда здесь в том, что в код вшито представление о каком-то «среднем человеке», делающем паузу 400-800 мс между каждой нажатой клавишей. Ок, может это «в среднем» и нормально. Но вы знаете, если используемая мною программа делает какие-то паузы в своей работы просто потому, что они кажутся ей красивыми и подходящими — это совсем не значит, что я разделяю её мнение. Мне хотелось бы иметь возможность настройки длительности данных пауз. И, если в случае Notepad++ это не очень критично, то в других программах мне иногда встречались настройки типа «обновлять данные: часто, нормально, редко», где «часто» не было для меня достаточно часто, а «редко» — не было достаточно редко. Да и «нормально» не было нормально. Подобный функционал должен давать пользователю возможность точно указать количество миллисекунд, который он хотел бы ждать до выполнения нужного действия. С обязательной возможностью ввести «0». Причём 0 в данном случае вообще не должен даже передаваться аргументом в функцию Sleep, а просто исключать её вызов (Sleep(0) на самом деле не возвращается мгновенно, а отдаёт оставшийся кусок выданного планировщиком временного слота другому потоку).
Выводы
С помощью Sleep можно и нужно выполнять ожидание тогда, когда это именно безусловно заданное ожидание в течение конкретного промежутка времени и есть какое-то логическое объяснение, почему он такой: «по техпроцессу», «время рассчитано вот по этой формуле», «столько ждать сказал заказчик». Ожидание каких-то событий или синхронизация потоков не должны реализовываться с использованием функции Sleep.
ссылка на оригинал статьи https://habr.com/company/infopulse/blog/427843/
Добавить комментарий