В С# это стандартный код для вызова события поточно-безопасным способом:
var handler = SomethingHappened;
if(handler != null)
handler(this, e);
Где, возможно, в другом потоке, метод добавления сгенерированного компилятором использует Delegate.Combine
для создания нового экземпляра делегата многоадресной передачи, который затем устанавливает в поле, генерируемое компилятором (с использованием блокированного обмена с обменом).
(Примечание: для целей этого вопроса нам не нужен код, который выполняется в подписчиках событий. Предположим, что он поточно-безопасный и надежный перед удалением.)
В моем собственном коде я хочу сделать что-то подобное в следующих строках:
var localFoo = this.memberFoo;
if(localFoo != null)
localFoo.Bar(localFoo.baz);
Где this.memberFoo
может быть задан другим потоком. (Это всего лишь один поток, поэтому я не думаю, что его нужно блокировать, но, возможно, здесь есть побочный эффект?)
(И, очевидно, предположим, что Foo
является "неизменным", что мы не активно его модифицируем, пока он используется в этом потоке.)
Теперь Я понимаю очевидную причину, что это поточно-безопасный: чтение из эталонных полей является атомарным. Копирование в локальный файл гарантирует, что мы не получим два разных значения. (Видимо, гарантирован только от .NET 2.0, но я уверен, что он безопасен в любой разумной реализации .NET?)
Но я не понимаю: как насчет памяти, занимаемой экземпляром объекта, на который ссылаются? В частности, в отношении когерентности кеша? Если поток "writer" выполняет это на одном CPU:
thing.memberFoo = new Foo(1234);
Что гарантирует, что память, в которой выделен новый Foo
, не входит в кеш процессора, на котором работает "читатель", с неинициализированными значениями? Что гарантирует, что localFoo.baz
(выше) не читает мусор? (И насколько это гарантировано на разных платформах? В Mono? On ARM?)
А что, если вновь созданный foo происходит из пула?
thing.memberFoo = FooPool.Get().Reset(1234);
Кажется, это не отличается от перспективы памяти новым распределением - но, может быть,.NET-распределитель делает какую-то магию, чтобы сделать первый случай работы?
Мое мышление в том, чтобы просить об этом, заключается в том, что для обеспечения безопасности потребуется защита памяти - не так много, что обращения к памяти не могут перемещаться, учитывая, что чтение зависит, но как сигнал к CPU, чтобы очистить любые недействительные кеширования.
Мой источник для этого Wikipedia, поэтому сделайте так, как хотите.
(Я мог бы предположить, что, возможно, обмен блокировкой-сравнением в потоке писателя делает недействительным кеш на читателе? Или, может быть, все чтения вызывают недействительность? Или разницы указателей вызывают недействительность? Я особенно обеспокоен тем, насколько специфичны для платформы эти вещи звук.)
Обновление:. Чтобы сделать его более явным, вопрос заключается в недействительности кэша CPU и том, что обеспечивает .NET(и как эти гарантии могут зависеть от архитектуры процессора):
- Скажем, у нас есть ссылка, хранящаяся в поле
Q
(ячейка памяти). - В CPU A (автор) мы инициализируем объект в ячейке памяти
R
и записываем ссылку наR
вQ
- В CPU B (читатель) поле разворота
Q
и получить назад ячейку памятиR
- Затем, на CPU B, мы читаем значение из
R
Предположим, что GC не работает в какой-либо точке. Ничего интересного не происходит.
Вопрос: Что не позволяет R
находиться в кеше B, начиная с A изменил его во время инициализации, так что когда B читает с R
, он получает устаревшие значения, несмотря на то, что получает новую версию Q
, чтобы знать, где R
в первую очередь?
(Альтернативная формулировка: что делает модификацию R
видимой для CPU B в точке или до того, что изменение на Q
будет видимым для CPU B.)
(И применяется ли это только к памяти, выделенной с помощью new
, или к любой памяти?) +
Примечание. Я разместил сам ответ здесь.