Я довольно новичок в программировании с использованием барьеров / ограждений памяти, и мне было интересно, как мы можем гарантировать, что установочные записи видны в рабочих функциях, которые впоследствии выполняются на других процессорах. Например, рассмотрим следующее:
int setup, sheep;
void SetupSheep(): // Run once
CPU 1: setup = 0;
... much later
CPU 1: sheep = 9;
CPU 1: std::atomic_thread_fence(std::memory_order_release);
CPU 1: setup = 1;
Запустить потом (не одновременно), много, много раз:
void ManipulateSheep():
CPU 2: int mySetup = setup;
CPU 2: std::atomic_thread_fence(std::memory_order_acquire);
CPU 2: // Use sheep...
На CPU 2, если mySetup
это 1, sheep
тогда гарантированно будет 9 — но как мы можем гарантировать, что mySetup
не 0?
Пока что все, о чем я могу думать, это крутить-ждать на CPU 2, пока setup
1. Но это кажется довольно уродливым, учитывая, что спин-ожидание будет ждать только в первый раз ManipulateSheep()
назывался. Конечно, должен быть лучший способ?
Обратите внимание, что есть также симметричная проблема с кодом деинициализации: допустим, вы пишете структуру данных без блокировки, которая выделяет память в течение всего срока ее службы. В деструкторе (при условии, что все потоки закончили вызывать методы) вы хотите освободить всю память, а это означает, что вам нужен процессор, на котором запущен деструктор, чтобы иметь самые последние значения переменных. В этом сценарии даже невозможно вращаться в ожидании, так как деструктор не сможет узнать, каково было «последнее» состояние, чтобы проверить его.
редактировать: Я предполагаю, что я спрашиваю: есть ли способ сказать «Подождите, пока все мои хранилища распространятся на другие процессоры» (для инициализации) и «Подождите, пока все хранилища распространятся на мой процессор» (для деинициализации)?
Оказывается, что #StoreLoad
это точно правильный барьер для этой ситуации. Как объяснил просто Джефф Прешинг:
Барьер StoreLoad гарантирует, что все хранилища, выполненные до барьера, будут видны другим процессорам, и что все нагрузки, выполненные после барьера, получат самое последнее значение, которое видно во время барьера.
В C ++ 11 std::atomic_thread_fence(std::memory_order_seq_cst)
по-видимому, действует как #StoreLoad
барьер (как и остальные три: #StoreStore
, #LoadLoad
, а также #LoadStore
). Увидеть этот проект C ++ 11.
Примечание: на x86 mfence
инструкция действует как #StoreLoad
; как правило, это может быть испущено с _mm_fence()
встроенный компилятор, если это необходимо.
Таким образом, шаблон для кода без блокировки может быть:
Initialize:
CPU 1: setupStuff();
CPU 1: std::atomic_thread_fence(std::memory_order_seq_cst);
Run parallel stuff
Uninitialize:
CPU 2: std::atomic_thread_fence(std::memory_order_seq_cst);
CPU 2: teardownStuff();
Действительно, барьеры памяти не дают вам возможности ждать, пока условие станет реальностью. Вы почти наверняка захотите использовать функциональные возможности, предоставляемые вашей операционной системой, такие как переменная условия pthread или низкоуровневые примитивы, такие как вызовы futex в Linux.
Тем не менее, барьеры, которые вы показали в вашем примере, по крайней мере, достаточно, чтобы ManipulateSheep
могу сказать, готовы ли овцы еще.
(Баа.)