Как сделать так, чтобы хранилища памяти в одном потоке "быстро" были видны в других потоках?

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

#include <atomic>

volatile int * const Device_reg_ptr = reinterpret_cast<int *>(0x666);

// This variable is read by multiple threads.
std::atomic<int> device_reg_copy;

// ...

// Method 1
const_cast<volatile std::atomic<int> &>(device_reg_copy)
  .store(*Device_reg_ptr, std::memory_order_relaxed);

// Method 2
device_reg_copy.store(*Device_reg_ptr, std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_release);

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

РЕДАКТИРОВАТЬ: В своем ответе, пожалуйста, рассмотрите следующий сценарий:

  • Код работает на процессоре во встроенной системе.
  • На ЦП работает одно приложение.
  • В приложении гораздо меньше потоков, чем в процессорных ядрах.
  • Каждое ядро ​​имеет огромное количество регистров.
  • Приложение достаточно маленькое, чтобы вся оптимизация программы успешно использовалась при создании исполняемого файла.

Как сделать так, чтобы хранилище в одном потоке не оставалось невидимым для других потоков в течение неопределенного времени?

2 ответа

Стандарт C++ довольно расплывчат в том, чтобы сделать атомарные хранилища видимыми для других потоков.

29.3.12 Реализации должны сделать атомные хранилища видимыми для атомных нагрузок в течение разумного периода времени.

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

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

Если важна подсказка, я бы подумал об использовании операции Read-Modify-Write (RMW) для загрузки значения. Это атомарные операции, которые читают и изменяют атомарно (то есть в одном вызове) и имеют дополнительное свойство, что они гарантированно будут работать с самым последним значением. Но так как они должны достигать немного дальше, чем локальный кеш, эти вызовы также имеют тенденцию быть более дорогостоящими для выполнения.

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

// thread 1
device_reg_copy.store(*Device_reg_ptr, std::memory_order_release);


// thread 2
device_reg_copy.fetch_add(0, std::memory_order_acquire);

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

Если атомарная переменная не зависит от каких-либо других данных, вы можете использовать std::memory_order_relaxed; Порядок хранения всегда гарантирован для одной атомарной переменной.

Как уже упоминалось, нет необходимости volatile когда дело доходит до связи между потоками с std::atomic,

Если вы хотите обновить значение device_reg_copy то атомно, то device_reg_copy.store(*Device_reg_ptr, std::memory_order_relaxed); достаточно.

Там нет необходимости применять volatile для атомарных переменных, это не нужно.

std::memory_order_relaxed Предполагается, что для хранилища требуется меньше всего времени на синхронизацию. На х86 это просто равнина mov инструкция.

Однако, если вы хотите обновить его таким образом, чтобы эффекты любых предыдущих хранилищ стали видны другим потокам вместе с новым значением device_reg_copyзатем используйте std::memory_order_release хранить, т.е. device_reg_copy.store(*Device_reg_ptr, std::memory_order_release);, Читатели должны загрузить device_reg_copy как std::memory_order_acquire в этом случае. Опять на х86 std::memory_order_release магазин простой mov,

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

Вот почему они говорят, что модель памяти x86 слишком сильна для C++11: обычная mov инструкция std::memory_order_release в магазинах и std::memory_order_acquire на нагрузках. Там нет расслабленного магазина или нагрузки на x86.

Я не могу порекомендовать достаточно статьи об ошибке очистки кэша процессора.

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