multithreading : visibility control
От: uzhas Ниоткуда  
Дата: 30.05.13 07:30
Оценка: :)
интересует вот какой сценарий: в одном потоке я заполняю некоторую структуру данных (пусть будет std::map), далее я запускаю несколько потоков, которые будут использовать заполненный объект в режиме readonly (пусть будут выполнять find) (предполагается использовать без мьютексов)
1) какие есть гарантии по поводу видимости всего объекта в других потоках? я должен быть уверен, что все потоки работают с одним и тем же словарем
2) какие есть средства в языке для управления видимостью ? я так понимаю, что барьеры выполняют эту функцию, только не ясно как оптимально их использовать. каждый раз при чтении использовать барьер может быть затратно

интересует как C++11, так и VS-specific
Re: multithreading : visibility control
От: Stanislav V. Zudin Россия  
Дата: 30.05.13 07:46
Оценка: +1 :)
Здравствуйте, uzhas, Вы писали:

U>интересует вот какой сценарий: в одном потоке я заполняю некоторую структуру данных (пусть будет std::map), далее я запускаю несколько потоков, которые будут использовать заполненный объект в режиме readonly (пусть будут выполнять find) (предполагается использовать без мьютексов)


На здоровье, работать будет.

U>1) какие есть гарантии по поводу видимости всего объекта в других потоках? я должен быть уверен, что все потоки работают с одним и тем же словарем


Что передашь, с тем и будут работать.
Создающий поток ждет остальных? Тогда можно создавать на стеке. Если нет, то размещать мап надо где-то в общедоступном месте.
Указатель на мап или на "общедоступное место" можно передать в функцию потока.

U>2) какие есть средства в языке для управления видимостью ? я так понимаю, что барьеры выполняют эту функцию, только не ясно как оптимально их использовать. каждый раз при чтении использовать барьер может быть затратно


Не, барьеры из другой оперы.
_____________________
С уважением,
Stanislav V. Zudin
Re: multithreading : visibility control
От: saf_e  
Дата: 30.05.13 08:40
Оценка:
Здравствуйте, uzhas, Вы писали:

U>интересует вот какой сценарий: в одном потоке я заполняю некоторую структуру данных (пусть будет std::map), далее я запускаю несколько потоков, которые будут использовать заполненный объект в режиме readonly (пусть будут выполнять find) (предполагается использовать без мьютексов)

U>1) какие есть гарантии по поводу видимости всего объекта в других потоках? я должен быть уверен, что все потоки работают с одним и тем же словарем
U>2) какие есть средства в языке для управления видимостью ? я так понимаю, что барьеры выполняют эту функцию, только не ясно как оптимально их использовать. каждый раз при чтении использовать барьер может быть затратно

U>интересует как C++11, так и VS-specific


Если на момент обращения из других потоков map создан и заполнен, все ОК, больше ничего не надо.

Если используется создание по первому обращению, тут могут быть варианты.
Re[2]: multithreading : visibility control
От: uzhas Ниоткуда  
Дата: 30.05.13 12:03
Оценка:
Здравствуйте, saf_e, Вы писали:

_>все ОК

хочется изучить вопрос поглубже, собственно интересуют гарантии с точки зрения модели памяти, абстрактной машины из стандарта языка C++11, необходимые средства синхронизации
опишу частный пример более подробно:

std::map<int, std::string> globalMap; // global var

//run in thread1
void t1_func()
{
  globalMap[1] = "1";
  globalMap[2] = "12";
  globalMap[3] = "123";
  spawnThread(&t2_func);
}

//run in thread2
void t2_func()
{
  auto it = globalMap.find(2);
  assert(it != globalMap.end());
}


интересует какие правила регулируют то, что все модификации, произведенные в первом потоке будут видны во втором потоке в момент вызова find. вернет ли find globalMap.end() при каких-то условиях и следует ли как-то явно подсказывать компилятору о необходимости сбросить все модификации перед чтением
Re[3]: multithreading : visibility control
От: Stanislav V. Zudin Россия  
Дата: 30.05.13 12:21
Оценка:
Здравствуйте, uzhas, Вы писали:

U>
U>std::map<int, std::string> globalMap; // global var

U>//run in thread1
U>void t1_func()
U>{
U>  globalMap[1] = "1";
U>  globalMap[2] = "12";
U>  globalMap[3] = "123";
U>  spawnThread(&t2_func);
U>}

U>//run in thread2
U>void t2_func()
U>{
U>  auto it = globalMap.find(2);
U>  assert(it != globalMap.end());
U>}
U>


U>интересует какие правила регулируют то, что все модификации, произведенные в первом потоке будут видны во втором потоке в момент вызова find. вернет ли find globalMap.end() при каких-то условиях и следует ли как-то явно подсказывать компилятору о необходимости сбросить все модификации перед чтением


В этом примере все будет работать как и положено.
Точки следования заданы явно и непротиворечиво, порядок вызова функций будет такой, как в исходном коде.
До вызова spawnThread рабочего потока не существует.

Вот если вызвать spawnThread до инициализации мапки:

void t1_func()
{
  spawnThread(&t2_func);
  globalMap[1] = "1";
  globalMap[2] = "12";
  globalMap[3] = "123";
}


то определить момент, когда запустится рабочий поток и полезет в мапку невозможно (разве что создавать поток suspended и дальше включать вручную).
Может повезти и инициализация произойдет до переключения контекста.
Только к языку это не имеет отношения.
_____________________
С уважением,
Stanislav V. Zudin
Re[3]: multithreading : visibility control
От: uzhas Ниоткуда  
Дата: 30.05.13 12:24
Оценка:
Здравствуйте, uzhas, Вы писали:

U>следует ли как-то явно подсказывать компилятору о необходимости сбросить все модификации перед чтением


гипотеза: в данном случае имеем UB из-за отсутствия синхронизации, так как нет никаких мемори барьеров
предположительный фикс (мета код):
//run in thread1
void t1_func()
{
  globalMap[1] = "1";
  globalMap[2] = "12";
  globalMap[3] = "123";
  mfence (release);
  spawnThread(&t2_func);
}

//run in thread2
void t2_func()
{
  mfence (acquire);
  auto it = globalMap.find(2);
  assert(it != globalMap.end());
}

если фикс верный, то мне он не нраыится слишком плохой гранулярностью барьеров. есть ли более точечные методы синхронизации для потока thread2 (предположим, что find будет вызываться много раз)?
ссылка по теме: http://stackoverflow.com/questions/16071682/does-intel-sfence-have-release-semantics
Re[4]: multithreading : visibility control
От: saf_e  
Дата: 30.05.13 12:58
Оценка: :)
Здравствуйте, uzhas, Вы писали:

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


U>>следует ли как-то явно подсказывать компилятору о необходимости сбросить все модификации перед чтением


U>гипотеза: в данном случае имеем UB из-за отсутствия синхронизации, так как нет никаких мемори барьеров

U>предположительный фикс (мета код):
U>
U>//run in thread1
U>void t1_func()
U>{
U>  globalMap[1] = "1";
U>  globalMap[2] = "12";
U>  globalMap[3] = "123";
U>  mfence (release);
U>  spawnThread(&t2_func);
U>}

U>//run in thread2
U>void t2_func()
U>{
U>  mfence (acquire);
U>  auto it = globalMap.find(2);
U>  assert(it != globalMap.end());
U>}
U>

U>если фикс верный, то мне он не нраыится слишком плохой гранулярностью барьеров. есть ли более точечные методы синхронизации для потока thread2 (предположим, что find будет вызываться много раз)?
U>ссылка по теме: http://stackoverflow.com/questions/16071682/does-intel-sfence-have-release-semantics

Intel гарантирует load-store порядок для одинаковых ячеек памяти.

Как только вы что-то записали в память, это становится видно всем процессорам в системе. Поэтому в данном случае любые барьеры излишни.
Re[3]: multithreading : visibility control
От: okman Беларусь https://searchinform.ru/
Дата: 30.05.13 20:04
Оценка: 10 (1)
Здравствуйте, uzhas, Вы писали:

U>опишу частный пример более подробно:


std::map<int, std::string> globalMap; // global var

//run in thread1
void t1_func()
{
  globalMap[1] = "1";
  globalMap[2] = "12";
  globalMap[3] = "123";
  spawnThread(&t2_func);
}

//run in thread2
void t2_func()
{
  auto it = globalMap.find(2);
  assert(it != globalMap.end());
}


U>интересует какие правила регулируют то, что все модификации, произведенные в первом потоке будут видны во втором потоке в момент вызова find. вернет ли find globalMap.end() при каких-то условиях и следует ли как-то явно подсказывать компилятору о необходимости сбросить все модификации перед чтением


Правила все те же — compiler reordering, hardware reordering.

Для данного кода необходима гарантия того, что публикация данных для других потоков происходит
строго после инициализации данных, причем гарантия как на уровне компилятора, так и на уровне CPU.

Первое обеспечивается вставкой компиляторного барьера (_ReadWriteBarrier на Visual C++)
между 'globalMap[3] = "123"' и вызовом spawnThread. Если этого не сделать, вызов spawnThread
может быть перемещен, например, в начало функции, до заполнения globalMap данными.
Конкретно для данного примера это выглядит маловероятным, но его легко упростить, сведя к
тому самому "клиническому случаю".

По поводу второго можно не беспокоиться, так как публикация данных для других потоков —
это так или иначе установка какого-нибудь флага, то есть, запись в память. В результате
получается комбинация store-store, которая эффектам hardware ordering не подвержена.
То есть, первый store (инициализация) будет гарантированно завершен до второго (публикация).
Чисто гипотетически, если spawnThread не использовала бы запись в память или барьер памяти,
это пришлось бы сделать за нее, опять же между 'globalMap[3] = "123"' и вызовом spawnThread,
только здесь барьер нужен не компиляторный, а хардварный (если быть точным, release-барьер).
Но такую ситуацию довольно трудно себе представить.

Еще можно заподозрить компилятор в том, что однажды он проявит излишнюю сообразительность и
реализует globalMap через регистры, из-за чего другие потоки не увидят данных.
Не поручусь за другие компиляторы, но Visual C++ не занимается такими вещами по отношению к
глобальным переменным, да и к локальным тоже, если есть хотя бы один шанс из миллиона, что ее
будут использовать за пределами области видимости.

Поэтому все, что нужно для данного примера — это вставить _ReadWriteBarrier перед
вызовом spawnThread в функции t1_func.

Все написанное справедливо для IA-32 и AMD64, на других процессорных архитектурах с
более "расслабленной" моделью памяти может потребоваться установка хардварного барьера.
Re[4]: multithreading : visibility control
От: uzhas Ниоткуда  
Дата: 31.05.13 07:08
Оценка:
Здравствуйте, okman, Вы писали:

ну вот первый квалифицированный ответ

O>Правила все те же — compiler reordering, hardware reordering.

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

O>Если этого не сделать, вызов spawnThread может быть перемещен, например, в начало функции, до заполнения globalMap данными.

я слабо себе представляю законные причины для данного вида переупорядочивания в нашем примере. склоняюсь к тому, что это будет нарушение стандарта, т.к. компилятор должен быть уверен, что данное переупорядочивание не повлияет на логику работу первого потока (? или всего приложения?) (observable behaviour может быть разным)

O>По поводу второго можно не беспокоиться, так как публикация данных для других потоков -

O>это так или иначе установка какого-нибудь флага, то есть, запись в память. В результате
O>получается комбинация store-store, которая эффектам hardware ordering не подвержена.
тут я не понял о каком флаге идет речь и что такое публикация

O>Еще можно заподозрить компилятор в том, что однажды он проявит излишнюю сообразительность и

O>реализует globalMap через регистры, из-за чего другие потоки не увидят данных.
и какие подсказки надо использовать, чтобы словарь все же расшарился между потоками?

O>Все написанное справедливо для IA-32 и AMD64, на других процессорных архитектурах с

O>более "расслабленной" моделью памяти может потребоваться установка хардварного барьера.

я рассматриваю общий случай. в пред. сообщении я уже предложил фикс, используя именно хардварный барьер. хочу разобраться, нужно ли еще что-то и можно ли соптимизировать доступ к словарю во втором потоке
Re[4]: multithreading : visibility control
От: Павел Дмитриев Россия  
Дата: 31.05.13 07:21
Оценка:
Здравствуйте, okman, Вы писали:

O>Если этого не сделать, вызов spawnThread

O>может быть перемещен, например, в начало функции, до заполнения globalMap данными.
O>Конкретно для данного примера это выглядит маловероятным, но его легко упростить, сведя к
O>тому самому "клиническому случаю".

Если мне склероз не изменяет, раздел стандарта "1.9 Program execution" запрещает такие клинические случаи. Какие бы оптимизации не применялись, куда бы вызов spawnThread не был помещен, результат должен быть таким же, как если бы все операторы выполнялись последовательно.
Re[5]: multithreading : visibility control
От: okman Беларусь https://searchinform.ru/
Дата: 31.05.13 16:26
Оценка:
Здравствуйте, uzhas, Вы писали:

U>так вот сложность как раз увидеть здесь реордеринг и проследить как же он влияет на визибилити. ведь непонятно что с чем может поменяться местами.


Да, в этом и есть главная сложность — увидеть. Ну или хотя бы заподозрить.

U>когда описывают проблему с переупорядочиванием. всегда фигурирует как минимум две переменные. здесь же переменная одна — глобальный словарь


O>>Если этого не сделать, вызов spawnThread может быть перемещен, например, в начало функции,

O>>до заполнения globalMap данными.

U>я слабо себе представляю законные причины для данного вида переупорядочивания в нашем примере. склоняюсь к тому, что это будет нарушение стандарта, т.к. компилятор должен быть уверен, что данное переупорядочивание не повлияет на логику работу первого потока (? или всего приложения?) (observable behaviour может быть разным)


Это зависит от того, что именно находится в теле spawnThread.
Например, там может быть установка булевого флага, в ожидании которого крутится другой поток.
Вот и получается вторая переменная. В определенных условиях компилятор может заинлайнить тело
spawnThread по месту вызова, после чего обе переменные окажутся близко друг другу.
Не разделенные инструкцией call и стандартным прологом функции... В общем, до компиляторного
реордеринга будет рукой подать.

O>>По поводу второго можно не беспокоиться, так как публикация данных для других потоков -

O>>это так или иначе установка какого-нибудь флага, то есть, запись в память. В результате
O>>получается комбинация store-store, которая эффектам hardware ordering не подвержена.

U>тут я не понял о каком флаге идет речь и что такое публикация


Упрощенно:
int  Data;
bool fReady = false;

void thread_producer()
{
    Data  = 123;  // Запись данных.
    Ready = true; // Публикация данных.
}

void thread_consumer()
{
    while (false == fReady); // Ожидание публикации данных.
    assert(123   == Data);   // Чтение данных.
}


Публикация данных одним потоком для других — это практически всегда так или иначе или
запись в память какого-нибудь флага "данные готовы", или установка события, или
порождение/возобновление потока, или что-то другое, но всегда в результате получается,
как минимум, release-барьер памяти. Вы не сможете уведомить другой поток CPU о
наступлении некоего события, не используя запись в память или хотя бы один из системных
механизмов — прерывания, ввод-вывод, блокировка шины, переключение контекста, а они
все действуют, как полный барьер (fence), заставляя процессор форсировать незавершенные
операции, включая те, что находятся в store buffers (временный буфер для операций store).
В итоге, комбинация "запись-публикация" будет на уровне CPU превращаться или в store-store,
или в store-fence.

Ну и симметрично, поток-потребитель должен сначала дождаться публикации, и только после
этого читать данные. А ожидание публикации — это либо операция чтения флага "данные готовы" в
цикле, либо ожидание события ядра, или еще что-то, в результате чего так или иначе получится
либо комбинация load-load, либо fence-load. И опять переупорядочивания на уровне CPU не будет.

Вот почему в данном примере хардварные барьеры не нужны (на IA-32 и AMD64).

Еще отмечу, что приведенный код все равно некорректный, так как не гарантируется
порядок операций на уровне компилятора и видимость данных в обоих потоках.

O>>Еще можно заподозрить компилятор в том, что однажды он проявит излишнюю сообразительность и

O>>реализует globalMap через регистры, из-за чего другие потоки не увидят данных.

U>и какие подсказки надо использовать, чтобы словарь все же расшарился между потоками?


Вообще-то, в своем предыдущем сообщении я был не прав на этот счет.
Компилятор действительно может поступить с любой переменной так, как ему вздумается,
хотя для этого должны быть достаточно подходящие условия.

Простые типы естественных размеров — BYTE, WORD, DWORD (и QWORD в режиме "Long Mode" CPU)
легко помещаются компилятором в регистры без особых колебаний. std::map в регистр не
запихнешь, но у него есть члены естественных размеров, и они как раз таки тоже могут
быть "закэшированы".

Приведу пример:
#include <Windows.h>
#include <process.h>



int  Value   = 0;
int  Base    = 0;
bool fReady  = false;



unsigned int _stdcall Thread_Consumer(void *)
{
    while (false == fReady);
    if (1 != Value) throw;

    return 0;
}



unsigned int _stdcall Thread_Producer(void *)
{
    Value  = Base + 1;
    fReady = true;
    
    return 0;
}



int main()
{
    HANDLE hThreads[2];
    
    hThreads[0] = (HANDLE)_beginthreadex(NULL, 0, Thread_Consumer, NULL, 0, NULL);
    Sleep(1000);
    hThreads[1] = (HANDLE)_beginthreadex(NULL, 0, Thread_Producer, NULL, 0, NULL);

    WaitForMultipleObjects(2, &hThreads[0], TRUE, INFINITE);

    CloseHandle(hThreads[0]);
    CloseHandle(hThreads[1]);

    return 0;
}




Давайте посмотрим ассемблерный код функций потоков.
(Visual C++ 2008/2010/2012, x64, Release, настройки оптимизации и инлайнинга по максимуму).

Thread_Consumer:
?Thread_Consumer@@YAIPEAX@Z PROC            ; Thread_Consumer, COMDAT

; 13   : {

$LN11:
    sub    rsp, 40                    ; 00000028H
    movzx    eax, BYTE PTR ?fReady@@3_NA        ; fReady
    npad    5
$LL3@Thread_Con:

; 14   :     while (false == fReady);

    test    al, al
    je    SHORT $LL3@Thread_Con
    
    ...

Худшие опасения оправдываются — fReady попала в регистр и изменения, сделанные в
другом потоке, не видны. Программа повисает в бесконечном цикле.

Thread_Producer:
?Thread_Producer@@YAIPEAX@Z PROC            ; Thread_Producer, COMDAT

; 24   :     Value  = Base + 1;

    mov    eax, DWORD PTR ?Base@@3HA        ; Base

; 25   :     fReady = true;

    mov    BYTE PTR ?fReady@@3_NA, 1        ; fReady
    inc    eax
    mov    DWORD PTR ?Value@@3HA, eax        ; Value

    ...

И здесь тоже не все гладко — установка fReady в true происходит до записи Value.
Компилятор посчитал, что так будет эффективнее.

В результате на простом с виду коде имеем обе проблемы — видимость и переупорядочивание.
Бороться с этим можно двумя способами — volatile и барьеры компилятора.
Оба способа, во-первых, заставляют компилятор удерживать исходный порядок операций, а
во-вторых, форсят обращение к памяти.

volatile (C++)
http://msdn.microsoft.com/en-us/library/12a04hfd(v=vs.100).aspx

Microsoft Specific

Objects declared as volatile are not used in certain optimizations because their values
can change at any time. The system always reads the current value of a volatile object at
the point it is requested, even if a previous instruction asked for a value from the
same object. Also, the value of the object is written immediately on assignment.

When optimizing, the compiler must maintain ordering among references to volatile objects
as well as references to other global objects. In particular,

A write to a volatile object (volatile write) has Release semantics; a reference to a
global or static object that occurs before a write to a volatile object in the instruction
sequence will occur before that volatile write in the compiled binary.

A read of a volatile object (volatile read) has Acquire semantics; a reference to a
global or static object that occurs after a read of volatile memory in the instruction
sequence will occur after that volatile read in the compiled binary.


_ReadWriteBarrier
http://msdn.microsoft.com/en-us/library/f20w0x5e(v=vs.100).aspx

Microsoft Specific

Forces reads and writes to memory to complete at the point of the call.

The _ReadBarrier, _WriteBarrier, and _ReadWriteBarrier functions help ensure the correct
operation of multithreaded programs that are optimized by the Visual C++ compiler.
A correctly optimized program yields the same results when it executes on multiple
threads as when it executes on a single thread.

The point in an application where a _ReadBarrier, _WriteBarrier, or _ReadWriteBarrier
function executes is called a memory barrier. A memory barrier can be for reads, writes,
or both.

An instruction that accesses a variable in memory might be deleted or moved across a memory
barrier as part of an optimization. Consequently, a thread might read an old value from a
global variable before another thread completes writing a new value to the variable, or
write a new value before another thread completes reading an old value from the variable.

To help ensure that the optimized program operates correctly, the _ReadWriteBarrier function
forces reads and writes to memory to complete at the point of the call. After the call,
other threads can access the memory without fear that the thread that made the call might
have a pending read or write to the memory. A memory barrier prevents the compiler from
optimizing memory accesses across the barrier, but enables the compiler to still optimize
instructions between barriers.


В итоге получается два корректных варианта, один с volatile, второй с _ReadWriteBarrier.
Вариант 1:
// ...

int  volatile Value   = 0;
int  volatile Base    = 0;
bool volatile fReady  = false;

// ...


Вариант 2:
// ...

unsigned int _stdcall Thread_Consumer(void *)
{
    while (false == fReady)
    {
        _ReadWriteBarrier(); // Форсим обращение к памяти fReady.
    }
    
    if (1 != Value) throw;

    return 0;
}



unsigned int _stdcall Thread_Producer(void *)
{
    Value  = Base + 1;
    _ReadWriteBarrier(); // Сохраняем порядок записи в Value и в fReady.
    fReady = true;
    
    return 0;
}

// ...


Сгенерированный код будет примерно одинаковым:
Thread_Consumer:
?Thread_Consumer@@YAIPEAX@Z PROC            ; Thread_Consumer, COMDAT

; 13   : {

$LN11:
    sub    rsp, 40                    ; 00000028H
$LL3@Thread_Con:

; 14   :     while (false == fReady);

    movzx    eax, BYTE PTR ?fReady@@3_NC        ; fReady
    test    al, al
    je    SHORT $LL3@Thread_Con

    ...


Thread_Producer:
?Thread_Producer@@YAIPEAX@Z PROC            ; Thread_Producer, COMDAT

; 24   :     Value  = Base + 1;

    mov    eax, DWORD PTR ?Base@@3HC        ; Base
    inc    eax
    mov    DWORD PTR ?Value@@3HC, eax        ; Value

; 25   :     fReady = true;

    mov    BYTE PTR ?fReady@@3_NC, 1        ; fReady
    
    ...


Теперь пример с STL-контейнером, который уж наверняка не поместится в регистре.
Вопросы синхронизации доступа к std::queue оставим за кадром:
// ...

std::queue<int> g_Queue;



unsigned int _stdcall Thread_Consumer(void *)
{
    while (0 == g_Queue.size());
    if    (123 != g_Queue.front()) throw;

    return 0;
}



unsigned int _stdcall Thread_Producer(void *)
{
    g_Queue.push(123);
    
    return 0;
}

// ...


На тех же настройках компилятора этот код точно также повисает.
Из ассемблерного листинга понятно почему:
?Thread_Consumer@@YAIPEAX@Z PROC            ; Thread_Consumer, COMDAT

; 12   : {

$LN29:
    sub    rsp, 72                    ; 00000048H
    mov    rax, QWORD PTR ?g_Queue@@3V?$queue@HV?$deque@HV?$allocator@H@std@@@std@@@std@@A+32
    npad    5
$LL3@Thread_Con:

; 13   :     while (0 == g_Queue.size());

    test    rax, rax
    je    SHORT $LL3@Thread_Con
    
    ...

Компилятор закэшировал один из членов std::queue в регистре и "не хочет" синхронизировать
его с реальным значением, хранящимся в памяти. Но стоит лишь добавить барьер компилятора,
как все становится на свои места:
// ...

unsigned int _stdcall Thread_Consumer(void *)
{
    size_t Size;
    
    do
    {
        Size = g_Queue.size();
        _ReadWriteBarrier();
    } while (0 == Size);       

    if    (123 != g_Queue.front()) throw;

    return 0;
}

// ...

Здесь обращение к памяти, в которой хранится size, форсится на каждой итерации цикла:
?Thread_Consumer@@YAIPEAX@Z PROC            ; Thread_Consumer, COMDAT

; 12   : {

$LN30:
    sub    rsp, 72                    ; 00000048H
$LL4@Thread_Con:

; 13   :     size_t Size;
; 14   :     
; 15   :     do
; 16   :     {
; 17   :         Size = g_Queue.size();

    mov    rax, QWORD PTR ?g_Queue@@3V?$queue@HV?$deque@HV?$allocator@H@std@@@std@@@std@@A+32

; 18   :         _ReadWriteBarrier();
; 19   :     } while (0 == Size);       

    test    rax, rax
    je    SHORT $LL4@Thread_Con

    ...


Остается вопрос: что выбрать — volatile или _ReadWriteBarrier ?
MSDN пишет:
http://msdn.microsoft.com/en-us/library/f20w0x5e(v=vs.100).aspx

Marking memory with a memory barrier is similar to marking memory with the
volatile (C++) keyword. However, a memory barrier is more efficient because reads and
writes are forced to complete at specific points in the program rather than globally.
The optimizations that can occur if a memory barrier is used cannot occur if the
variable is declared volatile.


Мне больше нравится volatile — поставил один раз и забыл. А за барьерами приходится следить,
чтобы не забыть поставить, где нужно. Это иногда чревато, особенно во время рефакторинга.
Но с другой стороны, если объект C++ объявить volatile, нельзя будет вызывать его
не-volatile методы. Ну то есть, тогда барьерам и альтернативы нет.
Re[6]: multithreading : visibility control
От: uzhas Ниоткуда  
Дата: 31.05.13 17:04
Оценка:
Здравствуйте, okman, Вы писали:

O>Это зависит от того, что именно находится в теле spawnThread.

там просто запуск потока, пусть будет beginthreadex

O>Например, там может быть установка булевого флага, в ожидании которого крутится другой поток.

нет там флагов, нет там ожидания, какие-то ненужные домыслы. в моем примере просто запускается второй поток и мне нужно аккуратно предоставить ему возможность увидеть консистентный и заполненный std::map

O>Вот почему в данном примере хардварные барьеры не нужны (на IA-32 и AMD64).

до сих пор не понял почему не нужен хардварный барьер. меня интересует общий случай для модели памяти, утвержденной в стандарт. Intel\AMD меня не интересуют в данном случае.

O>В результате на простом с виду коде имеем обе проблемы — видимость и переупорядочивание.

O>Бороться с этим можно двумя способами — volatile и барьеры компилятора.
тааак...это уже ближе к теме. и что мне порекомендуете для моего примера? с учетом частого использования find во втором потоке?

O>во-вторых, форсят обращение к памяти.

ну так вот это, видимо, и есть визибилити?!

O>В итоге получается два корректных варианта, один с volatile, второй с _ReadWriteBarrier.


O>unsigned int _stdcall Thread_Consumer(void *)

O>{
O> while (false == fReady)
O> {
O> _ReadWriteBarrier(); // Форсим обращение к памяти fReady.
O> }

O> if (1 != Value) throw;


O> return 0;

O>}

думаю, здесь форс получается не из-за _ReadWriteBarrier, давайте сюда вставим вызов любой другой сложной функции (пусть будет puts), думаю, поведение не изменится

O>Компилятор закэшировал один из членов std::queue в регистре и "не хочет" синхронизировать

O>его с реальным значением, хранящимся в памяти. Но стоит лишь добавить барьер компилятора,
O>как все становится на свои места:
аналогичная ситуация: компилятор просто боится того, что некая функция поменяет контейнер. поэтому не кеширует size() имхо

O>Остается вопрос: что выбрать — volatile или _ReadWriteBarrier ?

O>MSDN пишет:
O>http://msdn.microsoft.com/en-us/library/f20w0x5e(v=vs.100).aspx
O>

O>Marking memory with a memory barrier is similar to marking memory with the
O>volatile (C++) keyword. However, a memory barrier is more efficient because reads and
O>writes are forced to complete at specific points in the program rather than globally.
O>The optimizations that can occur if a memory barrier is used cannot occur if the
O>variable is declared volatile.


не очень понятно как поставить барьер именно на конкретную память. меня барьеры смущают именно из-за неконтролируемой гранулярности (отсутствие локального действия). volatile же привязывается к переменной — более локальное действие.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.