Использует ли частные установщики только в конструкторе сделать объект потокобезопасным?

Я знаю, что могу создать неизменяемый (то есть потокобезопасный) объект, подобный этому:

class CantChangeThis
{
    private readonly int value;

    public CantChangeThis(int value)
    {
        this.value = value;
    }

    public int Value { get { return this.value; } } 
}

Однако я обычно "обманываю" и делаю это:

class CantChangeThis
{
    public CantChangeThis(int value)
    {
        this.Value = value;
    }

    public int Value { get; private set; } 
}

Тогда мне стало интересно: "Почему это работает?" Это действительно потокобезопасно? Если я использую его так:

var instance = new CantChangeThis(5);
ThreadPool.QueueUserWorkItem(() => doStuff(instance));

Тогда то, что он действительно делает (я думаю):

  • Выделение пространства в общей папке для экземпляра
  • Инициализация значения внутри экземпляра в куче
  • Запись указателя/ссылки на это пространство в локальную переменную (конкретный стек)
  • Передача ссылки на этот поток как значение. (Интересно, как я это написал, ссылка находится внутри закрытия, что делает то же самое, что и мой экземпляр, но пусть игнорирует это.)
  • Тема переходит в кучу и считывает данные из экземпляра.

Однако значение этого экземпляра сохраняется в общей памяти. Эти два потока могут иметь несовместимые с кэшем представления этой памяти в куче. Что это значит, что поток threadpool фактически видит построенный экземпляр, а не некоторые данные мусора? Есть ли неявный барьер памяти в конце любой конструкции объекта?

Ответ 1

  • Запись указателя/ссылки на это пространство в локальную переменную (конкретный стек)
  • Инициализация значения внутри экземпляра в куче

Нет... инвертировать их. Он больше похож на:

  • выделена память для объекта
  • конструктор - это (базовые классы), называемые
  • ссылка на память/объект "возвращается" из оператора new/keyword,
  • ссылка "сохраняется" в операторе присваивания var instance (=)

Вы можете проверить это, выбросив исключение в конструктор. Контрольная переменная не будет назначена.

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

В Intel гарантировано:

Процессор x86-x64 не будет переупорядочивать две записи и не будет переупорядочивать два чтения.

Это очень важно:-), и это гарантирует, что этой проблемы не будет. Эта гарантия не является частью .NET или ECMA С#, но на Intel она гарантирована от процессора, а на Itanium (без этой гарантии архитектуры) это было сделано компилятором JIT (см. Ту же ссылку). Похоже, что на ARM это не гарантируется (все та же ссылка). Но я не видел, чтобы кто-то говорил об этом.

в общем случае, в примере дайте, это не важно, потому что:

Почти все операции, связанные с потоками, используют полный барьер памяти (см. Генераторы барьеров памяти). Полный барьер памяти гарантирует, что все операции записи и чтения, которые находятся перед барьером, действительно выполняются перед барьером, и все операции чтения/записи, которые после барьера выполняются после барьера. ThreadPool.QueueUserWorkItem наверняка в определенный момент использует один полный барьер памяти. И начальная нить должна явно начинаться "свежей", поэтому она не может иметь устаревшие данные (и fooobar.com/questions/453449/..., я бы сказал, что можно с уверенностью предположить, что вы может полагаться на неявный барьер.)

Обратите внимание, что процессоры Intel, естественно, когерентны... Вам нужно отключить когерентность кэшей вручную, если вы этого не хотите (см., например, этот вопрос: https://software.intel.com/en-us/forums/topic/278286), поэтому единственными возможными проблемами могут быть переменные, которые "кэшируются" в регистре или чтения, которое ожидается, или запись, которая задерживается (и обе эти "проблемы" - исправлено "с использованием полного барьера памяти"

дополнение

Ваши две части кода эквивалентны. Авто свойства - это просто "скрытое" поле плюс шаблон get/set, который соответственно return hiddenfield; и hiddenfield = value. Поэтому, если возникла проблема с v2 кода, была бы такая же проблема с v1 кода: -)

Ответ 2

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

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