Может ли переупорядочение памяти вызвать С# для доступа к нераспределенной памяти?

Я понимаю, что С# является безопасным языком и не позволяет получить доступ к нераспределенной памяти, кроме как через unsafe ключевое слово. Однако его модель памяти позволяет переупорядочивать, когда существует несинхронизированный доступ между потоками. Это приводит к опасностям гонки, когда ссылки на новые экземпляры кажутся доступными для гоночных нитей до того, как экземпляры были полностью инициализированы, и является широко известной проблемой для двойной проверки блокировки. Крис Брумме (из команды CLR) объясняет это в своей статье о модели памяти:

Рассмотрим стандартный протокол двойной блокировки:

if (a == null)
{
    lock(obj)
    {
        if (a == null) 
            a = new A();
    }
}

Это обычная техника, позволяющая избежать блокировки чтения "a" в типичном случае. Он отлично работает на X86. Но это будет нарушено юридической, но слабой реализацией спецификации CLI ECMA. Его правда, что, согласно спецификации ECMA, приобретение блокировки приобретает семантику, а релиз блокировки имеет семантику выпуска.

Однако мы должны предположить, что во время строительства "a. Эти магазины могут быть произвольно переупорядочены, в том числе возможность отложить их до публикации издательского магазина, который присваивает новый объект "a". В этот момент перед магазином есть небольшое окно. Исключение подразумевается выходом из замка. Внутри этого окна другие процессоры могут перемещаться по ссылке '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.

Теперь я обращусь к нескольким вашим нецентральным точкам.

Я понимаю, что С# является безопасным языком и не позволяет получить доступ к нераспределенной памяти, кроме как через небезопасное ключевое слово.

Это более или менее корректно. Есть некоторые механизмы, с помощью которых можно получить доступ к фальшивой памяти без использования unsafe - например, через неуправляемый код или злоупотреблять макетом структуры. Но в целом, да, С# является безопасным для памяти.

Однако его модель памяти позволяет переупорядочивать, когда существует несинхронизированный доступ между потоками.

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

Крис Брумме (из команды CLR)...

Постоянные великие статьи Криса - это драгоценные камни и дают представление о первых днях CLR, но я отмечаю, что с 2003 года были некоторые укрепления модели памяти, когда эта статья была написана, особенно в отношении проблемы, которую вы поднять.

Крис прав, что блокировка с двойной проверкой очень опасна. Существует правильный способ сделать двойную проверку блокировки на С#, и в тот момент, когда вы отходите от нее даже немного, вы находитесь в сорняках ужасных ошибок, которые воспроизводятся только на оборудовании модели с слабой памятью.

означает ли это, что другой поток может читать память, которая все еще содержит данные из собранных мусором объектов

Я думаю, ваш вопрос не в том, что касается старой модели памяти 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 наблюдается двумя потоками, то один из этих потоков может наблюдать нарушение инварианта.