Почему стандартный шаблон вызова событий C # является потокобезопасным без барьера памяти или аннулирования кэша? А как насчет аналогичного кода?

В C # это стандартный код для вызова события потокобезопасным способом:

var handler = SomethingHappened;
if(handler != null)
    handler(this, e);

Где, потенциально в другом потоке, сгенерированный компилятором метод add используетDelegate.Combine создать новый экземпляр делегата многоадресной рассылки, который он затем устанавливает в поле, сгенерированном компилятором (используя блокировку сравнения-обмена).

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

В своем собственном коде я хочу сделать что-то похожее по следующим направлениям:

var localFoo = this.memberFoo;
if(localFoo != null)
    localFoo.Bar(localFoo.baz);

кудаthis.memberFoo может быть установлен другим потоком. (Это всего лишь одна нить, поэтому я не думаю, что она должна быть заблокирована - но, может быть, здесь есть побочный эффект?)

(И, очевидно, предположим, чтоFoo является "достаточно неизменным", поэтому мы не активно его модифицируем, пока он используется в этом потоке.)

СейчасЯ понимаюочевидный причина того, что это потокобезопасно: чтения из справочных полей являются атомными. Копирование в локальную систему гарантирует, что мы не получим два разных значения. (По-видимому гарантируется только от .NET 2.0, но я предполагаю, что это безопасно в любой разумной реализации .NET?)

Но что я не понимаю: как насчет памяти, занимаемой экземпляром объекта, на который ссылаются? В частности, в отношении когерентности кэша? Если поток «писатель» делает это на одном процессоре:

thing.memberFoo = new Foo(1234);

Что гарантирует, что память, где новыйFoo в кеше ЦП, на котором работает «читатель», не находится ли он с неинициализированными значениями? Что обеспечивает этоlocalFoo.baz (выше) не читает мусор? (И насколько хорошо это гарантировано на разных платформах? На Mono? На ARM?)

А что, если вновь созданный foo происходит из пула?

thing.memberFoo = FooPool.Get().Reset(1234);

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

Когда я спрашиваю об этом, я думаю, что потребуется барьер памяти - не столько, чтобы доступ к памяти не мог быть перемещен, учитывая, что чтение является зависимым, - но и как сигнал для ЦПУ сбросить все недействительные значения кэша.

Мой источник для этогоВикипедиятак что делай из этого что хочешь.

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

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

Скажем, у нас есть ссылка, хранящаяся в полеQ (место в памяти).На процессореA (писатель) мы инициализируем объект в ячейке памятиRи напишите ссылку наR вQНа процессореB (читатель), мы разыскиваем полеQи получить обратно расположение памятиRЗатем на процессореBмы читаем значение изR

Предположим, что GC не работает в любой точке. Больше ничего интересного не происходит.

Вопрос: Что мешаетR от того, чтобы быть вBкеш, издо A изменил его во время инициализации, так что когдаB читает изR он получает устаревшие значения, несмотря на то, что получает свежую версиюQ знать гдеR на первом месте?

(Альтернативная формулировка: что делает модификациюR видимый для процессораB в момент или до того момента, когда изменениеQ виден процессоруB.)

(И относится ли это только к памяти, выделенной сnewили в какую память?) +

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

Ответы на вопрос(5)

Ваш ответ на вопрос