Может ли переупорядочение памяти привести к тому, что C# получит доступ к нераспределенной памяти?
Насколько я понимаю, C# является безопасным языком и не позволяет получить доступ к нераспределенной памяти, кроме как через unsafe
ключевое слово. Однако его модель памяти позволяет переупорядочивать при несинхронизированном доступе между потоками. Это приводит к гоночным опасностям, когда ссылки на новые экземпляры, по-видимому, доступны гоночным нитям до полной инициализации экземпляров, и является широко известной проблемой для двойной проверки блокировки. Крис Брамм (из команды CLR) объясняет это в своей статье о модели памяти:
Рассмотрим стандартный протокол двойной блокировки:
if (a == null)
{
lock(obj)
{
if (a == null)
a = new A();
}
}
Это обычная техника, позволяющая избежать блокировки чтения "а" в типичном случае. Он отлично работает на X86. Но это будет нарушено легальной, но слабой реализацией спецификации CLI ECMA. Это правда, что, согласно спецификации ECMA, получение блокировки имеет семантику получения, а освобождение блокировки имеет семантику освобождения.
Тем не менее, мы должны предположить, что ряд магазинов имел место во время строительства "а". Эти хранилища могут быть произвольно переупорядочены, включая возможность их задержки до тех пор, пока хранилище публикации не назначит новый объект "a". В этот момент перед магазином есть маленькое окошко. Релиз подразумевается, если оставить замок. Внутри этого окна другие процессоры могут перемещаться по ссылке "а" и видеть частично построенный экземпляр.
Меня всегда смущало, что означает "частично сконструированный экземпляр". Предполагая, что среда выполнения.NET очищает память при выделении, а не при сборке мусора ( обсуждение), означает ли это, что другой поток может читать память, которая по-прежнему содержит данные из объектов, собираемых мусором (например, что происходит в небезопасных языках)?
Рассмотрим следующий конкретный пример:
byte[] buffer = new byte[2];
Parallel.Invoke(
() => buffer = new byte[4],
() => Console.WriteLine(BitConverter.ToString(buffer)));
Выше есть состояние гонки; выход будет либо 00-00
или же 00-00-00-00
, Однако возможно ли, что второй поток читает новую ссылку на buffer
до того, как память массива была инициализирована в 0, и вместо этого выводит какую-то другую произвольную строку?
1 ответ
Давайте не будем хоронить лиду здесь: ответ на ваш вопрос - нет, вы никогда не будете наблюдать заранее выделенное состояние памяти в модели памяти CLR 2.0.
Сейчас я рассмотрю пару ваших нецентральных пунктов.
Насколько я понимаю, C# является безопасным языком и не позволяет получить доступ к нераспределенной памяти, кроме как через ключевое слово unsafe.
Это более или менее правильно. Есть несколько механизмов, с помощью которых можно получить доступ к поддельной памяти, не используя unsafe
- очевидно, с помощью неуправляемого кода или злоупотребления структурой. Но в целом, да, C# безопасен для памяти.
Однако его модель памяти позволяет переупорядочивать при несинхронизированном доступе между потоками.
Опять же, это более или менее правильно. Лучший способ думать об этом - то, что C# позволяет переупорядочивать в любой точке, где переупорядочение было бы невидимым для однопоточной программы, с учетом определенных ограничений. Эти ограничения включают введение семантики получения и выпуска в определенных случаях и сохранение определенных побочных эффектов в определенных критических точках.
Крис Брамм (из команды CLR) ...
Последние великие статьи Криса являются жемчужинами и дают глубокое понимание первых дней CLR, но я отмечаю, что с 2003 года, когда была написана эта статья, были некоторые усовершенствования модели памяти, особенно в том, что касается проблемы, с которой вы столкнулись. поднять.
Крис прав, что двойная проверка блокировки очень опасна. Существует правильный способ сделать двойную проверку блокировки в C#, и в тот момент, когда вы даже слегка отступаете от нее, вы попадаете в кучу ужасных ошибок, которые воспроизводятся только на слабом оборудовании модели памяти.
Означает ли это, что другой поток может читать память, которая по-прежнему содержит данные из объектов, собираемых мусором
Я думаю, что ваш вопрос касается не старой модели слабой памяти ECMA, которую описывал Крис, а скорее о том, какие гарантии на самом деле предоставляются сегодня.
Для повторного заказа невозможно представить предыдущее состояние объектов. Вам гарантировано, что когда вы читаете только что выделенный объект, все его поля равны нулю.
Это стало возможным благодаря тому факту, что все записи имеют семантику выпуска в текущей модели памяти; смотрите это для деталей:
http://joeduffyblog.com/2007/11/10/clr-20-memory-model/
Запись, которая инициализирует память до нуля, не будет перемещена во времени относительно чтения позже.
Меня всегда смущали "частично построенные объекты"
Джо обсуждает это здесь: http://joeduffyblog.com/2010/06/27/on-partiallyconstructed-objects/
Здесь проблема не в том, что мы могли бы видеть состояние предварительного выделения объекта. Скорее проблема в том, что один поток может увидеть объект, пока конструктор все еще работает в другом потоке.
В самом деле, возможно, что конструктор и финализатор будут работать одновременно, что очень странно! Финализаторы трудно написать правильно по этой причине.
Иными словами: CLR гарантирует вам, что его собственные инварианты будут сохранены. Инвариант CLR заключается в том, что вновь выделенная память считается обнуленной, так что инвариант будет сохранен.
Но CLR не в деле сохранения ваших инвариантов! Если у вас есть конструктор, который гарантирует это поле x
является true
если и только если y
не является нулевым, тогда вы несете ответственность за то, чтобы этот инвариант всегда соблюдался. Если каким-то образом this
наблюдается двумя потоками, тогда один из этих потоков может наблюдать нарушение инварианта.