Моделирование и видимость модели памяти?

Я попытался найти подробности об этом, я даже прочитал стандарт по мьютексам и атомизации... но все же я не мог понять возможности видимости модели памяти С++ 11. Из того, что я понимаю, очень важной особенностью взаимного исключения мьютекса BESIDE является обеспечение видимости. Ака недостаточно, чтобы только один поток за время увеличивал счетчик, важно, чтобы поток увеличил счетчик, который был сохранен потоком, который последний раз использовал мьютекс (я действительно не знаю, почему люди больше не упоминают об этом больше при обсуждении мьютексы, может быть, у меня были плохие учителя:)). Поэтому из того, что я могу сказать, атомный не обеспечивает немедленную видимость: (от человека, который поддерживает boost:: thread и реализовал библиотеку С++ 11 thread и mutex):

Забор с memory_order_seq_cst не обеспечивает немедленного видимость для других потоков (а также инструкция MFENCE). Ограничения порядка памяти С++ 0x - это то, что: порядок ограничения. Операции memory_order_seq_cst образуют общий порядок, но нет никаких ограничений на то, что этот порядок, за исключением того, что он должен согласовываться всеми потоками, и он не должен нарушать другие заказы ограничения. В частности, потоки могут продолжать видеть "устаревшие" значения в течение некоторого времени, если они видят значения в порядке, согласованном с ограничения.

И я в порядке с этим. Но проблема в том, что мне трудно понять, какие конструкции С++ 11 относительно атома являются "глобальными" и которые обеспечивают согласованность только для атомных переменных. В частности, у меня есть понимание того, какие (если таковые имеются) из следующих порядков памяти гарантируют, что будет забор памяти до и после загрузки и хранения: http://www.stdthread.co.uk/doc/headers/atomic/memory_order.html

Из того, что я могу сказать, std:: memory_order_seq_cst вставляет мембранный барьер, в то время как другие только обеспечивают упорядочение операций в определенной ячейке памяти.

Итак, кто-нибудь может это понять, я полагаю, что многие люди собираются делать ужасные ошибки, используя std:: atomic, esp, если они не используют по умолчанию (std:: memory_order_seq_cst порядок памяти)
2. Если я прав, это означает, что вторая строка redundand в этом коде:

atomicVar.store(42);
std::atomic_thread_fence(std::memory_order_seq_cst);  

3. do std:: atomic_thread_fences имеют те же требования, что и мьютексы, в том смысле, что для обеспечения согласованности seq на неатомических vars нужно делать std:: atomic_thread_fence (std:: memory_order_seq_cst); перед загрузкой и           станд:: atomic_thread_fence (станд:: memory_order_seq_cst);
после магазинов?
4. Является ли

  {
    regularSum+=atomicVar.load();
    regularVar1++;
    regularVar2++;
    }
    //...
    {
    regularVar1++;
    regularVar2++;
    atomicVar.store(74656);
  }

эквивалентно

std::mutex mtx;
{
   std::unique_lock<std::mutex> ul(mtx);
   sum+=nowRegularVar;
   regularVar++;
   regularVar2++;
}
//..
{
   std::unique_lock<std::mutex> ul(mtx);
    regularVar1++;
    regularVar2++;
    nowRegularVar=(74656);
}

Я думаю, что нет, но я хотел бы быть уверен.

EDIT: 5. Может утверждать огонь?
Существуют только два потока.

atomic<int*> p=nullptr; 

первый поток пишет

{
    nonatomic_p=(int*) malloc(16*1024*sizeof(int));
    for(int i=0;i<16*1024;++i)
    nonatomic_p[i]=42;
    p=nonatomic;
}

вторая строка читает

{
    while (p==nullptr)
    {
    }
    assert(p[1234]==42);//1234-random idx in array
}

Ответ 1

Если вам нравится иметь дело с заборами, то a.load(memory_order_acquire) эквивалентно a.load(memory_order_relaxed), за которым следует atomic_thread_fence(memory_order_acquire). Аналогично, a.store(x,memory_order_release) эквивалентно вызову atomic_thread_fence(memory_order_release) перед вызовом a.store(x,memory_order_relaxed). memory_order_consume - частный случай memory_order_acquire, для зависимых данных только. memory_order_seq_cst является специальным и формирует полный порядок во всех операциях memory_order_seq_cst. Смешанный с другими, это то же самое, что и приобретение для загрузки, и выпуск для магазина. memory_order_acq_rel предназначен для операций чтения-модификации-записи и эквивалентен приобретению в считываемой части и освобождению на части записи RMW.

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

На x86 нагрузки всегда приобретаются, а магазины всегда выпускаются. memory_order_seq_cst требует более сильного упорядочения либо с инструкцией MFENCE, либо с инструкцией prefixed LOCK (здесь есть выбор реализации, чтобы сделать хранилище более сильным или загружать). Следовательно, автономные захват и освобождение забора не являются операциями, но atomic_thread_fence(memory_order_seq_cst) не является (опять требуется команда MFENCE или LOCK ed).

Важным эффектом ограничений порядка является то, что они упорядочивают другие операции.

std::atomic<bool> ready(false);
int i=0;

void thread_1()
{
    i=42;
    ready.store(true,memory_order_release);
}

void thread_2()
{
    while(!ready.load(memory_order_acquire)) std::this_thread::yield();
    assert(i==42);
}

thread_2 вращается, пока не прочитает true из ready. Поскольку хранилище до ready в thread_1 - это релиз, а загрузка - это приобретение, то хранилище синхронизируется с загрузкой, а хранилище до i происходит до загрузки из i в assert, и утверждение не будет срабатывать.

2) Вторая строка в

atomicVar.store(42);
std::atomic_thread_fence(std::memory_order_seq_cst);  

действительно потенциально избыточно, потому что хранилище до atomicVar по умолчанию использует memory_order_seq_cst. Однако, если в этом потоке есть другие не-t28 > атомные операции, то забор может иметь последствия. Например, он будет действовать как забор для последующего a.store(x,memory_order_relaxed).

3) Заборы и атомные операции не работают как мьютексы. Вы можете использовать их для создания мьютексов, но они не работают, как они. Вам не нужно использовать atomic_thread_fence(memory_order_seq_cst). Нет никаких требований, чтобы любые атомные операции были memory_order_seq_cst, и упорядочение по неатомным переменным может быть достигнуто без, как в приведенном выше примере.

4) Нет, это не эквивалентно. Таким образом, ваш фрагмент без блокировки мьютекса - это гонка данных и поведение undefined.

5) Нет вашего утверждения, который не может стрелять. При упорядочении памяти по умолчанию memory_order_seq_cst хранилище и загрузка от атомарного указателя p работают как хранилище и загрузка в моем примере выше, и хранилища к элементам массива гарантированно произойдут - перед чтением.

Ответ 2

Из того, что я могу сказать, std:: memory_order_seq_cst вставляет mem барьер, в то время как другие только обеспечивают упорядочение операций в определенной ячейке памяти.

Это действительно зависит от того, что вы делаете, и на какой платформе вы работаете. Сильная модель упорядочения памяти на платформе, такой как x86, создаст другой набор требований к существованию операций забора памяти по сравнению с более слабой моделью упорядочения на таких платформах, как IA64, PowerPC, ARM и т.д. Что такое параметр по умолчанию std::memory_order_seq_cst обеспечение того, что в зависимости от платформы будут использоваться правильные инструкции по ограждению памяти. На платформе, такой как x86, нет необходимости в полном защите памяти, если вы не выполняете операцию чтения-изменения-записи. В модели памяти x86 все нагрузки имеют семантику загрузки нагрузки, и все магазины имеют семантику хранилища. Таким образом, в этих случаях перечисление std::memory_order_seq_cst в основном создает no-op, так как модель памяти для x86 уже гарантирует, что эти типы операций согласованы между потоками, и поэтому нет инструкций по сборке, которые реализуют эти типы парциальных барьеров памяти. Таким образом, одно и то же условие no-op было бы истинным, если бы вы явно установили параметр std::memory_order_release или std::memory_order_acquire на x86. Кроме того, требование полного барьера памяти в этих ситуациях было бы лишним препятствием для производительности. Как отмечено, это потребовалось бы только для операций чтения-изменения-хранения.

Однако на других платформах с более слабыми моделями согласованности памяти это не так, и поэтому использование std::memory_order_seq_cst будет использовать правильные операции забора памяти, если пользователь не должен явно указывать, хотят ли они загружать-приобретать, хранить - освобождение или полная операция забора памяти. Эти платформы имеют конкретные машинные инструкции для обеспечения выполнения таких согласований согласованности с памятью, а параметр std::memory_order_seq_cst будет работать в правильном случае. Если пользователь хотел бы специально вызвать одну из этих операций, они могут через явные типы перечисления std::memory_order, но это было бы необязательно... компилятор разработал правильные настройки.

Я предполагаю, что многие люди собираются делать ужасные ошибки, используя std:: atomic, esp, если они не используют по умолчанию (std:: memory_order_seq_cst порядок памяти)

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

Наконец, имейте в виду, что в вашей ситуации № 4 относительно мьютекса есть две разные вещи, которые должны произойти здесь:

  • У компилятора не должно быть возможности переупорядочивать операции в мьютексе и критическом разделе (особенно в случае оптимизирующего компилятора).
  • Должны быть созданы необходимые ограждения памяти (в зависимости от платформы), которые поддерживают состояние, в котором все хранилища завершены до критического раздела и чтения переменной mutex, и все магазины завершаются до выхода из критического раздела.

Поскольку по умолчанию запасы атомов и нагрузки реализуются с помощью std::memory_order_seq_cst, то использование атомистики также будет реализовывать надлежащие механизмы для удовлетворения условий №1 и №2. При этом в вашем первом примере с атомикой нагрузка обеспечивала бы получение семантики для блока, в то время как хранилище обеспечивало бы семантику выпуска. Тем не менее, это не повлияло бы на какой-либо конкретный порядок внутри "критического раздела" между этими двумя операциями. Во втором примере у вас есть два разных раздела с замками, каждый из которых имеет семантику. Поскольку в какой-то момент вам придется освобождать блокировки, которые будут иметь семантику выпуска, тогда нет, два блока кода не будут эквивалентны. В первом примере вы создали большой "критический раздел" между загрузкой и хранилищем (при условии, что все это происходит в одном потоке). Во втором примере у вас есть два разных критических раздела.

P.S. Я нашел следующий PDF особенно поучительный, и вы можете найти его также: http://www.nwcpp.org/Downloads/2008/Memory_Fences.pdf