синхронизация потоков
От: tdiff  
Дата: 28.03.14 14:21
Оценка: 1 (1) :))) :))
Возьмём классический пример синхронизации потоков:


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, которую компилятор не может позволить себе отоптимизировать.
Так ли это? Если да, то что это за инструкция на самом деле?
Re: синхронизация потоков
От: fdn721  
Дата: 28.03.14 14:42
Оценка: 1 (1) +2 -1
Здравствуйте, tdiff, Вы писали:

T>Подобный код приводится в качестве примера ошибочного, т.к. может быть оптимизирован компилятором до


Добавь volatile, и всё будет работать корректно.


volatile bool m_flag;
Re[2]: синхронизация потоков
От: Kernan Ниоткуда https://rsdn.ru/forum/flame.politics/
Дата: 28.03.14 14:44
Оценка:
Здравствуйте, fdn721, Вы писали:

F>Добавь volatile, и всё будет работать корректно.

Не совсем чтобы корректно, но будет работать, да.
Sic luceat lux!
Re: синхронизация потоков
От: Lazin Россия http://evgeny-lazin.blogspot.com
Дата: 28.03.14 14:56
Оценка: +1
Здравствуйте, tdiff, Вы писали:

T>Моё предположение такое: где-то внутри mutex используется какая-то специальная инструкция типа memory barrier, которую компилятор не может позволить себе отоптимизировать.

T>Так ли это? Если да, то что это за инструкция на самом деле?

как правило, генерируется полный барьер, например в boost/shared_ptr/detail/spin_lock.hpp для этого используется макрос

#define BOOST_COMPILER_FENCE __asm__ __volatile__( "" : : : "memory" );


должен ли компилятор в данном случае оптимизировать именение переменной несмотря на барьер я не знаю

сам mutex может быть устроен по разному, это может быть простой спин-лок на атомарных операциях (InterlockedExchange), может быть примитив операционной системы, который идет в ядро, а может быть что-то более сложно, на основе очередей
Re[2]: синхронизация потоков
От: tdiff  
Дата: 28.03.14 14:57
Оценка:
Здравствуйте, fdn721, Вы писали:

F>Здравствуйте, tdiff, Вы писали:


T>>Подобный код приводится в качестве примера ошибочного, т.к. может быть оптимизирован компилятором до


F>Добавь volatile, и всё будет работать корректно.



F>
F>volatile bool m_flag;
F>



volatile это стрёмно.
Если не ошибаюсь, в стандарте ++ его вообще нет, так что каждый компилятор ведёт себя, как захочется.
Re[2]: синхронизация потоков
От: Lazin Россия http://evgeny-lazin.blogspot.com
Дата: 28.03.14 15:01
Оценка: 1 (1)
Здравствуйте, Lazin, Вы писали:

L>должен ли компилятор в данном случае оптимизировать именение переменной несмотря на барьер я не знаю


думаю без volatile в данном случае все же нельзя, даже если компилятор учитывает наличие барьера (сомневаюсь в этом) и не будет оптимизировать и выносить проверку переменной из цикла, он все равно может поместить ее в регистр, что, опять же, изменит ее видимость для другого потока
Re: синхронизация потоков
От: dead0k  
Дата: 28.03.14 15:10
Оценка: -1
Здравствуйте, tdiff, Вы писали:

T>
T>void thread_1_sync()
T>{
T>    while (true) {
T>        {
T>            scoped_lock lock(m);
T>            if (flag) break;
T>        }
T>        work();
T>    }
T>}


flag — глобальная переменная, которую библиотечная функция lock(...) — видит и, как следствие, может изменить, что приведет к завершению(или продолжению) цикла.
если компилятор перестроит тело так, как во втором примере, видимое поведение функции thrad_1_sync изменится. следовательно компилятор так делать не может.
замечательный факт — про многопоточность мы еще не говорили
Re[2]: синхронизация потоков
От: Lazin Россия http://evgeny-lazin.blogspot.com
Дата: 28.03.14 15:13
Оценка:
Здравствуйте, dead0k, Вы писали:

D>flag — глобальная переменная, которую библиотечная функция lock(...) — видит и, как следствие, может изменить, что приведет к завершению(или продолжению) цикла.

D>если компилятор перестроит тело так, как во втором примере, видимое поведение функции thrad_1_sync изменится. следовательно компилятор так делать не может.
D>замечательный факт — про многопоточность мы еще не говорили

А если ф-я lock в той же еденице трансляции? Очень актуально для header only реализаций, как в boost/shared_ptr.
Re[3]: синхронизация потоков
От: tdiff  
Дата: 28.03.14 15:15
Оценка:
Здравствуйте, Lazin, Вы писали:

L>Здравствуйте, Lazin, Вы писали:


L>>должен ли компилятор в данном случае оптимизировать именение переменной несмотря на барьер я не знаю


L>думаю без volatile в данном случае все же нельзя, даже если компилятор учитывает наличие барьера (сомневаюсь в этом) и не будет оптимизировать и выносить проверку переменной из цикла, он все равно может поместить ее в регистр, что, опять же, изменит ее видимость для другого потока


ну вот тут пишут, что локов достаточно:
https://code.google.com/p/thread-sanitizer/wiki/PopularDataRaces#Notification :
To fix such race you need to use compiler and/or memory barriers (if you are not an expert -- simply use locks).

Вроде это достаточно серьёзный проект: часть clang и gcc.
Кстати, возможно, из этого следует, что слова насчёт locks применимы именно к этим компиляторам.
Re[3]: синхронизация потоков
От: dead0k  
Дата: 28.03.14 15:18
Оценка:
Здравствуйте, Lazin, Вы писали:

L>А если ф-я lock в той же еденице трансляции? Очень актуально для header only реализаций, как в boost/shared_ptr.

А разве есть header-only библиотеки синхронизации?
тот-же буст пользует pthread/winapi
Re[2]: синхронизация потоков
От: tdiff  
Дата: 28.03.14 15:19
Оценка:
Здравствуйте, dead0k, Вы писали:

D>Здравствуйте, tdiff, Вы писали:


T>>
T>>void thread_1_sync()
T>>{
T>>    while (true) {
T>>        {
T>>            scoped_lock lock(m);
T>>            if (flag) break;
T>>        }
T>>        work();
T>>    }
T>>}


D>flag — глобальная переменная, которую библиотечная функция lock(...) — видит и, как следствие, может изменить, что приведет к завершению(или продолжению) цикла.

D>если компилятор перестроит тело так, как во втором примере, видимое поведение функции thrad_1_sync изменится. следовательно компилятор так делать не может.
D>замечательный факт — про многопоточность мы еще не говорили

А work() тоже видит flag. Следовательно, по-вашему, оптимизация thread_1_real() невозможна или я что-то недопонял?
Re[3]: синхронизация потоков
От: tdiff  
Дата: 28.03.14 15:22
Оценка:
Здравствуйте, tdiff, Вы писали:

T>Здравствуйте, dead0k, Вы писали:


D>>Здравствуйте, tdiff, Вы писали:


T>>>
T>>>void thread_1_sync()
T>>>{
T>>>    while (true) {
T>>>        {
T>>>            scoped_lock lock(m);
T>>>            if (flag) break;
T>>>        }
T>>>        work();
T>>>    }
T>>>}


D>>flag — глобальная переменная, которую библиотечная функция lock(...) — видит и, как следствие, может изменить, что приведет к завершению(или продолжению) цикла.

D>>если компилятор перестроит тело так, как во втором примере, видимое поведение функции thrad_1_sync изменится. следовательно компилятор так делать не может.
D>>замечательный факт — про многопоточность мы еще не говорили

T>А work() тоже видит flag. Следовательно, по-вашему, оптимизация thread_1_real() невозможна или я что-то недопонял?


кажется понял — дело в том, что она библиотечная.
Re[3]: синхронизация потоков
От: dead0k  
Дата: 28.03.14 15:25
Оценка:
Здравствуйте, tdiff, Вы писали:
T>А work() тоже видит flag. Следовательно, по-вашему, оптимизация thread_1_real() невозможна или я что-то недопонял?
если work — находится в другой единице трансляции, то да (тут правда может еще сыграть whole program optimization, в энтом деле я не соображаю)
Re[4]: синхронизация потоков
От: Lazin Россия http://evgeny-lazin.blogspot.com
Дата: 28.03.14 15:28
Оценка:
Здравствуйте, 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-е.
Re[4]: синхронизация потоков
От: tdiff  
Дата: 28.03.14 15:30
Оценка:
Здравствуйте, dead0k, Вы писали:

D>Здравствуйте, tdiff, Вы писали:

T>>А work() тоже видит flag. Следовательно, по-вашему, оптимизация thread_1_real() невозможна или я что-то недопонял?
D>если work — находится в другой единице трансляции, то да (тут правда может еще сыграть whole program optimization, в энтом деле я не соображаю)

а если
bool flag
заменить на
static bool flag

что-то изменится?
Re[5]: синхронизация потоков
От: dead0k  
Дата: 28.03.14 15:47
Оценка:
Здравствуйте, Lazin, Вы писали:
L>В boost/shared_ptr/detail есть простой spin_lock, который полностью header-only и построен на атомарных операциях и барьерах.
Можно ссылку?
Я просто всегда думал, что атомарные операции и барьеры суть std/crt. Т.е. все равно вызов неких библиотечных функций

t>а если

t>bool flag
t>заменить на
t>static bool flag
static = per-thread, не так-ли? почему бы и не соптимизировать?
Re[6]: синхронизация потоков
От: dead0k  
Дата: 28.03.14 16:20
Оценка:
Здравствуйте, dead0k, Вы писали:
t>>а если
t>>bool flag
t>>заменить на
t>>static bool flag
D>static = per-thread, не так-ли? почему бы и не соптимизировать?
вру конечно, static и per-thread совсем разные.
что касается того почему static bool не даст компилятору соптимизировать и поломать программу — можно предположить, что lock может получить доступ к flag-у через указатели, но тогда компилятор может удостоверится, что в единице трансляции никто не берет адрес flag-а, а значит — можно оптимизировать... так-что в этом вопросе я пас.
Re: синхронизация потоков
От: Pzz Россия https://github.com/alexpevzner
Дата: 28.03.14 16:47
Оценка: +1
Здравствуйте, 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.
Re[2]: синхронизация потоков
От: Аноним  
Дата: 28.03.14 19:24
Оценка: +1 -2
Здравствуйте, 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); // внешняя функция, хз что делает, может и флаг поменять - нельзя оптимизировать
    ...
  }
}
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.