Почему стандартный шаблон вызова событий 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
или в какую память?) +
Примечание: я отправилответ здесь сам.