Что означает каждый элемент memory_order?

Я прочитал главу, и мне это не очень понравилось. Мне все еще неясно, в чем различия между каждым порядком памяти. Это мое текущее предположение, которое я понял после прочтения гораздо более простого http://en.cppreference.com/w/cpp/atomic/memory_order

Ниже это неправильно, поэтому не пытайтесь учиться на этом

  • memory_order_relaxed: не синхронизируется, но не игнорируется, когда порядок сделан из другого режима в другом атомарном var
  • memory_order_consume: синхронизирует чтение этой атомарной переменной, однако не синхронизирует расслабленные переменные, написанные до этого. Однако, если поток использует var X при изменении Y (и освобождает его). Другие потоки, использующие Y, также увидят, что X выпущен? Я не знаю, означает ли это, что этот поток выталкивает изменения х (и, очевидно, у)
  • memory_order_acquire: Синхронизирует чтение этой атомарной переменной И гарантирует, что расслабленные переменные, записанные до этого, также синхронизируются. (Значит ли это, что все атомные переменные во всех потоках синхронизируются?)
  • memory_order_release: передает атомарное хранилище в другие потоки (но только если они читают переменную с использованием / получением)
  • memory_order_acq_rel: для операций чтения / записи. Делает приобретение, чтобы вы не изменяли старое значение и не отменяли изменения.
  • memory_order_seq_cst: то же самое, что и release release, за исключением того, что заставляет обновления быть видимыми в других потоках (если a Хранить с расслабленным на другом потоке. Я храню b с seq_cst. Чтение 3-го потока a с расслаблением увидим изменения вместе с b и любая другая атомная переменная?).

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

3 ответа

Решение

GCC Wiki дает очень подробное и простое для понимания объяснение с примерами кода.

(отредактированный отрывок и выделение добавлено)

ВАЖНЫЙ:

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

Первая модель является "последовательно последовательной". Этот режим используется по умолчанию, если он не указан, и является наиболее ограничительным. Это также может быть явно указано через memory_order_seq_cst, Он обеспечивает те же ограничения и ограничения для перемещения нагрузки, с которыми последовательные программисты по своей природе знакомы, за исключением того, что он применяется ко всем потокам.
[...]
С практической точки зрения это сводится ко всем атомным операциям, выступающим в качестве барьеров для оптимизации. Можно переупорядочивать вещи между атомарными операциями, но не между операциями. Локальное содержимое потока также не затронуто, так как нет никакой видимости другим потокам. [...] Этот режим также обеспечивает согласованность всех потоков.

Противоположный подход memory_order_relaxed, Эта модель обеспечивает гораздо меньшую синхронизацию, устраняя ограничения, возникающие до того. У этих типов атомарных операций также могут быть различные оптимизации, такие как удаление мертвых хранилищ и их объединение. [...] Без каких-либо ребер, предшествующих событию, ни один поток не может рассчитывать на конкретный порядок в другом потоке.
Расслабленный режим чаще всего используется, когда программист просто хочет, чтобы переменная была атомарной по своей природе, а не использовала ее для синхронизации потоков для других данных общей памяти.

Третий режим (memory_order_acquire / memory_order_release) является гибридом между двумя другими. Режим получения / выпуска аналогичен последовательно согласованному режиму, за исключением того, что он применяет только отношение "до и к" для зависимых переменных. Это позволяет ослабить синхронизацию, требуемую между независимыми чтениями независимых записей.

memory_order_consume является еще одним тонким уточнением в модели памяти освобождения / приобретения, которая слегка ослабляет требования, удаляя случай перед упорядочиванием по независимым разделяемым переменным.
[...]
Реальная разница сводится к тому, в каком состоянии аппаратное обеспечение должно быть сброшено для синхронизации. Поскольку операция потребления может, следовательно, выполняться быстрее, тот, кто знает, что он делает, может использовать ее для приложений, критичных для производительности.

Вот моя собственная попытка более мирского объяснения:

Другой подход к этому - взглянуть на проблему с точки зрения переупорядочения операций чтения и записи, как атомарных, так и обычных:

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

Теперь расслабленный - это просто минимум. Кроме того, он ничего не делает и не предоставляет никаких других гарантий. Это самая дешевая операция. Для операций чтения-изменения-записи в строго упорядоченных процессорных архитектурах (например, x86/amd64) это сводится к обычному обычному ходу.

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

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

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

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

В настоящее время не рекомендуется использовать порядок потребления во время пересмотра спецификации.

Это довольно сложный предмет. Попробуйте прочитать http://en.cppreference.com/w/cpp/atomic/memory_order несколько раз, попробуйте прочитать другие ресурсы и т. Д.

Вот упрощенное описание:

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

Обычно вы можете использовать блокировки для синхронизации. Проблема в том, что они медленные. Атомарные операции выполняются намного быстрее, потому что синхронизация происходит на уровне ЦП (т. Е. ЦП гарантирует, что никакой другой поток, даже на другом ЦП, не изменит какую-либо переменную и т. Д.).

Итак, единственная проблема, с которой мы сталкиваемся, это изменение порядка доступа к памяти. memory_order enum указывает, какие типы переупорядочений компилятор должен запретить.

relaxed - нет ограничений.

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

acquire - никакие грузы не могут быть переупорядочены в отношении. атомная нагрузка. Т.е., если они находятся после атомарной нагрузки в исходном коде, они будут происходить и после атомарной нагрузки.

release - никакие магазины не могут быть переупорядочены в отношении. атомный магазин. Т.е., если они находятся перед атомарным хранилищем в исходном коде, они будут происходить и до атомарного хранилища.

acq_rel - acquire а также release вместе взятые.

seq_cst - сложнее понять, почему требуется этот порядок. По сути, все остальные упорядочения гарантируют, что определенные запрещенные переупорядочения не будут происходить только для потоков, которые потребляют / выпускают одну и ту же атомарную переменную. Доступ к памяти все еще может распространяться на другие потоки в любом порядке. Этот порядок гарантирует, что этого не произойдет (таким образом, последовательная последовательность). Для случая, когда это необходимо, смотрите пример в конце связанной страницы.

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


Что следует игнорировать:

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

Большая часть статьи cppreference о заказах памяти посвящена «потреблению», поэтому ее удаление значительно упрощает работу.

Это также позволяет игнорировать связанные функции, такие как а также .


Гонки данных: запись в неатомарную переменную из одного потока и одновременное чтение/запись в нее из другого потока называется гонкой данных и вызывает неопределенное поведение.


memory_order_relaxedявляется самым слабым и предположительно самым быстрым порядком памяти.

Любые операции чтения/записи в atomics не могут вызвать гонку данных (и последующий UB). обеспечивает именно эту минимальную гарантию для одной переменной. Он не дает никаких гарантий для других переменных (атомарных или нет).

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

Единственные правила:

  • Каждый поток будет обращаться к каждой отдельной переменной именно в том порядке, в котором вы ему укажете. Например Напишу , тогда , никогда в обратном порядке. Но доступы к разным переменным в одном и том же потоке все равно могут быть переупорядочены относительно друг друга.
  • Если поток A записывает в переменную несколько раз, то поток B читает несколько раз, он получит значения в том же порядке (но, конечно, он может прочитать некоторые значения несколько раз, или пропустить некоторые, если вы не синхронизируете нити другими способами).

Пример использования: Все, что не пытается использовать атомарную переменную для синхронизации доступа к неатомарным данным: различные счетчики (которые существуют только в информационных целях) или «флаги остановки», чтобы сигнализировать другим потокам об остановке. Другой пример: операции над s, которые увеличивают счетчик ссылок, внутри используют .


Заборы: ничего не делает.


, memory_order_acquireделай все делает и многое другое (так что это якобы медленнее или эквивалентно).

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

Они позволяют синхронизировать потоки:

  • Допустим, поток 1 читает/записывает в некоторую память M (любые неатомарные или атомарные переменные, не имеет значения).

  • Затем поток 1 выполняет освобождение хранилища в переменной A. Затем он перестает обращаться к этой памяти.

  • Если поток 2 затем выполняет загрузку той же самой переменной A, говорят, что эта загрузка синхронизируется с соответствующим хранилищем в потоке 1.

  • Теперь поток 2 может безопасно читать/записывать в эту память M.

Вы синхронизируетесь только с последним модулем записи, а не с предыдущими модулями записи.

Вы можете связать синхронизацию между несколькими потоками.

Существует специальное правило, согласно которому синхронизация распространяется на любое количество операций чтения-изменения-записи независимо от их порядка в памяти. Например, если поток 1 делает , то поток 2 делает , то поток 3 делает , то поток 1 успешно синхронизируется с потоком 3, несмотря на то, что посередине есть нестрогая операция.

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

Если задействованы операции чтения-изменения-записи, ничто не мешает вам синхронизироваться более чем с одной операцией. В приведенном выше примере, если использовал или , он также синхронизировался бы с потоком 1, и наоборот, если бы он использовал или , поток 3 синхронизировался бы с 2 в дополнение к 1.


Пример использования: уменьшает свой счетчик ссылок, используя что-то вроде .

Вот почему: представьте, что поток 1 читает/пишет в , затем уничтожает свою копию , уменьшая счетчик ссылок. Затем поток 2 уничтожает последний оставшийся указатель, также уменьшая счетчик ссылок, а затем запускает деструктор.

Поскольку деструктор в потоке 2 собирается получить доступ к памяти, к которой ранее обращался поток 1, синхронизация в является необходимым. В противном случае у вас будет гонка данных и UB.


Заборы: Использование , вы можете по существу превратить расслабленные атомарные операции в операции освобождения/приобретения. Один забор может применяться к более чем одной операции и/или может выполняться условно.

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

И наоборот, если вы делаете , за которым следует любое количество (возможно, упрощенных) записей, эти записи считаются операциями освобождения.

Забор сочетает в себе эффект и ограждения.


Сходство с другими функциями стандартной библиотеки:

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


memory_order_seq_cstделает все / делать и многое другое. Это якобы самый медленный порядок, но и самый безопасный.

чтения считаются как операции получения. записи считаются операциями освобождения. Операции чтения-изменения-записи учитываются как обе.

операции могут синхронизироваться друг с другом и с операциями захвата/освобождения. Остерегайтесь особых эффектов их смешивания (см. ниже).

порядок по умолчанию, например заданный , делает .

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

Среди прочего, этот порядок соответствует описанному выше отношению синхронизации с получением/освобождением, если только (и это странно) синхронизация не происходит из -за смешивания операции seq-cst с операцией получения/освобождения (синхронизация освобождения с помощью seq-cst или синхронизация seq-cst с приобретать). Такой микс по существу понижает затронутую операцию seq-cst до операции захвата/освобождения (возможно, он сохраняет некоторые свойства seq-cst, но на это лучше не рассчитывать).


Пример использования:

      atomic_bool x = true;
atomic_bool y = true;
// Thread 1:
x.store(false, seq_cst);
if (y.load(seq_cst)) {...}
// Thread 2:
y.store(false, seq_cst);
if (x.load(seq_cst)) {...}

Допустим, вы хотите, чтобы только один поток мог войти в тело. позволяет это сделать. Приобретение/выпуск или более слабые ордера здесь будут недостаточны.


Заборы: делает все забор делает, плюс дополнительная функция.

Скажем:

  • Поток 1 обращается к переменной X, используя порядок, затем
  • поток 2 обращается к одной и той же переменной X, используя любой порядок (не обязательно вызывая синхронизацию), затем
  • нить 2 делает .

Тогда любая операция в потоке 2 после ограждения будет выполняться после доступа к seq-cst в потоке 1 (но операции без seq-cst до этой операции seq-cst в потоке 1 не обязательно будут выполняться до ограждения в потоке 2).

Обратное также работает:

  • Тема 1 делает , затем
  • поток 1 обращается к переменной X в любом порядке, затем
  • поток 2 обращается к той же самой переменной X, используя порядок.

Тогда любые операции перед ограждением в потоке 1 будут выполняться до этой операции seq-cst в потоке 2, но не обязательно перед последующими операциями, отличными от seq-cst, в потоке 2.

Вы также можете иметь заборы с обеих сторон:

  • Тема 1 делает , затем
  • поток 1 обращается к переменной X в любом порядке, затем
  • поток 2 обращается к той же самой переменной X, используя любой порядок, затем
  • нить 2 делает atomic_thread_fence(seq_cst)

Тогда что-либо в потоке 1 перед забором произойдет раньше, чем что-либо в потоке 2 после забора.


Взаимодействие между различными заказами

Подводя итог вышесказанному:

* = Участвующая операция seq-cst получает неверный порядок seq-cst, что фактически понижается до операции захвата/освобождения. Это объясняется выше.


Ускоряет ли использование более сильного порядка памяти передачу данных между потоками?

Нет, кажется, нет.


Последовательная согласованность для программ без гонки данных

Стандарт объясняет, что если ваша программа использует только доступа (и мьютексов) и не имеет гонок данных (которые вызывают UB), вам не нужно думать обо всех причудливых переупорядочениях операций. Программа будет вести себя так, как если бы одновременно выполнялся только один поток, причем потоки непредсказуемо чередовались.

Другие вопросы по тегам