Вопрос

У меня есть вопрос по поводу следующего примера кода (m_value ( значение m_value ) не является изменчивым, и каждый поток выполняется на отдельном процессоре)

void Foo() // executed by thread #1, BEFORE Bar() is executed
{
   Interlocked.Exchange(ref m_value, 1);
}

bool Bar() // executed by thread #2, AFTER Foo() is executed
{
   return m_value == 1;
}

Использует ли Взаимосвязано.Обмен в Foo() гарантирует, что при выполнении Bar() я увижу значение "1"?(даже если значение уже существует в регистре или строке кэша?) Или мне нужно установить барьер памяти перед чтением значения m_value ( значение m_value )?

Кроме того (не связанный с исходным вопросом), законно ли объявлять изменяемый элемент и передавать его по ссылке на Взаимосвязанный XXX методы?(компилятор предупреждает о передаче летучих веществ по ссылке, поэтому должен ли я игнорировать предупреждение в таком случае?)

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

Это было полезно?

Решение

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

while (!pShared->lock.testAndSet_Acquire()) ;
// (this loop should include all the normal critical section stuff like
// spin, waste, 
// pause() instructions, and last-resort-give-up-and-blocking on a resource 
// until the lock is made available.)

// Access to shared memory.

pShared->foo = 1 
v = pShared-> goo

pShared->lock.clear_Release()

Указанный выше барьер получения памяти гарантирует, что все загрузки (pShared-> goo), которые могли быть запущены до успешной модификации блокировки, будут сброшены и при необходимости будут перезапущены.

Барьер освобождения памяти гарантирует, что загрузка из goo в (локальную, скажем) переменную v будет завершена до того, как будет очищено слово блокировки, защищающее разделяемую память.

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

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

pShared->goo = 14

pShared->atomic.setBit_Release()

Без барьера "записи" здесь, в производителе, у вас нет гарантии, что аппаратное обеспечение не попадет в хранилище atomic до того, как хранилище goo пройдет через очереди хранилища cpu и вверх по иерархии памяти, где оно видно (даже если у вас есть механизм, который гарантирует, что компилятор упорядочивает вещи так, как вы хотите).

В потребителе

if ( pShared->atomic.compareAndSwap_Acquire(1,1) )
{
   v = pShared->goo 
}

Без барьера "чтение" здесь вы не будете знать, что аппаратное обеспечение не загрузило goo для вас до завершения атомарного доступа.Атомарный (то есть:память, управляемая с помощью заблокированных функций, выполняющих такие вещи, как lock cmpxchg), является "атомарной" только по отношению к самой себе, а не к другой памяти.

Теперь, оставшаяся вещь, о которой следует упомянуть, - это то, что конструкции барьера крайне непереносимы.Ваш компилятор, вероятно, предоставляет варианты _acquire и _release для большинства методов атомарной манипуляции, и это те способы, которыми вы могли бы их использовать.В зависимости от используемой вами платформы (т.е.:ia32), это вполне может быть именно то, что вы получили бы без суффиксов _acquire() или _release().Платформы, где это имеет значение, - ia64 (фактически мертвый, за исключением HP, где он все еще слегка подергивается) и powerpc.ia64 имел модификаторы команд .acq и .rel в большинстве инструкций загрузки и сохранения (включая атомарные, такие как cmpxchg).powerpc имеет отдельные инструкции для этого (isync и lwsync предоставляют вам барьеры чтения и записи соответственно).

Сейчас же.Сказав все это.У вас действительно есть веская причина пойти по этому пути?Сделать все это правильно может быть очень сложно.Будьте готовы к многочисленным сомнениям в себе и неуверенности в проверках кода и убедитесь, что у вас много тестов с высоким уровнем параллелизма со всевозможными случайными сценариями синхронизации.Используйте критический раздел, если только у вас нет очень-очень веских причин избегать этого, и не пишите этот критический раздел самостоятельно.

Другие советы

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

Что вам действительно нужно, так это атомарные операции, т. е.Функции InterlockedXXX, или изменяемые переменные в C #.Если бы строка чтения в была атомарной, вы могли бы гарантировать, что ни компилятор, ни центральный процессор не выполняют никаких оптимизаций, которые не позволяют ему считывать значение либо до записи в Foo, либо после записи в Foo, в зависимости от того, что выполняется первым.Поскольку вы говорите, что вы "знаете", что запись Foo происходит до чтения Bar , то Bar всегда будет возвращать true .

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

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

Я не совсем уверен, но я думаю, что сблокированный.Exchange будет использовать Функция InterlockedExchange из Windows API в любом случае, это обеспечивает полный барьер памяти.

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

Взаимосвязанные операции обмена гарантируют наличие барьера памяти.

Следующие функции синхронизации используют соответствующие барьеры для обеспечения упорядоченности памяти:

  • Функции, которые входят в критические разделы или покидают их

  • Функции, которые сигнализируют об объектах синхронизации

  • Функции ожидания

  • Взаимосвязанные функции

(Источник : Ссылка)

Но вам не повезло с регистровыми переменными.Если m_value находится в регистре в Bar, вы не увидите изменения на m_value.В связи с этим вам следует объявить общие переменные 'volatile'.

Если m_value не помечен как volatile, тогда нет оснований думать , что значение , прочитанное в Bar огорожен забором.Оптимизация компилятора, кэширование или другие факторы могут изменить порядок операций чтения и записи.Взаимосвязанный обмен полезен только тогда, когда он используется в экосистеме должным образом огороженных ссылок на память.В этом весь смысл маркировки поля volatile.Модель памяти .Net не так прямолинейна, как некоторые могли бы ожидать.

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

Я удивлен, что компилятор жалуется на передачу volatile в Interlocked .Exchange() - тот факт, что вы используете Interlocked.Exchange() почти должен указывать volatile переменную.

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

Если вы не сообщите компилятору или среде выполнения, что m_value не следует считывать перед Bar(), он может и может кэшировать значение m_value впереди Bar() и просто используйте кэшированное значение.Если вы хотите убедиться, что он видит "последнюю" версию m_value, либо засунуть в Thread.MemoryBarrier() или использовать Thread.VolatileRead(ref m_value).Последнее обходится дешевле, чем полный барьер памяти.

В идеале вы могли бы вставить ReadBarrier, но среда CLR, похоже, не поддерживает это напрямую.

Редактировать:Другой способ подумать об этом заключается в том, что на самом деле существует два вида барьеров памяти:барьеры памяти компилятора, которые сообщают компилятору, как упорядочивать операции чтения и записи, и барьеры памяти процессора, которые сообщают процессору, как упорядочивать операции чтения и записи.В Interlocked функции используют барьеры памяти процессора.Даже если компилятор рассматривал их как барьеры памяти компилятора, это все равно не имело бы значения, как в данном конкретном случае, Bar() могло быть скомпилировано отдельно и не было известно о других видах использования m_value для этого потребовался бы барьер памяти компилятора.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top