- PVSM.RU - https://www.pvsm.ru -
Не так давно мимо нас пробегала неплохая статья об ужасном состоянии производительности современного ПО (оригинал на английском [1], перевод на Хабре [2]). Эта статья напомнила мне об одном антипаттерне кода, который встречается весьма часто и в общем кое-как работает, но приводит к небольшим потерям производительности то тут, то там. Ну, знаете, мелочь, пофиксить которую руки никак не дойдут. Беда лишь в том, что десяток таких «мелочей», разбросанных в разных местах кода начинают приводить к проблемам типа «вроде у меня последний Intel Core i7, а прокрутка дёргается».
Я говорю о неверном использовании функции Sleep [3] (регистр может отличаться в зависимости от языка программирования и платформы). Итак, что же такое Sleep? Документация отвечает на этот вопрос предельно просто: это пауза в выполнении текущего потока на указанное количество миллисекунд. Нельзя не отметить эстетическую красоту прототипа данной функции:
void Sleep(DWORD dwMilliseconds);
Всего один параметр (предельно понятный), никаких кодов ошибок или исключений — работает всегда. Таких приятных и понятных функций очень мало!
Что же могло пойти не так? То, что программисты используют эту замечательную функцию не для того, для чего она предназначена.
А предназначена она для программной симуляции какого-то внешнего, определённого чем-то реальным, процесса паузы.
Мы пишем приложение «часы», в котором раз в секунду нужно менять цифру на экране (или положение стрелки). Функция Sleep здесь подходит как нельзя лучше: нам реально нечего делать чётко определённый промежуток времени (ровно одну секунду). Почему бы и не поспать?
Мы пишем контроллер самогонного аппарата хлебопечки. Алгоритм работы задаётся одной из программ и выглядит примерно так:
Здесь тоже всё чётко: мы работаем со временем, оно задано технологическим процессом. Использование Sleep — приемлемо.
А теперь посмотрим на примеры неверного использования Sleep.
Когда мне нужен какой-то пример некорректного кода на С++ — я иду в репозиторий [4] кода текстового редактора Notepad++. Его код ужасен настолько, что любой антипаттерн там точно найдётся, я об этом даже статью [5] когда-то писал. Не подвёл меня ноутпадик++ и в этот раз! Давайте посмотрим, как в нём используется Sleep.
При старте 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 [6], подписавшись на событие HSHELL_WINDOWCREATED — и получить нотификацию о создании окна мгновенно. Да, код становиться чуть сложнее, но буквально на 3-4 строки. И мы выигрываем до 100 мс! А самое главное — мы больше не используем функции безусловного ожидания там, где ожидание не является безусловным.
HANDLE hThread = ::CreateThread(NULL, 0, threadTextTroller, &trollerParams, 0, NULL);
int sleepTime = 1000 / x * y;
::Sleep(sleepTime);
Я не очень разбирался, чего конкретно и как долго ждёт этот код в Notepad++, но общий антипаттерн «запустить поток и подождать» я видел часто. Люди ждут разного: начала работы другого потока, получения из него каких-то данных, окончания его работы. Плохо здесь сразу два момента:
Здесь мы видим фоновый поток, который спит в ожидании каких-то событий.
class CReadChangesServer
{
...
void Run()
{
while (m_nOutstandingRequests || !m_bTerminate)
{
::SleepEx(INFINITE, true);
}
}
...
void RequestTermination()
{
m_bTerminate = true;
...
}
...
bool m_bTerminate;
};
Нужно признать, что здесь используется не Sleep, а SleepEx [7], который более интеллектуален и может прерывать ожидание при некоторых событиях (типа завершения асинхронных операций). Но это нисколько не помогает! Дело в том, что цикл while (!m_bTerminate) имеет полное право работать бесконечно, игнорируя вызванный из другого потока метод RequestTermination(), сбрасывающий переменную m_bTerminate в true. О причинах и следствия этого я писал в предыдущей статье [8]. Для избегания этого следовало бы использовать что-то, гарантированно правильно работающее между потоками: atomic, event или что-то подобное.
Да, формально SleepEx не виноват в проблеме использования обычной булевой переменной для синхронизации потоков, это отдельная ошибка другого класса. Но почему она стала возможной в этом коде? Потому, что сначала программист подумал «тут надо спать», а затем задумался как долго и по какому условию прекратить это делать. А в правильном сценарии у него даже и не должно было бы возникнуть первой мысли. В голове должно была бы возникнуть мысль «тут надо ожидать события» — и вот с этого момента мысль уже бы работала в сторону выбора правильного механизма синхронизации данных между потоками, который исключил бы как булевскую переменную, так и использование SleepEx.
В этом примере мы посмотрим на функцию 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 секунд, это достаточно большой объём, чтобы инициировать бекап немедленно (вдруг мы его откуда-то вырезали и там его не осталось, а редактор через секунду крешнется).
Т.е. простым ожиданием мы здесь заменяем отсутствующий более интеллектуальный алгоритм.
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.
Автор: Владимир
Источник [9]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/297104
Ссылки в тексте:
[1] оригинал на английском: http://tonsky.me/blog/disenchantment/
[2] перевод на Хабре: https://habr.com/post/423889/
[3] Sleep: https://docs.microsoft.com/en-us/windows/desktop/api/synchapi/nf-synchapi-sleep
[4] репозиторий: https://github.com/notepad-plus-plus/notepad-plus-plus
[5] статью: https://habr.com/company/infopulse/blog/185144/
[6] SetWindowsHookEx: https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-setwindowshookexa
[7] SleepEx: https://docs.microsoft.com/en-us/windows/desktop/api/synchapi/nf-synchapi-sleepex
[8] предыдущей статье: https://habr.com/company/infopulse/blog/424729/
[9] Источник: https://habr.com/post/427843/?utm_source=habrahabr&utm_medium=rss&utm_campaign=427843
Нажмите здесь для печати.