Возьмём классический пример синхронизации потоков:
bool flag = false;
void thread_1()
{
while (!flag) {
work();
}
}
void thread_2()
{
sleep(1000);
flag = true;
}
В функции work() flag не меняется.
Подобный код приводится в качестве примера ошибочного, т.к. может быть оптимизирован компилятором до
void thread_1_real()
{
bool f = flag;
while (!f) {
work();
}
}
Такая оптимизация возможна, т.к. компилятор видит, что раз внутри work() flag не меняется, поэтому
нет смысла считывать его на каждой итерации.
Стандартное в практике решение этой проблемы — использовать mutex:
mutex m;
bool flag = false;
void thread_1_sync()
{
while (true) {
{
scoped_lock lock(m);
if (flag) break;
}
work();
}
}
void thread_2_sync()
{
sleep(1000);
{
scoped_lock lock(m);
flag = true;
}
}
Вопрос: что помешает компилятору и в этом случае оптимизировать чтение значения flag примерно вот так:
void thread_1_sync_real()
{
bool f;
scoped_lock lock(m) {
f = flag;
}
while (true) {
if (f) break;
work();
}
}
Моё предположение такое: где-то внутри mutex используется какая-то специальная инструкция типа memory barrier, которую компилятор не может позволить себе отоптимизировать.
Так ли это? Если да, то что это за инструкция на самом деле?
Здравствуйте, tdiff, Вы писали:
T>Моё предположение такое: где-то внутри mutex используется какая-то специальная инструкция типа memory barrier, которую компилятор не может позволить себе отоптимизировать. T>Так ли это? Если да, то что это за инструкция на самом деле?
как правило, генерируется полный барьер, например в boost/shared_ptr/detail/spin_lock.hpp для этого используется макрос
должен ли компилятор в данном случае оптимизировать именение переменной несмотря на барьер я не знаю
сам mutex может быть устроен по разному, это может быть простой спин-лок на атомарных операциях (InterlockedExchange), может быть примитив операционной системы, который идет в ядро, а может быть что-то более сложно, на основе очередей
Здравствуйте, fdn721, Вы писали:
F>Здравствуйте, tdiff, Вы писали:
T>>Подобный код приводится в качестве примера ошибочного, т.к. может быть оптимизирован компилятором до
F>Добавь volatile, и всё будет работать корректно.
F>
F>volatile bool m_flag;
F>
volatile это стрёмно.
Если не ошибаюсь, в стандарте ++ его вообще нет, так что каждый компилятор ведёт себя, как захочется.
Здравствуйте, Lazin, Вы писали:
L>должен ли компилятор в данном случае оптимизировать именение переменной несмотря на барьер я не знаю
думаю без volatile в данном случае все же нельзя, даже если компилятор учитывает наличие барьера (сомневаюсь в этом) и не будет оптимизировать и выносить проверку переменной из цикла, он все равно может поместить ее в регистр, что, опять же, изменит ее видимость для другого потока
flag — глобальная переменная, которую библиотечная функция lock(...) — видит и, как следствие, может изменить, что приведет к завершению(или продолжению) цикла.
если компилятор перестроит тело так, как во втором примере, видимое поведение функции thrad_1_sync изменится. следовательно компилятор так делать не может.
замечательный факт — про многопоточность мы еще не говорили
Здравствуйте, dead0k, Вы писали:
D>flag — глобальная переменная, которую библиотечная функция lock(...) — видит и, как следствие, может изменить, что приведет к завершению(или продолжению) цикла. D>если компилятор перестроит тело так, как во втором примере, видимое поведение функции thrad_1_sync изменится. следовательно компилятор так делать не может. D>замечательный факт — про многопоточность мы еще не говорили
А если ф-я lock в той же еденице трансляции? Очень актуально для header only реализаций, как в boost/shared_ptr.
Здравствуйте, Lazin, Вы писали:
L>Здравствуйте, Lazin, Вы писали:
L>>должен ли компилятор в данном случае оптимизировать именение переменной несмотря на барьер я не знаю
L>думаю без volatile в данном случае все же нельзя, даже если компилятор учитывает наличие барьера (сомневаюсь в этом) и не будет оптимизировать и выносить проверку переменной из цикла, он все равно может поместить ее в регистр, что, опять же, изменит ее видимость для другого потока
Вроде это достаточно серьёзный проект: часть clang и gcc.
Кстати, возможно, из этого следует, что слова насчёт locks применимы именно к этим компиляторам.
Здравствуйте, Lazin, Вы писали:
L>А если ф-я lock в той же еденице трансляции? Очень актуально для header only реализаций, как в boost/shared_ptr.
А разве есть header-only библиотеки синхронизации?
тот-же буст пользует pthread/winapi
D>flag — глобальная переменная, которую библиотечная функция lock(...) — видит и, как следствие, может изменить, что приведет к завершению(или продолжению) цикла. D>если компилятор перестроит тело так, как во втором примере, видимое поведение функции thrad_1_sync изменится. следовательно компилятор так делать не может. D>замечательный факт — про многопоточность мы еще не говорили
А work() тоже видит flag. Следовательно, по-вашему, оптимизация thread_1_real() невозможна или я что-то недопонял?
D>>flag — глобальная переменная, которую библиотечная функция lock(...) — видит и, как следствие, может изменить, что приведет к завершению(или продолжению) цикла. D>>если компилятор перестроит тело так, как во втором примере, видимое поведение функции thrad_1_sync изменится. следовательно компилятор так делать не может. D>>замечательный факт — про многопоточность мы еще не говорили
T>А work() тоже видит flag. Следовательно, по-вашему, оптимизация thread_1_real() невозможна или я что-то недопонял?
Здравствуйте, tdiff, Вы писали: T>А work() тоже видит flag. Следовательно, по-вашему, оптимизация thread_1_real() невозможна или я что-то недопонял?
если work — находится в другой единице трансляции, то да (тут правда может еще сыграть whole program optimization, в энтом деле я не соображаю)
Здравствуйте, dead0k, Вы писали:
D>Здравствуйте, Lazin, Вы писали:
L>>А если ф-я lock в той же еденице трансляции? Очень актуально для header only реализаций, как в boost/shared_ptr. D>А разве есть header-only библиотеки синхронизации? D>тот-же буст пользует pthread/winapi
В boost/shared_ptr/detail есть простой spin_lock, который полностью header-only и построен на атомарных операциях и барьерах. Обычный mutex может использовать такой подход: spin lock + backoff на обычный mutex, в случае захвата на продолжительное время. И это тоже может быть описано в header-е.
Здравствуйте, dead0k, Вы писали:
D>Здравствуйте, tdiff, Вы писали: T>>А work() тоже видит flag. Следовательно, по-вашему, оптимизация thread_1_real() невозможна или я что-то недопонял? D>если work — находится в другой единице трансляции, то да (тут правда может еще сыграть whole program optimization, в энтом деле я не соображаю)
Здравствуйте, Lazin, Вы писали: L>В boost/shared_ptr/detail есть простой spin_lock, который полностью header-only и построен на атомарных операциях и барьерах.
Можно ссылку?
Я просто всегда думал, что атомарные операции и барьеры суть std/crt. Т.е. все равно вызов неких библиотечных функций
t>а если t>bool flag t>заменить на t>static bool flag
static = per-thread, не так-ли? почему бы и не соптимизировать?
Здравствуйте, dead0k, Вы писали: t>>а если t>>bool flag t>>заменить на t>>static bool flag D>static = per-thread, не так-ли? почему бы и не соптимизировать?
вру конечно, static и per-thread совсем разные.
что касается того почему static bool не даст компилятору соптимизировать и поломать программу — можно предположить, что lock может получить доступ к flag-у через указатели, но тогда компилятор может удостоверится, что в единице трансляции никто не берет адрес flag-а, а значит — можно оптимизировать... так-что в этом вопросе я пас.
Здравствуйте, tdiff, Вы писали:
T>Возьмём классический пример синхронизации потоков:
T>
T>bool flag = false;
T>void thread_1()
T>{
T> while (!flag) {
T> work();
T> }
T>}
T>void thread_2()
T>{
T> sleep(1000);
T> flag = true;
T>}
T>
T>В функции work() flag не меняется.
T>Подобный код приводится в качестве примера ошибочного, т.к. может быть оптимизирован компилятором до
Такой код является ошибочным потому, что:
1) в процессе ожидания он впустую грузит процессор
2) компилятор, действительно, может и не переопрашивать флаг. Но конкретно в данном примере он вряд ли это сделает, потому что флаг объявлен, как глобальная переменная, а work() — как вызов внешней функции. Компилятор не может знать, имеет ли внешняя функция доступ к флагу, поэтому будет переепрашивать его после каждого вызова. Однако это было бы не так, если бы флаг был оформлен как локальная для данного файла переменная, либо же функция work() находилась бы в том же файле, и компилятор, проанализировав бы ее, пришел к выводу, что она не меняет флаг.
3) компилятор может переставить местами действия, с целью оптимизации, при условии, что общий смысл не изменится. Например, написано a = 1; b = 2; а компилятор может сделать присваивания в обратном порядке. При этом с точки зрения текущего потока разницы нет, поскольку нет обращений к этим переменным, но вот соседний поток может "увидеть" эти присванивания в обратном порядке, а компилятору на это наплевать. Такую перестановку (обращений к переменным), ровно как и запоминание значения в регистре, можно выключить словом volatile
4) аппаратура тоже может переставить местами действия, и вот это уже словом volatile не выключается, для этого существуют memory barrier'ы.
T>Вопрос: что помешает компилятору и в этом случае оптимизировать чтение значения flag примерно вот так:
Я это уже упоминал — вызов функции, которая делает, с точки зрения компилятора, неизвестно что. Например, меняет флаг.
T>Моё предположение такое: где-то внутри mutex используется какая-то специальная инструкция типа memory barrier, которую компилятор не может позволить себе отоптимизировать. T>Так ли это? Если да, то что это за инструкция на самом деле?
Сишный компилятор поёт то, что видит. А видит он только текущий исходник и тексты, включенные в него через #include. Поэтому он не может знать, что где-то там в дебрях неизвестной ему функции используется memory barrier.
Здравствуйте, fdn721, Вы писали:
F>Добавь volatile, и всё будет работать корректно.
Ага, но только под Visual C++ и x86.
Re[2]: синхронизация потоков
От:
Аноним
Дата:
28.03.14 20:30
Оценка:
Здравствуйте, Pzz, Вы писали:
Pzz>Такой код является ошибочным потому, что: Pzz>3) компилятор может переставить местами действия, с целью оптимизации, при условии, что общий смысл не изменится. Например, написано a = 1; b = 2; а компилятор может сделать присваивания в обратном порядке. При этом с точки зрения текущего потока разницы нет, поскольку нет обращений к этим переменным, но вот соседний поток может "увидеть" эти присванивания в обратном порядке, а компилятору на это наплевать. Такую перестановку (обращений к переменным), ровно как и запоминание значения в регистре, можно выключить словом volatile
А в конкретном примере что может быть переставлено местами?
T>>Вопрос: что помешает компилятору и в этом случае оптимизировать чтение значения flag примерно вот так: Pzz>Я это уже упоминал — вызов функции, которая делает, с точки зрения компилятора, неизвестно что. Например, меняет флаг.
Ок, тогда немного изменим условия: пускай не bool flag, а static bool flag. Что-то изменится? Или всегда кто-то может угадать адрес flag в памяти и поменять его?
Если дело именно в использовании неизвестной в данной единице трансляции функции, есть ли смысл использовать именно мьютексы при синхронизации доступа к bool?
Серьёзно, почему бы не писать:
bool flag
void thread_1()
{
while (!flag) {
sin(123); // внешняя функция, хз что делает, может и флаг поменять - нельзя оптимизировать
...
}
}