Порядок и видимость модели памяти?

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

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

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

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

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

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

3. предъявляют ли std :: atomic_thread_fences те же требования, что и мьютексы, в том смысле, что для обеспечения согласованности последовательностей на неатомарных варах необходимо выполнять std :: atomic_thread_fence (std :: memory_order_seq_cst); перед загрузкой и std :: atomic_thread_fence (std :: 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);
}

Думаю, что нет, но хотелось бы убедиться.

РЕДАКТИРОВАТЬ: 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
}

person NoSenseEtAl    schedule 18.09.2011    source источник


Ответы (2)


Если вам нравится иметь дело с забором, то 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, либо с помощью инструкции с префиксом LOCK (здесь есть выбор реализации: сделать ли магазин более строгим или загруженным). Следовательно, отдельные ограждения получения и освобождения не выполняются, но atomic_thread_fence(memory_order_seq_cst) - нет (опять же, требуются инструкции MFENCE или LOCKed).

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

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 происходит-до загрузки from i в assert, и assert не сработает.

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

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

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

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

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

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

person Anthony Williams    schedule 19.10.2011
comment
поэтому в 5) для (int i = 0; i ‹16 * 1024; ++ i) nonatomic_p [i] = 42; нельзя переместить после присвоения p? Поскольку memory_order_seq, я полагаю, я прав, я просто хочу проверить. Кстати, отличный ответ! - person NoSenseEtAl; 20.10.2011
comment
Кстати, не могли бы вы подробнее рассказать, почему 4) это гонка за данные? - person NoSenseEtAl; 20.10.2011
comment
Да, вы правы в 5 - назначения nonatomic_p[i] не могут быть перемещены после назначения p. - person Anthony Williams; 20.10.2011
comment
4) - это гонка данных, если два блока выполняются в разных потоках (что, как я предполагаю, и есть необходимость в блокировках мьютексов), поскольку нет ничего, что могло бы упорядочить запись в regularVar1 и regularVar2. Это также гонка данных, если есть какие-либо несинхронизированные чтения из regularVar1 и regularVar2 из другого потока. - person Anthony Williams; 20.10.2011
comment
idk, это противоречит правилам SO, но не могли бы вы прокомментировать, если то, что я спросил и вы ответили, объяснено в вашей книге ... - person NoSenseEtAl; 08.01.2012
comment
@AnthonyWilliams: Но вы отвечаете на 1) ответ на упорядочивающую часть запроса. А как насчет видимости? Или порядок подразумевает видимость во всех случаях, то есть, если сохранение в 'i' произошло до загрузки в 'i', может ли все еще наблюдаться устаревшее значение 'i' из-за буферов записи или когерентности базового кеша архитектура? - person user1715122; 28.01.2014
comment
Если конкретное хранилище происходит - перед определенным чтением из того же места, тогда чтение должно увидеть записанное значение или более позднее значение. - person Anthony Williams; 29.01.2014
comment
a.load(mo_acquire) слабее, чем a.load(mo_relaxed); fence(mo_acquire), если учесть другие переменные в системе с ослаблением, для считывателей, которые не синхронизируются с a. Обновление объясняет (preshing.com/ 20131125 /), что ограждения - это двусторонние барьеры, а поглощающая нагрузка - это только односторонний барьер: более ранние загрузки могут переупорядочиваться после a.load(mo_acquire) вплоть до критического участка или чего-то еще. Но грузовое ограждение останавливает все переупорядочение грузов в любом направлении по нему. Итак, ваша первая пара предложений не совсем точна. - person Peter Cordes; 03.06.2018

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

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

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

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

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

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

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

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

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

person Jason    schedule 18.09.2011
comment
Я думаю (в вашем # 2) загрузки и хранилища до того, как будет введен критический раздел, могут быть перемещены в критический раздел И загружены и сохранены после того, как CS может быть перемещен в CS. Это означает, что компилятор может по-прежнему переупорядочивать (до некоторой степени) не связанные с CS загрузки и сохранения вокруг границ CS. Это потому, что эти загрузки / хранилища изначально не были частью CS. Но ничего нельзя переместить из перед CS в после CS .. только в его середину. - person SoapBox; 19.09.2011
comment
Да, это правильно, по крайней мере, поскольку я понимаю значение семантики получения и выпуска. - person Jason; 19.09.2011
comment
Можно ли создать свой собственный рабочий мьютекс из набора атомарных операций, который вам дан в C ++ 11? - person Omnifarious; 19.09.2011
comment
Да и нет ... вы можете создать мьютекс, где lock() будет спин-блокировкой (то есть операцией занято-ожидание), но вам придется вызвать какой-то тип системного вызова на основе ОС, если вы собираетесь создавать операции, которые фактически используют ресурсы ОС для перевода потока в спящий режим (например, фьютекс в Linux и т. д.). При этом, при желании, вы могли бы использовать атомикс, чтобы создать целую библиотеку потоков на уровне пользователя ... насколько мне известно, это был бы единственный способ создать настоящий мьютекс на уровне пользователя. .. вам все равно придется выполнять вызовы ядра, но не обязательно для самого мьютекса ... - person Jason; 19.09.2011
comment
@Jason - Итак, с набором атомарных операций можно сказать, что все записи, которые происходят в коде до этой операции, должны появиться в памяти до того, как это произойдет. Другими словами, можно сообщить компилятору, что некоторые повторные заказы недействительны. - person Omnifarious; 19.09.2011
comment
@Omnifarious Да ... Я на самом деле только что немного изменил свой ответ в конце, и это потому, что атомарные хранилища и загрузки также реализуют по умолчанию std::memory_order_seq_cst упорядочение памяти. Побочным продуктом использования этого ограждения является то, что он проинструктирует компилятор C ++ также не изменять порядок инструкций. Таким образом, используя std::memory_order_seq_cst, вы эффективно получаете и компилятор, и забор памяти ... вам нужно было бы реализовать явные ограждения только в том случае, если вы установили атомарные загрузки и хранилища с другим членом std::atomic_thread_fence, который допускал упрощенное упорядочение. - person Jason; 19.09.2011