Я говорю о неверном использовании функции 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++ умеет «набирать текст» — т.е. эмулировать ввод текста человеком, делая паузы между вставкой букв. Вроде бы писалось это как «пасхальное яйцо», но можно придумать и какое-нибудь рабочее применение этой фиче (
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.
Комментарии (23)
Endeavour
26.10.2018 20:24+1Плохой пример 1: а автор подумал о гонке между установкой хука на создание окна, и созданием этого самого окна? Кроме того, по коду видно, что засыпает только процесс, задача которого — отдать основному сообщение об открытом файле, и не создающий никакого интерфейса, который мог бы тормозить.
tangro Автор
28.10.2018 21:11Так нужно сначала ставить хук, поток делать FindWindow и, если окно нашлось — снимать хук, а если нет — ждать событие. Мне это казалось настолько очевидным, что я даже не расписывал.
«засыпает только процесс, задача которого — отдать основному сообщение об открытом файле» — и того юзер ждёт открытия своего файла на 100 мс дольше, чем мог бы ждать.
johnfound
26.10.2018 20:39У меня очень давно сформулировано:
Правило №1
Использование таймера для управления чего нибудь, всегда ошибочно.Таймер, это всегда эмулятор события. И этот эмулятор, рано или поздно ошибается. Правда, иногда, просто невозможно найти реальный источник сигналов о событиях ("Корректный пример" №2) и тогда приходится использовать таймеры. Но всегда надо ясно осознавать, что это компромисс и все равно ошибка, а программа написанная таким образом, работает неправильно.
Temtaime
27.10.2018 00:12+1А что насчёт Sleep(0)?
vanxant
27.10.2018 05:26Вот да, зашёл почитать про sleep(0) и просто с очень малыми значениями. Особенно если нет SleepEx() или аналога
F0iL
27.10.2018 13:05На всякий случай напомню, если кто увидит коммент про sleep(0), заинтересуется и захочет использовать: в Linux sleep(0) может не сработать, но для такого же есть sched_yield() и std::this_thread::yield().
johnfound
27.10.2018 13:34Sleep(0) это не таймер вообще, а управление планировщика. Если я правильно понимаю о чем вы.
Но говоря вообще, меньше интервал таймера – более правильная программа. В пределе, интервал 0 дает правильный алгоритм.
catharsis
27.10.2018 09:15Exponential backoff — хорошее или плохое использование таймера?
tangro Автор
28.10.2018 21:20Хороший вопрос. Там, где она используется, это происходит от безнадёги — возможности получить сообщение о том, когда можно передавать данные, просто нет. Поэтому алгоритм подбора выдержки использует время в качестве инструмента нахождения консенсуса просто потому, что других инструментов нет.
emmibox
27.10.2018 11:18+1Функция идёт к планировщику потоков ОС и говорит ему «мы с моим потоком хотели бы отказаться от выделенного нам ресурса процессорного времени, сейчас и ещё на вот столько-то миллисекунд в будущем. Отдайте бедным!». Слегка удивлённый подобной щедростью планировщик выносит функции благодарность от имени процессора, отдаёт оставшийся кусок времени следующему желающему (а такие всегда найдутся) и не включает вызвавший Sleep поток в претенденты на передачу ему контекста выполнения на указанное количество миллисекунд. Красота!
Нет красоты там нет… Вы забыли, что время там выделяется дискретными кусками в зависимости от задания timeBeginPeriod (что в свою очередь зависит от самой ОС и запущенного софта, который в этот timeBeginPeriod полез и поставил то что ему нравится). Отсюда Sleep(1) может длится как примерно 1.1мс так и примерно 15.6мс.tangro Автор
28.10.2018 21:23Везде, где тестил последние лет 10, Sleep(1) работал около 1 мс. Паузу в 16 мс при Sleep(1) я реально последний раз видел году в 2005-ом на Windows XP.
emmibox
28.10.2018 22:45+1Вообще то во времена Win8 Микрософт возвращала 16мс в каких то из апдейтов на ноутбуках, по той причине, что при кванте 16мс значительно меньше потребление энергии от батарейки — больше времени работы в режиме ожидания. В то же время куча программ откручивают назад на 1 (зачастую не имея для этого оснований).
Поэтому поведение этой функции далеко не всегда соответствует ожидаемому.
third112
27.10.2018 12:55Спасибо! Понравилось — очень образно!:
Слегка удивлённый подобной щедростью планировщик выносит функции благодарность от имени процессора
В общем ИМХО тема очень сложная — была бы простая, все бы приложения сейчас не тормозили даже на средних ПК :)
Ждать выполнения работы в другом потоке «сколько-то миллисекунд» попросту глупо, поскольку если у нас не ОС реального времени — никто вам не даст никакой гарантии за сколько времени тот второй поток запустится или дойдёт до какого-то этапа своей работы.
Long time ago работал на уже тогда старой RT11 — и в ней было не просто со «Sleep» :)
assad77
первый хороший пример — на самом деле плохой.
в таких часах иногда 2 секунды будет одна и та же цифра а потом сразу скачок на две секунды.
Да и второй вариант в гуи приложении не очень хороший, особенно если такое будет делаться в главном потоке. такой подход удобен своей простотой в контроллерах.
ookami_kb
Да как бы вообще thread – штука тяжелая, все вроде как давно уже пришли к концепции thread pool. А тут вот так раз – и пусть себе целый thread простаивает.
По мне, так в 99% случаев sleep вообще использовать не стоит. Даже в тестах.
TimsTims
Тоже хотел про это сказать)
Про пароварку так вообще — нужно же рассчитывать, что свет может моргнуть (значит надо помнить сколько ещё готовить осталось), секунда может быть не секундой, а 1,1с, пользователь может захотеть изменить блюдо, но не сможет, те sleep заблокировал поток(отдал ос) итд итп.
NetBUG
А главное, на тех же МК эти задачи отлично решаются с помощью машины состояний и глобального таймера (неважно, реального времени или нет). Если очень хочется, то с помощью ОСРВ.
ser-mk
И чем же вам моргание света мешает использовать sleep?
TimsTims
Моргнул свет — система перезагрузилась, все настройки сбиты, текущее время в пароварке/мультиварке неизвестно (обычно в них нет батарейки для часов). Значит, неизвестно сколько блюдо готовилось до перезагрузки, и сколько ещё надо продолжать его готовить после перезагрузки, и в каком режиме. Регулярные просыпания, например каждые 10 секунд и сохранение в память текущего прогресса решают эту проблему. Sleep на 20 минут наоборот — не даёт реализовать такой функционал.
ser-mk
А что мешает спать по 10 секунд и между ними сохранять состояние?
TimsTims
Я про это и написал — просыпаться каждые 10 секунд. А в статье предлагается сделать sleep на 20 минут:
Вы непонятно почему спорите, читая невнимательно статью.
QtRoS
Присоединяюсь, обычно в системе есть событийный таймер, лучше его использовать!
tangro Автор
Получается, хороший пример придумать вообще трудно :)