Подтвердить что ты не робот

Какой std:: sync:: atomic:: Заказ для использования?

Все методы std::sync::atomic::AtomicBool принимают порядок памяти (Relaxed, Release, Acquire, AcqRel и SeqCst), которые я раньше не использовал. При каких обстоятельствах эти значения должны использоваться? В документации используются запутанные термины "загрузка" и "хранение", которые я действительно не понимаю. Например:

Производственный поток мутирует некоторое состояние, сохраненное Mutex, затем вызывает AtomicBool:: compare_and_swap(false, true, ordering) (для объединения недействительности), и если он поменялся, отправляет сообщение "invalidate" в параллельную очередь (например, mpsc или winapi PostMessage). Потребительский поток сбрасывает AtomicBool, считывает из очереди и считывает состояние, сохраненное Mutex. Может ли производитель использовать Relaxed ordering, потому что ему предшествует мьютекс или он должен использовать Release? Может ли потребитель использовать store(false, Relaxed), или он должен использовать compare_and_swap(true, false, Acquire) для получения изменений от мьютекса?

Что делать, если производитель и потребитель разделяют RefCell вместо Mutex?

4b9b3361

Ответ 1

Я не эксперт в этом, и это действительно сложно, поэтому, пожалуйста, не стесняйтесь критиковать мой пост. Как указывает mdh.heydari, cppreference.com имеет гораздо лучшую документацию по заказам, чем Rust (C++ имеет почти идентичный API).


На ваш вопрос

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

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

Если код производителя может запускаться несколько раз перед запуском клиента, вы не можете использовать RefCell потому что они могут RefCell данные, пока клиент их читает. В противном случае это нормально.

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


Какие заказы?

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

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

Вы должны думать об этих порядках как о применении к ячейке памяти (а не к инструкции).

Типы заказа

Расслабленный заказ

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

Получить заказ

Это ограничение говорит о том, что любые чтения переменных, которые происходят в вашем коде после применения "acqu", не могут быть переупорядочены, чтобы происходить до этого. Итак, скажем, в вашем коде вы читаете какое-то место в общей памяти и получаете значение X, которое было сохранено в этом месте памяти в момент времени T, а затем вы применяете ограничение "приобретения". Любые области памяти, из которых вы прочитали после применения ограничения, будут иметь значение, которое они имели в момент времени T или позже.

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

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

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

Выпуск заказа

Это ограничение говорит о том, что любые записи переменных, которые происходят в вашем коде до применения "релиза", не могут быть переупорядочены, чтобы происходить после него. Итак, скажем, в вашем коде вы пишете в несколько общих областей памяти, а затем устанавливаете некоторую область памяти t в момент времени T, а затем применяете ограничение "освобождение". Любые записи, которые появляются в вашем коде до применения "релиза", гарантированно произошли до него.

Опять же, это то, что большинство людей ожидают интуитивно, но это не гарантируется без ограничений.

Если другой поток, пытающийся прочитать значение X, не использует "acqu", тогда не гарантируется увидеть новое значение в отношении изменений в других значениях переменной. Таким образом, он может получить новое значение, но он может не увидеть новые значения для любых других общих переменных. Также имейте в виду, что тестирование сложно. Некоторое оборудование на практике не показывает переупорядочение с небезопасным кодом, поэтому проблемы могут остаться незамеченными.

Джефф Прешинг написал хорошее объяснение семантики получения и выпуска, поэтому прочитайте это, если это не ясно.

AcqRel Ordering

Это относится как к порядку Acquire и к Release (т.е. Применяются оба ограничения). Я не уверен, когда это необходимо - это может быть полезно в ситуациях с 3 или более потоками, если некоторые Release, некоторые Acquire, а некоторые делают оба, но я не совсем уверен.

SeqCst Заказ

Это наиболее строгий и, следовательно, самый медленный вариант. Это заставляет обращения к памяти появляться в одном, идентичном порядке для каждого потока. Для этого требуется инструкция MFENCE на x86 для всех MFENCE записи в атомарные переменные (полный барьер памяти, включая StoreLoad), в то время как более слабые упорядочения этого не делают. (Загрузка SeqCst не требует барьера для x86, как вы можете видеть в этом выводе компилятора C++.)

Доступ на чтение, изменение и запись, например, атомарный инкремент или сравнение и замена, выполняются в x86 с lock инструкциями, которые уже являются полными барьерами памяти. Если вам нужна компиляция в эффективный код для целей, отличных от x86, имеет смысл избегать SeqCst, когда это возможно, даже для атомарных операций чтения-изменения-записи. Однако есть случаи, когда это необходимо.

Дополнительные примеры того, как атомарная семантика превращается в ASM, см. В этом большом наборе простых функций для атомарных переменных C++. Я знаю, что это вопрос Rust, но он должен иметь в основном тот же API, что и C++. Godbolt может предназначаться для x86, ARM, ARM64 и PowerPC. Интересно, что ARM64 имеет инструкции load- ldar (ldar) и store-release (stlr), поэтому он не всегда должен использовать отдельные инструкции барьера.


Кстати, процессоры x86 по умолчанию всегда "строго упорядочены", что означает, что они всегда действуют так, как если бы был установлен режим AcqRel. Таким образом, для x86 "упорядочение" влияет только на поведение оптимизатора LLVM. ARM, с другой стороны, слабо упорядочен. По умолчанию установлено значение Relaxed, чтобы предоставить компилятору полную свободу в переупорядочении, и не требовать дополнительных инструкций барьера для слабо упорядоченных процессоров.