Comments 37
Пользуясь случаем хочу порекомендовать всем с/c++, использующим pthreads и c++11 погуглить на тему «Spurious wakeup», чтобы четко понимать, зачем всегда нужно оборачивать wait в while.
Это известная проблема pthreads, уходящая корнями в проблемы ядра, которая перекочевала в C++11 как важное примечание и особенно интерфейса метода std::condition_variable::wait с двумя параметрами.
Буквально на днях нарвался очень неприятно.
Это известная проблема pthreads, уходящая корнями в проблемы ядра, которая перекочевала в C++11 как важное примечание и особенно интерфейса метода std::condition_variable::wait с двумя параметрами.
Буквально на днях нарвался очень неприятно.
+9
Да, о spurious wakeup, действительно, мало кто подозревает. Одним из use cases для сигнальных переменных является «управляемый sleep», когда мы хотим усыпить какой-нибудь поток на какое-то длительное время, но при этом хотим оставить себе возможность разбудить его «досрочно», например, чтобы завершить. Вот тут spurious wakeup может внести свои коррективы в наши планы.
Поделитесь, как Вы решали проблему «перевода времени назад»?
Поделитесь, как Вы решали проблему «перевода времени назад»?
0
Решал классическим русским способом — авось не случится :-)
А на самом деле, мне кажется это вообще некорректной проблемой в продакшн коде.
Далеко не только pthreads используют условно говоря функцию time() и везде это решать — того не стоит.
Проще обязать админов переводить время через ускорение/замедление часов в ntp и резкие скачки времени объявить UB в лучших традициях C++.
Хотя, для коробочных продуктов для домашних пользователей проблема стоит, конечно, более остро. Но это пока не моя сфера.
А на самом деле, мне кажется это вообще некорректной проблемой в продакшн коде.
Далеко не только pthreads используют условно говоря функцию time() и везде это решать — того не стоит.
Проще обязать админов переводить время через ускорение/замедление часов в ntp и резкие скачки времени объявить UB в лучших традициях C++.
Хотя, для коробочных продуктов для домашних пользователей проблема стоит, конечно, более остро. Но это пока не моя сфера.
0
Внезапные пробуждения — это вовсе не _причина_ чтобы оборачивать wait в while. Причина — это то, что ожидание извещения и ожидание состояния — это идейно разные вещи. Просто ожидать наступления определенного состояния — это делается busy loop-ом на соответствующих переменных. Но поскольку это жжет циклы процессора без особого толку, то для большинства практических случаев, где не нужно ultra-low-latency, хочется как-то эти циклы более полезно использовать. И тут возникает такой инструмент как wait(), который позволяет сказать системному планировщику задач, что вот ближайшие сколько-то времени этому потоку процессор не нужен.
Вот только исходный busy-loop от этого никуда не девается — просто внутрь него добавляется этот самый wait. То есть это не wait оборачивается в цикл — это цикл дополняется wait-ом.
А внезапные пробуждения — это всего лишь наглядный пример, с помощью которого эту идею удобно объяснять новичкам. И, кстати, вы уверены, что вы именно на нее нарвались, а не на какой-то неучтенный notify? Потому что сама проблема вроде бы воспроизводилась очень редко даже когда этот баг в ядре был, а уж сейчас и вообще почти не воспроизводится.
Вот только исходный busy-loop от этого никуда не девается — просто внутрь него добавляется этот самый wait. То есть это не wait оборачивается в цикл — это цикл дополняется wait-ом.
А внезапные пробуждения — это всего лишь наглядный пример, с помощью которого эту идею удобно объяснять новичкам. И, кстати, вы уверены, что вы именно на нее нарвались, а не на какой-то неучтенный notify? Потому что сама проблема вроде бы воспроизводилась очень редко даже когда этот баг в ядре был, а уж сейчас и вообще почти не воспроизводится.
+6
Ну что значит не причина.
Тут как с любым другим UB (а вероятно это объявлено как UB в Стандарте) — если оно не проявляется, это не значит, что этот код не проблемный. Стандарт еще не глядел, но на cppreference требуется, чтобы или использовать внешний цикл вручную или дать лямбду, а Стандартная библиотека сделает все за тебя внутри.
Я неправильно выразился — проблем у меня не было, у меня было непонимание зачем while нужен и желание заменить его на if.
>> Вот только исходный busy-loop от этого никуда не девается — просто внутрь него добавляется этот самый wait. То есть это не wait оборачивается в цикл — это цикл дополняется wait-ом.
Ну вот в Джаве же не так? Я не очень хорошо ее знаю, особенно многопоточность, но там никаких аргументов у wait нет, а коллега Джавист воспринял эти ложные просыпания как очередную красноглазую дикость :-) Вероятно, там этой проблемы нет.
Это к вопросу о том, что в теории для wait-а не требуется цикл, а добавлять его приходится из чисто практических соображений (особенности ядра). Скажем так, в псевдокоде он бы не потребовался.
Тут как с любым другим UB (а вероятно это объявлено как UB в Стандарте) — если оно не проявляется, это не значит, что этот код не проблемный. Стандарт еще не глядел, но на cppreference требуется, чтобы или использовать внешний цикл вручную или дать лямбду, а Стандартная библиотека сделает все за тебя внутри.
Я неправильно выразился — проблем у меня не было, у меня было непонимание зачем while нужен и желание заменить его на if.
>> Вот только исходный busy-loop от этого никуда не девается — просто внутрь него добавляется этот самый wait. То есть это не wait оборачивается в цикл — это цикл дополняется wait-ом.
Ну вот в Джаве же не так? Я не очень хорошо ее знаю, особенно многопоточность, но там никаких аргументов у wait нет, а коллега Джавист воспринял эти ложные просыпания как очередную красноглазую дикость :-) Вероятно, там этой проблемы нет.
Это к вопросу о том, что в теории для wait-а не требуется цикл, а добавлять его приходится из чисто практических соображений (особенности ядра). Скажем так, в псевдокоде он бы не потребовался.
-1
>Ну что значит не причина.
Это значит, что это не причина, а лишь удобный пример, который проще объяснить молодым разработчикам, стремящимся побыстрее писать код, и не любящим вдаваться в концептуальные тонкости. Настоящая причина, повторяю, в том, что цикл проверки состояния — первичен, а wait() лишь способ его оптимизации.
>Ну вот в Джаве же не так?
Как — не так? В джаве точно так же надо оборачивать wait() в busy-loop. Если коллега-джавист воспринимает ложные просыпания как дикость, то он просто не в теме. Ну либо знает про настоящую причину — но это маловероятно, я крайне редко встречаю людей, которые идут дальше «ложных просыпаний».
>Это к вопросу о том, что в теории для wait-а не требуется цикл, а добавлять его приходится из чисто практических соображений (особенности ядра). Скажем так, в псевдокоде он бы не потребовался.
Как раз наоборот — в псевдокоде, реализующем лишь логику, как раз wait() не понадобился бы. Достаточно было бы busy loop. Псевдокод обычно не содержит технических оптимизаций.
Это значит, что это не причина, а лишь удобный пример, который проще объяснить молодым разработчикам, стремящимся побыстрее писать код, и не любящим вдаваться в концептуальные тонкости. Настоящая причина, повторяю, в том, что цикл проверки состояния — первичен, а wait() лишь способ его оптимизации.
>Ну вот в Джаве же не так?
Как — не так? В джаве точно так же надо оборачивать wait() в busy-loop. Если коллега-джавист воспринимает ложные просыпания как дикость, то он просто не в теме. Ну либо знает про настоящую причину — но это маловероятно, я крайне редко встречаю людей, которые идут дальше «ложных просыпаний».
>Это к вопросу о том, что в теории для wait-а не требуется цикл, а добавлять его приходится из чисто практических соображений (особенности ядра). Скажем так, в псевдокоде он бы не потребовался.
Как раз наоборот — в псевдокоде, реализующем лишь логику, как раз wait() не понадобился бы. Достаточно было бы busy loop. Псевдокод обычно не содержит технических оптимизаций.
+3
Перечитал ваше сообщение еще раз и не согласен в одном моменте:
>> тут возникает такой инструмент как wait(), который позволяет сказать системному планировщику задач, что вот ближайшие сколько-то времени этому потоку процессор не нужен.
wait бывает же без времени, бесконечный по событию.
Допустим я могу реализовать алгоритм, когда беру мютекс, проверяю, что некое абстрактное событие еще не произошло выставляю флажок — разбудите меня, когда закончите. И поток засыпает в пределе навечно, пока не разбудят. И вот тут использование while для непосвященного кажется совсем избыточным.
>> тут возникает такой инструмент как wait(), который позволяет сказать системному планировщику задач, что вот ближайшие сколько-то времени этому потоку процессор не нужен.
wait бывает же без времени, бесконечный по событию.
Допустим я могу реализовать алгоритм, когда беру мютекс, проверяю, что некое абстрактное событие еще не произошло выставляю флажок — разбудите меня, когда закончите. И поток засыпает в пределе навечно, пока не разбудят. И вот тут использование while для непосвященного кажется совсем избыточным.
0
Еще раз повторяю — это не использование while является избыточным, это использование wait() является вторичным по отношению к busy-loop.
Просто сейчас молодым разработчикам сходу дают шаблон wait/notify, хотя стоило бы начать с busy-loop. Конечно, это оптимальнее с точки зрения способности молодняка сразу писать приемлемый код — а то напишет у вас джуниор межпоточный протокол на busy loop-ах, и будете вы думать, почему у вас загрузка сервера вдруг 800%. Но с точки зрения правильного понимания сути лучше было бы учить сначала циклу ожидания, а потом уже его оптимизации через wait/notify. Потому что иначе получается как раз то, о чем мы сейчас с вами беседуем — человек просто не видит из каких частей состоит конструкция, которую он использует, и как эти части эволюционно развивались к своему текущему состоянию. И поэтому не понимает что первично, а что вторично, а что вообще не обязательно.
Просто сейчас молодым разработчикам сходу дают шаблон wait/notify, хотя стоило бы начать с busy-loop. Конечно, это оптимальнее с точки зрения способности молодняка сразу писать приемлемый код — а то напишет у вас джуниор межпоточный протокол на busy loop-ах, и будете вы думать, почему у вас загрузка сервера вдруг 800%. Но с точки зрения правильного понимания сути лучше было бы учить сначала циклу ожидания, а потом уже его оптимизации через wait/notify. Потому что иначе получается как раз то, о чем мы сейчас с вами беседуем — человек просто не видит из каких частей состоит конструкция, которую он использует, и как эти части эволюционно развивались к своему текущему состоянию. И поэтому не понимает что первично, а что вторично, а что вообще не обязательно.
+3
Использовать CLOCK_MONOTONIC источник, который не зависит от перевода времени
pthread_condattr_t condattr;
pthread_condattr_init(&condattr);
pthread_condattr_setclock(&condattr, CLOCK_MONOTONIC);
pthread_cond_t cond;
pthread_cond_init(&cond, &condattr);
clock_gettime(CLOCK_MONOTONIC, &t);
+18
Точнее CLOCK_MONOTONIC_RAW, который спасает от NTP.
+8
У ntp, кстати, есть два способо подведения времени. Есть ntp-client, который тупо обновляет системное время, и есть более умный ntpd. Если расхождение часов не большое, ntpd управляет скоростью хода часов, чтобы синхронизовать их соблюдая монотонность времени. И, только если разрыв совсем большой, ntpd переводит часы скачком.
+1
Да, век живи — век учись!
Погуглил эту тему и наткнулся на некоторое недопонимание: чем отличается CLOCK_MONOTONIC_RAW от CLOCK_MONOTONIC? И самое главное, почему именно CLOCK_MONOTONIC_RAW «спасает от NTP»? От любого перевода времени должен спасать любой monotonic ход времени.
Также немного смущает информация из другого Хабрапоста (http://habrahabr.ru/post/111234/), где автор указывает, что CLOCK_MONOTONIC_RAW «не смог протестировать ни на одной машине из доступных — везде они возвращали нули». Это non-portable?
Погуглил эту тему и наткнулся на некоторое недопонимание: чем отличается CLOCK_MONOTONIC_RAW от CLOCK_MONOTONIC? И самое главное, почему именно CLOCK_MONOTONIC_RAW «спасает от NTP»? От любого перевода времени должен спасать любой monotonic ход времени.
Также немного смущает информация из другого Хабрапоста (http://habrahabr.ru/post/111234/), где автор указывает, что CLOCK_MONOTONIC_RAW «не смог протестировать ни на одной машине из доступных — везде они возвращали нули». Это non-portable?
0
От перевода времени спасет и CLOCK_MONOTONIC и CLOCK_MONOTONIC_RAW, но как правильно написал @kibrtgus NTP может подводить часы без прыжков, а управляя скоростью хода, в этом случае CLOCK_MONOTONIC будет идти тоже с другой скоростью, а CLOCK_MONOTONIC_RAW никак затронут не будет.
Как пишут в мане — CLOCK_MONOTONIC_RAW (since Linux 2.6.28; Linux-specific), автор видимо тестировал со старыми ядрами.
Как пишут в мане — CLOCK_MONOTONIC_RAW (since Linux 2.6.28; Linux-specific), автор видимо тестировал со старыми ядрами.
0
void * SystemTimeManager::runnable(void *ptr)
{
...
while(!_finish)
Мне кажется тут страшная ошибка таится. Есть ли хоть одна причина по которой процессор не должен закешировать эту переменную и никогда не обновлять ее значение?
+3
UFO just landed and posted this here
Прочитал — задумался. На самом деле деле, действительно, с академической точки зрения небезопасно, но на практике компиллятор (по крайней мере известные мне версии gcc3 и 4) как-то догадывались сами, что оптимизировать это поле не стоит.
Согласен на 100%, что полагаться на догадки компилятора не следует, ибо надо писать true portable код, поэтому volatile нам в руки!
Согласен на 100%, что полагаться на догадки компилятора не следует, ибо надо писать true portable код, поэтому volatile нам в руки!
0
Компилятор не догадывается. Например это решение работает 100% в дебаг режиме. Из за того, что там все без оптимизаций. На практике же я однажды столкнулся именно с этом проблемой. (До этого момента я считал что такой код увидеть в реальном продукте не возможно). Решение поставлялось в дебаг сборке. Я настоял на переводе его в релиз. И тут на тебе — не работает :) Благо проблему нашел я быстро, так как знал о существовании этой проблемы.
+2
Вы просто пишите большие тела у циклов. Кешировать переменную на регистре смысла нет.
-2
Это был не совет. Это пояснение, почему на практике компиляторы как правило не кешируют переменную, проверяемую в определении цикла.
Если надо показать ситуацию, в которой такой подход сломается, надо делать очень маленькое тело цикла и тогда оптимизация делается. Но на практике толо достаточно большое, чтобы код работал и тесты не выявили мину замедленного действия.
Если надо показать ситуацию, в которой такой подход сломается, надо делать очень маленькое тело цикла и тогда оптимизация делается. Но на практике толо достаточно большое, чтобы код работал и тесты не выявили мину замедленного действия.
0
Конечно есть. В цикле вызываются функции pthread_* сторонней библиотеке. Компилятор не знает что они делают, а значит должен предполагать что любая переменная (ну, кроме тех, на которые заведомо никто не может иметь ссылки) может быть еми изменена.
0
Эм, это предположение или это именно механика работы? Можно ссылку где об этом расписано по подробнее?
0
Это механизм работы. Компилятор имеет право оптимизировать ТОЛЬКО если он знает, что оптимизации легитимны. Но предположение о том, что компилятор ничего не знает о pthread_* не обязательно верно. Особенно если статическую линковку сделать.
Кажется я слышал веселую байку про мьютексы на spark'е, где, благодаря аппаратной поддержке, команда освобождения мьютекса разворачивалась в одну ассемблерную команду и инлайнилась в место вызова. А компилятор сделал разрешенную в C+03 оптимизацию — перенес сохранение данных в память с регистров после этой команды (она же не читает и не пишет эти данные, конфликта нет).
Кажется я слышал веселую байку про мьютексы на spark'е, где, благодаря аппаратной поддержке, команда освобождения мьютекса разворачивалась в одну ассемблерную команду и инлайнилась в место вызова. А компилятор сделал разрешенную в C+03 оптимизацию — перенес сохранение данных в память с регистров после этой команды (она же не читает и не пишет эти данные, конфликта нет).
0
Любая атомарная операция является полным барьером (full memory barrier). Помнится в старых версиях ядра Linux использовалась атомарная инструкция прибавления 0 к одному из регистров в качестве memory barrier. Практически во всех pthread_* используются атомарные операции, следовательно после вызова pthread_* все модификации переменной
_finish
должны быть видны текущему потоку.-2
Так все таки, этот пример правильный потому что в pthread_* используются атомарные операции или же pthread_* вызовы сторонней библиотеке. Или и то и другое.
На счет первого я не сомневаюсь. А второе еще заставляет задуматься.
На счет первого я не сомневаюсь. А второе еще заставляет задуматься.
0
Чтобы не быть голословным, приведу ссылки:
Здесь указывается, что все регистры после системного вызова/прерывания(int 0x80) сохраняют свое значение (кроме EAX).
Здесь утверждается, что вызов функции обязан восстановить занчения большинства регистров.
Таким образом, без разницы, встроится (за-inline-ится) pthread_* функция полностью, частично, или не встоится вообще, значения регистров она должна восстановить и компилятор может применять оптимизиции ориентируясь на это. Другими словами вызов внешней функции не гарантирует то, что текущий поток увидит измененное значение переменной
Теперь давайте заглянем в тело pthread_mutex_lock Даже заглядывать не будем, просто посмотрим на POSIX и увидим что некоторые функции pthread обязаны использовать memory barrier.
Здесь указывается, что все регистры после системного вызова/прерывания(int 0x80) сохраняют свое значение (кроме EAX).
Здесь утверждается, что вызов функции обязан восстановить занчения большинства регистров.
Таким образом, без разницы, встроится (за-inline-ится) pthread_* функция полностью, частично, или не встоится вообще, значения регистров она должна восстановить и компилятор может применять оптимизиции ориентируясь на это. Другими словами вызов внешней функции не гарантирует то, что текущий поток увидит измененное значение переменной
_finish
.+1
>Любая атомарная операция является полным барьером (full memory barrier).
В таких обобщениях надо не забывать указывать архитектуру. Потому что да, на x86 'lock add %esp,0' используется как барьер — в яве volatile store как раз через него реализуется, хотя насчет полноты я не уверен, надо проверять. Но, например, на Azul VEGA (кажется) есть атомики без семантики барьера. Это не обязательно совершенно.
В таких обобщениях надо не забывать указывать архитектуру. Потому что да, на x86 'lock add %esp,0' используется как барьер — в яве volatile store как раз через него реализуется, хотя насчет полноты я не уверен, надо проверять. Но, например, на Azul VEGA (кажется) есть атомики без семантики барьера. Это не обязательно совершенно.
+2
> Любая атомарная операция является полным барьером (full memory barrier).
Нет.
en.cppreference.com/w/cpp/atomic/memory_order
Нет.
en.cppreference.com/w/cpp/atomic/memory_order
0
* Любая атомарная операция на x86 (обобщение и впрямь плохое сделал)
** За Azul VEGA спасибо, почитал, узнал много интересного.
** За Azul VEGA спасибо, почитал, узнал много интересного.
0
Sign up to leave a comment.
Pthread_cond_timedwait: проблема, решение, дискуссия