Несколько дней новогоднего волшебства:
Успейте начать обучение в 2018-ом году со скидкой до 30%!
Выбрать курс

Как программист ошибку искал

C++Deep3.04Site.png

Один день из жизни команды разработчиков

«Слипы в коде – это очень и очень плохо!» – разорялся программист Вася на совещании. «Согласен, но давай пока так оставим?» – робко парировал его коллега Антон. «Лааадно, ты прав. Так действительно проще.» – нехотя согласился Вася. Возможно, вы считаете использование в коде конструкций типа
sleep_for
или
sleep_until
свидетельством отсутствия некоторых компетенций программиста. Это выбор каждого – использовать или не использовать какие-либо конструкции в языке (исключение – goto, его использовать вообще нельзя).

Наглядный пример

Однажды для решения задачи программист Антон использовал sleep_for. Код ожидания какого-то очень нужного события выглядел примерно вот так:
while(!something_happened)
std::this_thread::sleep_for(std::chrono::milliseconds(100));
Код прошёл ревью, тестирование и стал частью релиза. Этот код являлся частью довольно важного для продукта модуля. Спустя некоторое время поступила жалоба от заказчика, что этот модуль периодически стал зависать. Помогал только полный перезапуск всего продукта, что заказчика, конечно же, не особо радовало. Проблема возникала спонтанно, видимых связей с реальностью не было никаких (в общем-то как всегда). Спустя довольно долгое тестирование и изучение хитростей работы с отладчиком никакого прогресса в решении проблемы у Антона (по «счастливой» случайности задачу на этот баг дали ему) не появилось. И вдруг удача – баг удалось воспроизвести! Отладчик показал, что основной поток проблемного модуля завис на строчке 2 из приведённого выше кода. Причём, завис намертво, а вовсе не на 100 миллисекунд, как планировалось.

Что же произошло?

Первым делом, конечно же, Антон пошёл читать документацию стандартной библиотеки, а конкретнее, вызов std::this_thread::sleep_for: вдруг чего забыл сделать или флаг какой выставить? Оказалось, всё верно: просто передаёшь интервал времени и радуешься, никаких дополнительных настроек или флагов. Следующим шагом стало изучение реализации функции sleep_for в Visual Studio 2015 (именно этой средой и пользовались для сборки проекта). И именно этот шаг оказался решающим в поиске причин возникшего бага. Оказалось, что sleep_for реализован через sleep_until, и в качестве часов использовались не steady_clock, а обычные system_clock. Набрав тестовый пример с проверкой члена is_steady, наш герой убедился, что часы, которые используются для функции sleep_for являются чувствительными к подведению. Простой тест показал, что, если в момент выполнения строчки кода под номером 2 успеть перевести часы, к примеру, на полчаса назад, то и ждать поток будет не запланированные 100 миллисекунды, а целых полчаса. Если подвести часы не назад, а вперёд, то ситуация станет ещё плачевнее – поток никогда так и не проснётся. Если вам это кажется неправильным, то так оно и есть – такое поведение вызывает вопросы не только у вас. Судя по этому багу, проблема до сих пор не исправлена: ПРУФ
P.S. Приведённая выше история является вымышленной, все совпадения событий и действующих лиц случайны.

Автор
5 комментариев
0
  1. На форуме Microsoft пишут, что "We have fixed the problem in an upcoming release."
  2. Каким образом все же удалось воспроизвести этот баг? Неужели при воспроизведении хаотично переставляли системные часы?
0

Продолжая вымышленную историю, могу предположить самый простой вариант - поставить sleep_for не на 100 миллисекунд, а секунд на 10-15. И за это время подвести часы. Вполне быстро воспроизведётся.

А по поводу исправления - обещали в VisualStudio 2017 15.2. Может, и вправду уже поправили. Под рукой студии нет проверить.

0

Здравствуйте Сергей! Спасибо за интересную статью! У меня возникли некоторые вопросы.

Приведены инструкции:

while(!something_happened)
std::this_thread::sleep_for(std::chrono::milliseconds(100));

Получается, если (флаг == false), то данный поток заснет. 1. Правильно понимаю, что он заснет на 100 мс и это равнозначно ожиданию 100 мс в этой строчке? 2. Если это так, то он никогда отсюда не выйдет! Выйти может только если другой поток изменит состояние флага где-нибудь в другом месте. Т.е. флаг должен быть разделяемым/в совместном доступе у нескольких потоков.

0

Владимир, всё верно - предполагается, что something_happened - это разделяемый флаг, который когда-нибудь будет выставлен каким-то другим потоком. И вот мы ждём этого события, опрашивая флаг каждые 100 мс. Не лучшее решение синхронизации, которое видывал свет, конечно. Но иногда встречается.

0

Понял, спасибо за ответ!

Для комментирования необходимо авторизоваться