Почему (или нет) установка полей в конструкторе поточно-безопасна?

Скажем, у вас есть простой класс:

class MyClass
{
    private readonly int a;
    private int b;

    public MyClass(int a, int b) { this.a = a; this.b = b; }

    public int A { get { return a; } }
    public int B { get { return b; } }
}

Я мог бы использовать этот класс многопоточным образом:

MyClass value = null;
Task.Run(() => {
    while (true) { value = new MyClass(1, 1); Thread.Sleep(10); }
});
while (true)
{
    MyClass result = value;
    if (result != null && (result.A != 1 || result.B != 1)) { 
        throw new Exception(); 
    }
    Thread.Sleep(10);
}

Мой вопрос: буду ли я когда-либо видеть этот (или другой похожий многопоточный код), вызывать исключение? Я часто вижу ссылку на тот факт, что энергонезависимая запись не может быть немедленно замечена другими потоками. Таким образом, похоже, что это может потерпеть неудачу, потому что запись в поле значения может произойти до записи в и b. Возможно ли это, или есть что-то в модели памяти, которая делает этот (довольно распространенный) шаблон безопасным? Если так, то, что это? Имеет ли значение только для этой цели? Имеет ли значение, если a и b являются типом, который не может быть атомарно написан (например, пользовательская структура)?

Ответ 1

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

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

Это означает, что инициализация конструктора не может быть перемещена после назначения ссылки на класс.

Джо Даффи упомянул об этом в своей статье о том же предмете.

Правило 2: У всех магазинов есть семантика выпуска, т.е. загрузка или хранение не могут перемещаться после одного.

Также статья Vance morrison здесь подтверждает то же самое (раздел Техника 4: ленивая инициализация).

Как и все методы, которые удаляют блокировки чтения, код на рисунке 7 опирается на сильное упорядочение написания. Например, этот код будет неверно в модели памяти ECMA, если myValue не был изменен потому что записи, которые инициализируют экземпляр LazyInitClass, могут быть задерживается до тех пор, пока вы не напишите в myValue, разрешив клиенту GetValue для чтения неинициализированного состояния. В .NET Framework 2.0 модель, код работает без изменчивых деклараций.

Гарантируются записи, начиная с CLR 2.0. Он не указывается в стандарте ECMA, это просто реализация Microsoft для CLR дает эту гарантию. Если вы запустите этот код в CLR 1.0 или любой другой реализации CLR, ваш код, вероятно, сломается.

История за этим изменением: (Из CPOW Страница 516)

Когда CLR 2.0 был перенесен на IA64, его первоначальная разработка была произошел на процессорах X86, и поэтому он был плохо оснащен произвольное переупорядочение хранилища (как разрешено IA64). То же самое было верно большинства кода, написанных для .NET. разработчиками nonMicrosoft таргетинг на Windows

Результат заключался в том, что во время работы IA64, в частности код, связанный с печально известной двойной проверкой который внезапно не работает должным образом. Мы рассмотрим это в контексте шаблона позже в этой главе. Но, если магазины могут передавать другие магазины, рассмотрите это: поток может инициализировать поля частных объектов, а затем опубликовать ссылку на это в общем месте; потому что магазины могут перемещаться, другой поток может видеть ссылку на объект, читать его и но видят поля, пока они все еще не состоят в неинициализированном состоянии. Это не только повлияло на существующий код, но и может нарушить систему типов свойства, такие как поля initonly.

Таким образом, архитекторы CLR приняли решение укрепить 2.0, выпустив все магазины на IA64 в качестве заборных ограждений. Это дало все программы CLR более сильное поведение модели памяти. Это гарантирует, что программистам не нужно должны беспокоиться о тонких условиях гонки, которые будут проявляться только в практика по неясной, редко используемой и дорогостоящей архитектуре.

Примечание. Джо Даффи говорит, что они усиливают 2.0, выпуская все магазины на IA64 в качестве затворов , что не означает, что другие процессоры могут изменить его порядок. Другие процессоры сами по себе обеспечивают гарантию того, что магазин-магазин (магазин, за которым следует магазин) не будет переупорядочен. Поэтому CLR не нужно явно гарантировать это.

Ответ 2

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

Если "значение" было структурой, тогда оно не было бы потокобезопасным, поскольку инициализация значения не была бы атомной.

Ответ 3

Таким образом, похоже, что это может потерпеть неудачу, потому что запись в поле значения может произойти до записи в и b. Возможно ли это,

Да, это, безусловно, возможно.

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

Ответ 4

Я когда-нибудь увижу этот (или другой похожий многопоточный код) исключение?


Да, на ARM (и любом другом оборудовании со слабой моделью памяти) вы будете наблюдать такое поведение.

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

Летучие не о мгновенности наблюдения изменений, о порядках и семантике получения/выпуска.
Более того, ECMA-335 говорит, что это может произойти (и это произойдет на ARM или любом другом оборудовании со слабой моделью памяти).

Имеет ли значение только для этой цели?

readonly не имеет ничего общего с переупорядочением и изменчивостью инструкций.

Было бы важно, чтобы a и b были типом, который не может быть атомарно записан (например, пользовательская структура)?

Атомность полей в этом сценарии не имеет значения. Чтобы предотвратить эту ситуацию, вы должны написать ссылку на созданный объект через Volatile.Write (или просто сделать эту ссылку volatile, а компилятор выполнит задание). Volatile.Write(ref value, new MyClass(1, 1)) выполнит трюк.

Для получения дополнительной информации о летучей семантике и модели памяти см. ECMA-335, раздел I.12.6

Ответ 5

Как написано, этот код является потокобезопасным, потому что value не обновляется, пока конструктор не завершит выполнение. Другими словами, строящийся объект не наблюдается никем.

Вы можете написать код, который поможет вам стрелять в лицо, явно публикуя this для внешнего мира, например

class C { public C( ICObserver observer ) { observer.Observe(this); } }

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

Ответ 6

Это было неправильно, извините...

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

Вы также можете получить ошибку, если цикл while проверяет значения на точный момент, когда новый класс создается и присваивается значению, но перед установкой переменных a и b.

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

Есть коллекции, которые будут обрабатывать это. Вы добавили бы классы в коллекцию в один поток, а также проверили и извлекли их в другой.

См. http://dotnetcodr.com/2014/01/14/thread-safe-collections-in-net-concurrentstack/ для примера.