Многопоточность и перечитывание актуальных данных
От: FrozenHeart  
Дата: 12.11.15 08:48
Оценка:
Скажите, пожалуйста, а что заставляет реализацию перечитать содержимое переменной some_global_variable вместо того, чтобы взять каким-то образом закешированное (например, то, которое было положено в регистр в результате выполнения выражения (1)) в следующем коде?

int some_global_variable; // Нет volatile

// ...

std::cout << some_global_variable << std::endl; // (1) -- Вот тут переменная попадает в регистр
mutex.lock(); // Да, лучше RAII, но речь сейчас не об этом
std::cout << some_global_variable << std::endl; // (2) -- Не обязан перечитать из памяти, даже несмотря на выполнение инструкции MFENCE?
mutex.unlock();


Или всё же ей ничто не мешает использовать то значение, которое было положено в регистр вместо перечитывания "актуальных" данных, и я должен указывать volatile в подобных случаях?

На секунду отбросим рассуждения о том, что обращение к разделяемому ресурсу вне области функционирования примитива синхронизации (1) может привести к UB. Мне интересен сам подход с точки зрения многопоточной / мультипроцессорной архитектуры.
C++ многопоточность
Re: Многопоточность и перечитывание актуальных данных
От: Zhendos  
Дата: 12.11.15 11:27
Оценка: 3 (1)
Здравствуйте, FrozenHeart, Вы писали:

FH>
FH>int some_global_variable; // Нет volatile

FH>// ...

FH>std::cout << some_global_variable << std::endl; // (1) -- Вот тут переменная попадает в регистр
FH>mutex.lock(); // Да, лучше RAII, но речь сейчас не об этом
FH>std::cout << some_global_variable << std::endl; // (2) -- Не обязан перечитать из памяти, даже несмотря на выполнение инструкции MFENCE?
FH>mutex.unlock();
FH>


FH>Скажите, пожалуйста, а что заставляет реализацию перечитать содержимое переменной some_global_variable вместо того, чтобы взять каким-то образом закешированное (например, то, которое было положено в регистр в результате выполнения выражения (1)) в следующем коде?



FH>Или всё же ей ничто не мешает использовать то значение, которое было положено в регистр вместо перечитывания "актуальных" данных, и я должен указывать volatile в подобных случаях?


FH>На секунду отбросим рассуждения о том, что обращение к разделяемому ресурсу вне области функционирования примитива синхронизации (1) может привести к UB. Мне интересен сам подход с точки зрения многопоточной / мультипроцессорной архитектуры.


Из C++11 стандарта:

The library defines a number of atomic operations (Clause 29) and operations on mutexes (Clause 30)
that are specially identified as synchronization operations. These operations play a special role in making
assignments in one thread visible to another.


и далее

Note: For example, a call that acquires a mutex will perform an acquire operation
on the locations comprising the mutex. Correspondingly, a call that releases the same mutex will perform a
release operation on those same locations. Informally, performing a release operation on A forces prior side
effects on other memory locations to become visible to other threads that later perform a consume or an
acquire operation on A.


т.е. компилятор, который реализует стандарт c++11, должен так сгенерировать
код чтобы другие потоки уведели изменения в

prior side
effects on other memory locations to become visible

после std::mutex::unlock
Re: Многопоточность и перечитывание актуальных данных
От: Abyx Россия  
Дата: 12.11.15 12:03
Оценка:
Здравствуйте, FrozenHeart, Вы писали:

FH>Скажите, пожалуйста, а что заставляет реализацию перечитать содержимое переменной some_global_variable вместо того, чтобы взять каким-то образом закешированное (например, то, которое было положено в регистр в результате выполнения выражения (1)) в следующем коде?


Лучше взять такой пример кода:
int x;
std::mutex m;
std::atomic<bool> run_thread;

void thread_fn() {
  while (!run_thread) /* busy-wait */;

  m.lock();
  x = 2;
  m.unlock();
}

int main() {
  std::thread t(thread_fn);
  x = 1;
  run_thread = true; // это синхронизирующая операция (release), именно одна заставляет "x" перечитываться

  m.lock();
  std::cout << x;
  m.unlock();

  t.join();
}

код валиден, в нем нет гонок, а значит нет UB.


Если мы уберем синхронизирующую операцию "run_thread = true" после присваивания "x", то будет гонка:
int main() {
  std::thread t(thread_fn);
  run_thread = true;
  x = 1; // гонка c операцией "x = 2" в thread_fn


Если мы переместим создание потока ниже "x = 1", то гонки опять не будет
  run_thread = true;
  x = 1;
  std::thread t(thread_fn); // теперь само создание потока - синхронизирующая операция


volatile к синхронизации отношения не имеет, и volatile не обязательно заставляет переменную перечитываться. Оно только заставляет компилятор делать так, чтобы наблюдаемое поведение программы было таким, что переменная перечитывается.
volatile int x;
x = 1;
// в "y" может сразу записаться 1, без перечитывания "x"
// потому что между этими строчками ничего нет, а значит нету наблюдателя, который может заметить что перечитывания не произошло
int y = x;
In Zen We Trust
Отредактировано 12.11.2015 12:04 Abyx . Предыдущая версия .
Re[2]: Многопоточность и перечитывание актуальных данных
От: uzhas Ниоткуда  
Дата: 12.11.15 12:13
Оценка:
Здравствуйте, Abyx, Вы писали:

A>Лучше взять такой пример кода:

лучше бы не брал, чесслово

A> run_thread = true; // это синхронизирующая операция (release), именно одна заставляет "x" перечитываться

к чему ты это написал, неясно. был бы смысл это писать, если бы в другом потоке эту переменную читали

A>код валиден, в нем нет гонок, а значит нет UB.

да, но что же он напечатает? либо 1, либо 2. зачем такой код нужен?

A>volatile к синхронизации отношения не имеет, и volatile не обязательно заставляет переменную перечитываться. Оно только заставляет компилятор делать так, чтобы наблюдаемое поведение программы было таким, что переменная перечитывается.

все ровно наоборот

A>volatile int x;

A>x = 1;
A>// в "y" может сразу записаться 1, без перечитывания "x"
A>// потому что между этими строчками ничего нет, а значит нету наблюдателя, который может заметить что перечитывания не произошло
а вот и не может, ибо в x уже может быть записано другим потоком что-то еще
Re: Многопоточность и перечитывание актуальных данных
От: smeeld  
Дата: 12.11.15 12:29
Оценка: 2 (1) -1
Здравствуйте, FrozenHeart, Вы писали:

[code]
FH>std::cout << some_global_variable << std::endl; // (1) -- Вот тут переменная попадает в регистр
FH>mutex.lock(); // Да, лучше RAII, но речь сейчас не об этом
FH>std::cout << some_global_variable << std::endl; // (2) -- Не обязан перечитать из памяти, даже несмотря на выполнение инструкции MFENCE?
FH>mutex.unlock();
FH>[/ccode]

Когда переменная попадает в регистр при вызове функции std::cout, то после, этот регистр перезапишется по сто раз,
как во время исполнения самой std::cout, и тем более при исполнении mutex.lock или любой другой функции
на её месте. Так что когда снова обратитесь к той переменной, то компилятор обязательно сгенерирует
считываение переменной из памяти, и это независимо от синхроизации между потоками. Это на ЯП C++
там всего три строчки кода, на asm-e это будет куча кода, в котором регистр, в который записалась
some_global_variable, будет перезаписан сотню раз, прежде чем произойдёт опять обращение к это переменной.
Сотню раз также может быть перезаписана строка в кеше CPU, что приведёт к задействованию протоколов когерентности.

ЗЫ в стандарте ответов на этот вопрос не искать, его там нет

FH>На секунду отбросим рассуждения о том, что обращение к разделяемому ресурсу вне области функционирования примитива синхронизации (1) может привести к UB. Мне интересен сам подход с точки зрения многопоточной / мультипроцессорной архитектуры.


Протоколы когерентости кеша работают на уровне аппаратуры, компилятору здесь делать нечего.
Re: Многопоточность и перечитывание актуальных данных
От: uzhas Ниоткуда  
Дата: 12.11.15 13:06
Оценка:
Здравствуйте, FrozenHeart, Вы писали:

FH>Скажите, пожалуйста, а что заставляет реализацию перечитать содержимое переменной some_global_variable вместо того, чтобы взять каким-то образом закешированное (например, то, которое было положено в регистр в результате выполнения выражения (1)) в следующем коде?


FH>
FH>int some_global_variable; // Нет volatile

FH>// ...

FH>std::cout << some_global_variable << std::endl; // (1) -- Вот тут переменная попадает в регистр
FH>mutex.lock(); // Да, лучше RAII, но речь сейчас не об этом
FH>std::cout << some_global_variable << std::endl; // (2) -- Не обязан перечитать из памяти, даже несмотря на выполнение инструкции MFENCE?
FH>mutex.unlock();
FH>


FH>Или всё же ей ничто не мешает использовать то значение, которое было положено в регистр вместо перечитывания "актуальных" данных, и я должен указывать volatile в подобных случаях?

для того, чтобы измененное значение было увидено в текущем потоке используются сложные правила, они описаны в стандарте
конкретно в вашем примере значение переменной не обязано перечитаться, т.к. не видно никаких синхронизационных процедур, связанных с этой переменной
mutex.lock имеет семантику acquire и не помогает увидеть значение переменной, записанное в другом потоке за исключением случая, когда запись в переменную в другом потоке делалась с помощью этого же мьютекса
//thread 2
mutex.lock();
some_global_variable = 777;
mutex.unlock();


в этом случае unlock будет иметь семантику release + благодаря тому, что lock образуют некий глобальный порядок (забыл, как в стандарте это называется), то в cout будет выведено 777 (опять же при условии, что lock в thread2 сработал _перед_ lock в главном потоке)
Re[3]: Многопоточность и перечитывание актуальных данных
От: Abyx Россия  
Дата: 12.11.15 13:18
Оценка: +1
Здравствуйте, uzhas, Вы писали:

A>> run_thread = true; // это синхронизирующая операция (release), именно одна заставляет "x" перечитываться

U>к чему ты это написал, неясно. был бы смысл это писать, если бы в другом потоке эту переменную читали

A>>код валиден, в нем нет гонок, а значит нет UB.

U>да, но что же он напечатает? либо 1, либо 2. зачем такой код нужен?

вопрос в том когда появляется UB, а не в практической полезности кода

A>>volatile к синхронизации отношения не имеет, и volatile не обязательно заставляет переменную перечитываться. Оно только заставляет компилятор делать так, чтобы наблюдаемое поведение программы было таким, что переменная перечитывается.

U>все ровно наоборот

поясни, как должно быть

A>>volatile int x;

A>>x = 1;
A>>// в "y" может сразу записаться 1, без перечитывания "x"
A>>// потому что между этими строчками ничего нет, а значит нету наблюдателя, который может заметить что перечитывания не произошло
U>а вот и не может, ибо в x уже может быть записано другим потоком что-то еще

другой поток ничего не может записать, т.к. если он это сделает, то будет гонка, а гонок в коде не должно быть
In Zen We Trust
Re[2]: Многопоточность и перечитывание актуальных данных
От: Abyx Россия  
Дата: 12.11.15 13:23
Оценка:
Здравствуйте, smeeld, Вы писали:

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


S>[code]

FH>>std::cout << some_global_variable << std::endl; // (1) -- Вот тут переменная попадает в регистр
FH>>mutex.lock(); // Да, лучше RAII, но речь сейчас не об этом
FH>>std::cout << some_global_variable << std::endl; // (2) -- Не обязан перечитать из памяти, даже несмотря на выполнение инструкции MFENCE?
FH>>mutex.unlock();
FH>>[/ccode]

S>Когда переменная попадает в регистр при вызове функции std::cout, то после, этот регистр перезапишется по сто раз,


регистр может восстановлен при выходе из этой функции (например esi, edi, ebx в win32 при stdcall,cdecl)
(хотя std::cout это переменная, а не функция, но это не важно)

S>ЗЫ в стандарте ответов на этот вопрос не искать, его там нет

всё что надо — там есть

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

компилятор вставляет всякие инструкции-барьеры
In Zen We Trust
Re: Многопоточность и перечитывание актуальных данных
От: Cruser Украина  
Дата: 12.11.15 13:48
Оценка:
Здравствуйте, FrozenHeart, Вы писали:

FH>Скажите, пожалуйста, а что заставляет реализацию перечитать содержимое переменной some_global_variable вместо того, чтобы взять каким-то образом закешированное (например, то, которое было положено в регистр в результате выполнения выражения (1)) в следующем коде?


Например, есть указатель на разделяемую память и два ядра, тогда тот, кто в память пишет, должен вызвать команду sync, чтобы новые данные реально записались в память. Сам указатель должен быть volatile, чтобы при чтении всё время чталось актуальное значение.
Re[3]: Многопоточность и перечитывание актуальных данных
От: smeeld  
Дата: 12.11.15 13:53
Оценка:
Здравствуйте, Abyx, Вы писали:

A>регистр может восстановлен при выходе из этой функции (например esi, edi, ebx в win32 при stdcall,cdecl)

A>(хотя std::cout это переменная, а не функция, но это не важно)

Только если ему это прикажежшь, укажешь ему clobber регистры, но это тема не про C++.

A>компилятор вставляет всякие инструкции-барьеры


Только если ему это явно прикажешь, что тоже не про C++.
Re[4]: Многопоточность и перечитывание актуальных данных
От: uzhas Ниоткуда  
Дата: 12.11.15 14:11
Оценка:
Здравствуйте, Abyx, Вы писали:

A>вопрос в том когда появляется UB, а не в практической полезности кода

ок. это что-то очень тонкое. видимо, ты хотел показать, что даже без UB нужно внимательно писать код

A>поясни, как должно быть

должно быть перечитывание переменной, независимо от того, пишет ли туда какой-то device или соседний поток. это унаследованное поведение и из-за обратной совместимости это по факту нельзя уничтожить

A>другой поток ничего не может записать, т.к. если он это сделает, то будет гонка, а гонок в коде не должно быть

тут я с тобой согласен, формально так нельзя делать. в частности, использовать volatile для многопоточки, когда есть уже достаточно существенные подвижки в стандарте и в компиляторах
ссылки по теме:
http://stackoverflow.com/questions/2484980/why-is-volatile-not-considered-useful-in-multithreaded-c-or-c-programming
http://stackoverflow.com/questions/72552/why-does-volatile-exist
Re: Многопоточность и перечитывание актуальных данных
От: andyp  
Дата: 12.11.15 18:51
Оценка: 3 (1)
Здравствуйте, FrozenHeart, Вы писали:

FH>Или всё же ей ничто не мешает использовать то значение, которое было положено в регистр вместо перечитывания "актуальных" данных, и я должен указывать volatile в подобных случаях?


FH>На секунду отбросим рассуждения о том, что обращение к разделяемому ресурсу вне области функционирования примитива синхронизации (1) может привести к UB. Мне интересен сам подход с точки зрения многопоточной / мультипроцессорной архитектуры.


IMHO, будет ли переменная второй раз вычитываться из памяти, зависит от того, сможет ли компилятор определить, что aliasing однозначно отстуствует между обращениями. Т.к. разговор идет про глобальную неконстантную переменную, то это означает, что определить он этого не сможет. Любой вызов функции, определение, которой недоступно компилятору, может изменить эту глобальную переменную, поэтому он будет считать, что aliasing возможен. Мьютексы тут совсем ни при чем — барьеры внутри них гарантируют выполнение к-л операций до или после барьера (acquire — release семантика)
Re[5]: Многопоточность и перечитывание актуальных данных
От: Abyx Россия  
Дата: 12.11.15 22:16
Оценка:
Здравствуйте, uzhas, Вы писали:

U>ссылки по теме:

U>http://stackoverflow.com/questions/2484980/why-is-volatile-not-considered-useful-in-multithreaded-c-or-c-programming
U>http://stackoverflow.com/questions/72552/why-does-volatile-exist
угу, и эта тоже — http://ru.stackoverflow.com/questions/165619/volatile-%D0%B2-%D0%BC%D0%BD%D0%BE%D0%B3%D0%BE%D0%BF%D0%BE%D1%82%D0%BE%D1%87%D0%BD%D0%BE%D0%B9-%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B5/420427#420427

в стандарте сказано (http://eel.is/c++draft/intro.execution#1)
> conforming implementations are required to emulate (only) the observable behavior of the abstract machine as explained below.

и ниже (http://eel.is/c++draft/intro.execution#8)
> The least requirements on a conforming implementation are:
> — Access to volatile objects are evaluated strictly according to the rules of the abstract machine.
> ...
> These collectively are referred to as the observable behavior of the program.

и всё бы хорошо, но есть сноска: (http://eel.is/c++draft/intro.execution#footnote-5)
> This provision is sometimes called the “as-if” rule, because an implementation is free to disregard any requirement of this International Standard as long as the result is as if the requirement had been obeyed, as far as can be determined from the observable behavior of the program. For instance, an actual implementation need not evaluate part of an expression if it can deduce that its value is not used and that no side effects affecting the observable behavior of the program are produced.

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

тут можно сказать, что таким наблюдателем может служить точка останова на чтение памяти (hw breakpoint, guard page), и по этому VC++ не убирает лишние чтения у volatile, но ничто не мешает сделать компилятор, которому будет наплевать на существование guard page, т.к. в стандарте про них ничего не сказано.
In Zen We Trust
Re[6]: Многопоточность и перечитывание актуальных данных
От: uzhas Ниоткуда  
Дата: 13.11.15 08:09
Оценка:
Здравствуйте, Abyx, Вы писали:

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


в твоем примере "наблюдателем" является переменная y и связанная с ней логика.
если переменная y не используется, то код может быть выброшен компилятором и чтение из x в том числе
если переменная y используется (к примеру, cout << y; ), то переменная x перечитывается
Re: Многопоточность и перечитывание актуальных данных
От: lpd Европа  
Дата: 13.11.15 09:18
Оценка:
Ты путаешь volatile и синхронизацию. Если объявить переменную volatile, то она будет считываться при любом обращении, даже без синхронизации. Без volatile компилятор может соптимизировать и убрать пересчитываеие. Предназначалась volatile для отображенных в память устройств, когда оставшееся в регистре значение может устареть, и некоторых других случаев.
В данном примере, как ответил smeeld, регистр с переменной(например, eax) просто портится вызовом mutex.lock(), т.к. вызов функции часто все регистры не сохраняет, особенно для такой критической к скорости функции, как lock().
У сложных вещей обычно есть и хорошие, и плохие аспекты.
Отредактировано 13.11.2015 9:19 lpd . Предыдущая версия .
Re: Многопоточность и перечитывание актуальных данных
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 13.11.15 12:48
Оценка:
Здравствуйте, FrozenHeart, Вы писали:

FH>Скажите, пожалуйста, а что заставляет реализацию перечитать содержимое переменной some_global_variable вместо того, чтобы взять каким-то образом закешированное (например, то, которое было положено в регистр в результате выполнения выражения (1)) в следующем коде?


FH>int some_global_variable; // Нет volatile


FH>// ...


FH>std::cout << some_global_variable << std::endl; // (1) -- Вот тут переменная попадает в регистр

FH>mutex.lock(); // Да, лучше RAII, но речь сейчас не об этом
FH>std::cout << some_global_variable << std::endl; // (2) -- Не обязан перечитать из памяти, даже несмотря на выполнение инструкции MFENCE?
FH>mutex.unlock();
FH>[/ccode]

FH>Или всё же ей ничто не мешает использовать то значение, которое было положено в регистр вместо перечитывания "актуальных" данных, и я должен указывать volatile в подобных случаях?


Что есть "реализация"?
Если код, сгенерированный компилятором, то обязательно сработает одно из следующих (зависит от реализации, что именно, но что-то сделано):
1. mutex.lock является функцией, выполняемое в которой не известно компилятору. Он не может узнать, какие побочные эффекты, включая изменение чего угодно в памяти, делается этой функцией, и поэтому вынужден перечитать переменную.
2. Реализация mutex.lock и mutex.unlock содержит явное указание на изменение памяти (для gcc, например, это asm()-оператор, пусть даже пустой, с указанием изменённого побочного ресурса "memory").
3. Возможно, что-то ещё такого же стиля (чтобы вызов функции lock был границей, на которой компилятор предполагает неизвестное вмешательство).

Могло бы также помочь, что на хранение переменной не хватило бы регистров, но это не гарантированный метод.

Если "реализация" — исполнение процессором, то изменение адреса в памяти от себя самого (переключенная задача) он знает и так, а от других процессоров — получит оповещение в рамках алгоритма синхронизации кэшей (все эти MESI, MOESI и т.п.)

FH>На секунду отбросим рассуждения о том, что обращение к разделяемому ресурсу вне области функционирования примитива синхронизации (1) может привести к UB. Мне интересен сам подход с точки зрения многопоточной / мультипроцессорной архитектуры.


Подход работает, иначе бы сама многонитевость выглядела бы в разы ужаснее.

FH>Не обязан перечитать из памяти, даже несмотря на выполнение инструкции MFENCE


MFENCE и т.п. это уже детали работы самого lock. Насколько я вижу в коде под x86, для load с acquire и для операций типа atomic_cmpset_int никакие дополнительные барьеры не используются, достаточно lock {mov, cmpxchg, xadd}.
Intel выпустил "Memory Ordering White Paper" (документ номер 318147), в котором явно сказано "Loads are not reordered with other loads".
The God is real, unless declared integer.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.