Порядок памяти и видимость? - PullRequest
27 голосов
/ 18 сентября 2011

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

Забор с 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 вставляет memбарьер, в то время как другие только обеспечивают порядок операций в определенной области памяти.

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

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

3.do std :: atomic_thread_fences предъявляет те же требования, что и мьютексы, в том смысле, что для обеспечения последовательности seq для неатомных переменных необходимо выполнить 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
}

Ответы [ 2 ]

25 голосов
/ 19 октября 2011

Если вам нравится иметь дело с заборами, то 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 или 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, и 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 работают так же, как сохранение и загрузка в моем примере выше, и сохранение элементов массива гарантированно произойдет до чтения.

7 голосов
/ 18 сентября 2011

Из того, что я могу сказать, 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, особенно если они не используют по умолчанию (std :: memory_order_seq_cst упорядочение памяти)

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

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...