Pull to refresh

Comments 37

Пользуясь случаем хочу порекомендовать всем с/c++, использующим pthreads и c++11 погуглить на тему «Spurious wakeup», чтобы четко понимать, зачем всегда нужно оборачивать wait в while.
Это известная проблема pthreads, уходящая корнями в проблемы ядра, которая перекочевала в C++11 как важное примечание и особенно интерфейса метода std::condition_variable::wait с двумя параметрами.
Буквально на днях нарвался очень неприятно.
Да, о spurious wakeup, действительно, мало кто подозревает. Одним из use cases для сигнальных переменных является «управляемый sleep», когда мы хотим усыпить какой-нибудь поток на какое-то длительное время, но при этом хотим оставить себе возможность разбудить его «досрочно», например, чтобы завершить. Вот тут spurious wakeup может внести свои коррективы в наши планы.

Поделитесь, как Вы решали проблему «перевода времени назад»?
Решал классическим русским способом — авось не случится :-)
А на самом деле, мне кажется это вообще некорректной проблемой в продакшн коде.
Далеко не только pthreads используют условно говоря функцию time() и везде это решать — того не стоит.
Проще обязать админов переводить время через ускорение/замедление часов в ntp и резкие скачки времени объявить UB в лучших традициях C++.

Хотя, для коробочных продуктов для домашних пользователей проблема стоит, конечно, более остро. Но это пока не моя сфера.
Внезапные пробуждения — это вовсе не _причина_ чтобы оборачивать wait в while. Причина — это то, что ожидание извещения и ожидание состояния — это идейно разные вещи. Просто ожидать наступления определенного состояния — это делается busy loop-ом на соответствующих переменных. Но поскольку это жжет циклы процессора без особого толку, то для большинства практических случаев, где не нужно ultra-low-latency, хочется как-то эти циклы более полезно использовать. И тут возникает такой инструмент как wait(), который позволяет сказать системному планировщику задач, что вот ближайшие сколько-то времени этому потоку процессор не нужен.

Вот только исходный busy-loop от этого никуда не девается — просто внутрь него добавляется этот самый wait. То есть это не wait оборачивается в цикл — это цикл дополняется wait-ом.

А внезапные пробуждения — это всего лишь наглядный пример, с помощью которого эту идею удобно объяснять новичкам. И, кстати, вы уверены, что вы именно на нее нарвались, а не на какой-то неучтенный notify? Потому что сама проблема вроде бы воспроизводилась очень редко даже когда этот баг в ядре был, а уж сейчас и вообще почти не воспроизводится.
Ну что значит не причина.
Тут как с любым другим UB (а вероятно это объявлено как UB в Стандарте) — если оно не проявляется, это не значит, что этот код не проблемный. Стандарт еще не глядел, но на cppreference требуется, чтобы или использовать внешний цикл вручную или дать лямбду, а Стандартная библиотека сделает все за тебя внутри.

Я неправильно выразился — проблем у меня не было, у меня было непонимание зачем while нужен и желание заменить его на if.

>> Вот только исходный busy-loop от этого никуда не девается — просто внутрь него добавляется этот самый wait. То есть это не wait оборачивается в цикл — это цикл дополняется wait-ом.
Ну вот в Джаве же не так? Я не очень хорошо ее знаю, особенно многопоточность, но там никаких аргументов у wait нет, а коллега Джавист воспринял эти ложные просыпания как очередную красноглазую дикость :-) Вероятно, там этой проблемы нет.

Это к вопросу о том, что в теории для wait-а не требуется цикл, а добавлять его приходится из чисто практических соображений (особенности ядра). Скажем так, в псевдокоде он бы не потребовался.
>Ну что значит не причина.

Это значит, что это не причина, а лишь удобный пример, который проще объяснить молодым разработчикам, стремящимся побыстрее писать код, и не любящим вдаваться в концептуальные тонкости. Настоящая причина, повторяю, в том, что цикл проверки состояния — первичен, а wait() лишь способ его оптимизации.

>Ну вот в Джаве же не так?

Как — не так? В джаве точно так же надо оборачивать wait() в busy-loop. Если коллега-джавист воспринимает ложные просыпания как дикость, то он просто не в теме. Ну либо знает про настоящую причину — но это маловероятно, я крайне редко встречаю людей, которые идут дальше «ложных просыпаний».

>Это к вопросу о том, что в теории для wait-а не требуется цикл, а добавлять его приходится из чисто практических соображений (особенности ядра). Скажем так, в псевдокоде он бы не потребовался.

Как раз наоборот — в псевдокоде, реализующем лишь логику, как раз wait() не понадобился бы. Достаточно было бы busy loop. Псевдокод обычно не содержит технических оптимизаций.
Перечитал ваше сообщение еще раз и не согласен в одном моменте:
>> тут возникает такой инструмент как wait(), который позволяет сказать системному планировщику задач, что вот ближайшие сколько-то времени этому потоку процессор не нужен.
wait бывает же без времени, бесконечный по событию.
Допустим я могу реализовать алгоритм, когда беру мютекс, проверяю, что некое абстрактное событие еще не произошло выставляю флажок — разбудите меня, когда закончите. И поток засыпает в пределе навечно, пока не разбудят. И вот тут использование while для непосвященного кажется совсем избыточным.
Еще раз повторяю — это не использование while является избыточным, это использование wait() является вторичным по отношению к busy-loop.

Просто сейчас молодым разработчикам сходу дают шаблон wait/notify, хотя стоило бы начать с busy-loop. Конечно, это оптимальнее с точки зрения способности молодняка сразу писать приемлемый код — а то напишет у вас джуниор межпоточный протокол на busy loop-ах, и будете вы думать, почему у вас загрузка сервера вдруг 800%. Но с точки зрения правильного понимания сути лучше было бы учить сначала циклу ожидания, а потом уже его оптимизации через wait/notify. Потому что иначе получается как раз то, о чем мы сейчас с вами беседуем — человек просто не видит из каких частей состоит конструкция, которую он использует, и как эти части эволюционно развивались к своему текущему состоянию. И поэтому не понимает что первично, а что вторично, а что вообще не обязательно.
Использовать 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);

Точнее CLOCK_MONOTONIC_RAW, который спасает от NTP.
У ntp, кстати, есть два способо подведения времени. Есть ntp-client, который тупо обновляет системное время, и есть более умный ntpd. Если расхождение часов не большое, ntpd управляет скоростью хода часов, чтобы синхронизовать их соблюдая монотонность времени. И, только если разрыв совсем большой, ntpd переводит часы скачком.
Да, век живи — век учись!

Погуглил эту тему и наткнулся на некоторое недопонимание: чем отличается CLOCK_MONOTONIC_RAW от CLOCK_MONOTONIC? И самое главное, почему именно CLOCK_MONOTONIC_RAW «спасает от NTP»? От любого перевода времени должен спасать любой monotonic ход времени.

Также немного смущает информация из другого Хабрапоста (http://habrahabr.ru/post/111234/), где автор указывает, что CLOCK_MONOTONIC_RAW «не смог протестировать ни на одной машине из доступных — везде они возвращали нули». Это non-portable?
От перевода времени спасет и CLOCK_MONOTONIC и CLOCK_MONOTONIC_RAW, но как правильно написал @kibrtgus NTP может подводить часы без прыжков, а управляя скоростью хода, в этом случае CLOCK_MONOTONIC будет идти тоже с другой скоростью, а CLOCK_MONOTONIC_RAW никак затронут не будет.

Как пишут в мане — CLOCK_MONOTONIC_RAW (since Linux 2.6.28; Linux-specific), автор видимо тестировал со старыми ядрами.
void * SystemTimeManager::runnable(void *ptr)
{
...
    while(!_finish)

Мне кажется тут страшная ошибка таится. Есть ли хоть одна причина по которой процессор не должен закешировать эту переменную и никогда не обновлять ее значение?
UFO just landed and posted this here
Это считается еще более худшим решением. Я правда забыл почему, и где я про это читал, но это часто проскакивает в беседах о данной проблеме. А может быть я так и не разобрался в тех аргументах которые выдвигают против этого решения.
UFO just landed and posted this here
Это ничего не гарантирует.
как написано в статье выше атомарность bool обеспечивается не везде.
volatile не имеет никакого отношения к многопоточности.
Она лишь дает побочный эффект который всем полюбился. Так как прост в использовании и почти всегда работает для bool.
Удачи в использовании каких-либо средств определения гонок с таким кодом.
так я же на против, имел в виду что это не правильно решение.
Прочитал — задумался. На самом деле деле, действительно, с академической точки зрения небезопасно, но на практике компиллятор (по крайней мере известные мне версии gcc3 и 4) как-то догадывались сами, что оптимизировать это поле не стоит.
Согласен на 100%, что полагаться на догадки компилятора не следует, ибо надо писать true portable код, поэтому volatile нам в руки!
Компилятор не догадывается. Например это решение работает 100% в дебаг режиме. Из за того, что там все без оптимизаций. На практике же я однажды столкнулся именно с этом проблемой. (До этого момента я считал что такой код увидеть в реальном продукте не возможно). Решение поставлялось в дебаг сборке. Я настоял на переводе его в релиз. И тут на тебе — не работает :) Благо проблему нашел я быстро, так как знал о существовании этой проблемы.
Вы просто пишите большие тела у циклов. Кешировать переменную на регистре смысла нет.
Это был не совет. Это пояснение, почему на практике компиляторы как правило не кешируют переменную, проверяемую в определении цикла.
Если надо показать ситуацию, в которой такой подход сломается, надо делать очень маленькое тело цикла и тогда оптимизация делается. Но на практике толо достаточно большое, чтобы код работал и тесты не выявили мину замедленного действия.
Конечно есть. В цикле вызываются функции pthread_* сторонней библиотеке. Компилятор не знает что они делают, а значит должен предполагать что любая переменная (ну, кроме тех, на которые заведомо никто не может иметь ссылки) может быть еми изменена.
Эм, это предположение или это именно механика работы? Можно ссылку где об этом расписано по подробнее?
Это механизм работы. Компилятор имеет право оптимизировать ТОЛЬКО если он знает, что оптимизации легитимны. Но предположение о том, что компилятор ничего не знает о pthread_* не обязательно верно. Особенно если статическую линковку сделать.
Кажется я слышал веселую байку про мьютексы на spark'е, где, благодаря аппаратной поддержке, команда освобождения мьютекса разворачивалась в одну ассемблерную команду и инлайнилась в место вызова. А компилятор сделал разрешенную в C+03 оптимизацию — перенес сохранение данных в память с регистров после этой команды (она же не читает и не пишет эти данные, конфликта нет).
Любая атомарная операция является полным барьером (full memory barrier). Помнится в старых версиях ядра Linux использовалась атомарная инструкция прибавления 0 к одному из регистров в качестве memory barrier. Практически во всех pthread_* используются атомарные операции, следовательно после вызова pthread_* все модификации переменной _finish должны быть видны текущему потоку.
Так все таки, этот пример правильный потому что в pthread_* используются атомарные операции или же pthread_* вызовы сторонней библиотеке. Или и то и другое.

На счет первого я не сомневаюсь. А второе еще заставляет задуматься.
Чтобы не быть голословным, приведу ссылки:
Здесь указывается, что все регистры после системного вызова/прерывания(int 0x80) сохраняют свое значение (кроме EAX).
Здесь утверждается, что вызов функции обязан восстановить занчения большинства регистров.

Таким образом, без разницы, встроится (за-inline-ится) pthread_* функция полностью, частично, или не встоится вообще, значения регистров она должна восстановить и компилятор может применять оптимизиции ориентируясь на это. Другими словами вызов внешней функции не гарантирует то, что текущий поток увидит измененное значение переменной _finish.

Теперь давайте заглянем в тело pthread_mutex_lock Даже заглядывать не будем, просто посмотрим на POSIX и увидим что некоторые функции pthread обязаны использовать memory barrier.
>Любая атомарная операция является полным барьером (full memory barrier).

В таких обобщениях надо не забывать указывать архитектуру. Потому что да, на x86 'lock add %esp,0' используется как барьер — в яве volatile store как раз через него реализуется, хотя насчет полноты я не уверен, надо проверять. Но, например, на Azul VEGA (кажется) есть атомики без семантики барьера. Это не обязательно совершенно.
* Любая атомарная операция на x86 (обобщение и впрямь плохое сделал)
** За Azul VEGA спасибо, почитал, узнал много интересного.
Sign up to leave a comment.