Можно ли наблюдать частично построенный объект из другого потока? - PullRequest
27 голосов
/ 02 декабря 2011

Я часто слышал, что в модели памяти .NET 2.0 при записи всегда используются ограждения для выпуска.Это правда?Означает ли это, что даже без явных барьеров или блокировок памяти невозможно наблюдать частично построенный объект (с учетом только ссылочных типов) в потоке, отличном от того, в котором он создан?Я явно исключаю случаи, когда конструктор пропускает ссылку this.

Например, допустим, у нас был неизменный ссылочный тип:

public class Person
{
    public string Name { get; private set; }
    public int Age { get; private set; }

    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }
}

Можно ли с помощью следующего кода наблюдать любой результат, кроме "John 20" и "Jack 21", скажем, «null 20» или «Jack 0»?

// We could make this volatile to freshen the read, but I don't want
// to complicate the core of the question.
private Person person;

private void Thread1()
{
    while (true)
    {
        var personCopy = person;

        if (personCopy != null)
            Console.WriteLine(personCopy.Name + " " + personCopy.Age);
    }
}

private void Thread2()
{
    var random = new Random();

    while (true)
    {
        person = random.Next(2) == 0
            ? new Person("John", 20)
            : new Person("Jack", 21);
    }
}

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

Ответы [ 3 ]

10 голосов
/ 02 декабря 2011

Я часто слышал, что в модели памяти .NET 2.0 записи всегда используют освободить заборы. Это правда?

Это зависит от того, на какую модель вы ссылаетесь.

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

  • Спецификация ECMA имеет упрощенную модель, в которой записи не предоставляют эту гарантию.
  • Где-то упоминалось, что реализация CLR, предоставленная Microsoft, усиливает модель, делая записи семантикой ограничения выпуска.
  • Архитектуры x86 и x64 усиливают модель, записывая барьеры освобождения и ограждения и читая барьеры захвата и ограждения.

Таким образом, возможно, что другая реализация CLI (например, Mono), работающая на эзотерической архитектуре (например, ARM, на которую теперь будет ориентироваться Windows 8), будет , а не , обеспечивать семантику ограничения выпуска при записи. Обратите внимание, что я сказал, что это возможно, но не уверен. Но между всеми играющими моделями памяти, такими как различные программные и аппаратные уровни, вы должны написать код для самой слабой модели, если вы хотите, чтобы ваш код был действительно переносимым. Это означает кодирование по модели ECMA и не делать никаких предположений.

Мы должны сделать список слоев модели памяти в игре просто явным.

  • Компилятор: C # (или VB.NET или любой другой) может перемещать инструкции.
  • Время выполнения: Очевидно, что время выполнения CLI через JIT-компилятор может перемещать инструкции.
  • Аппаратное обеспечение: И, конечно, архитектура процессора и памяти также играет роль.

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

Да (квалифицировано): Если среда, в которой запущено приложение, достаточно неясна, возможно, что частично построенный экземпляр будет наблюдаться из другого потока. Это одна из причин, по которой двойная проверка шаблона блокировки была бы небезопасной без использования volatile. В действительности, однако, я сомневаюсь, что вы когда-нибудь столкнетесь с этим главным образом потому, что реализация CLI от Microsoft не будет переупорядочивать инструкции таким образом.

Можно ли с помощью следующего кода наблюдать любой вывод кроме «Джон 20» и «Джек 21», скажем «ноль 20» или «Джек 0»?

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

Тем не менее, я должен отметить, что, поскольку person не помечен как volatile, возможно, что вообще ничего не печатается, потому что поток чтения всегда может видеть его как null. В действительности, однако, я уверен, что вызов Console.WriteLine заставит компиляторы C # и JIT избежать операции подъема, которая в противном случае могла бы вывести чтение person за пределы цикла. Я подозреваю, что вы уже хорошо знаете этот нюанс.

Означает ли это, что я могу просто сделать все общие поля глубоко неизменные ссылочные типы volatile и (в большинстве случаев) с моей работой?

Я не знаю. Это довольно загруженный вопрос. Мне неудобно отвечать в любом случае без лучшего понимания контекста. Что я могу сказать, так это то, что я обычно избегаю использования volatile в пользу более явных инструкций памяти, таких как Interlocked операции, Thread.VolatileRead, Thread.VolatileWrite и Thread.MemoryBarrier. Опять же, я также стараюсь вообще избегать кода без блокировки в пользу механизмов синхронизации более высокого уровня, таких как lock.

Обновление:

Один из способов, которым я люблю визуализировать вещи, - это предположить, что компилятор C #, JITer и т. Д. Будут оптимизированы настолько агрессивно, насколько это возможно.Это означает, что Person.ctor может быть кандидатом на встраивание (поскольку оно простое), что приведет к следующему псевдокоду.

Person ref = allocate space for Person
ref.Name = name;
ref.Age = age;
person = instance;
DoSomething(person);

И поскольку записи не имеют семантики ограничения выпуска в спецификации ECMA, тогда как другиеоперации чтения и записи могут «плавать» за присвоением person, приводя к следующей действительной последовательности инструкций.

Person ref = allocate space for Person
person = ref;
person.Name = name;
person.Age = age;
DoSomething(person);

Таким образом, в этом случае вы можете видеть, что person назначается до его инициализации.Это верно, потому что с точки зрения исполняющего потока логическая последовательность остается согласованной с физической последовательностью.Там нет непреднамеренных побочных эффектов.Но по причинам, которые должны быть очевидны, эта последовательность была бы катастрофической для другого потока.

1 голос
/ 03 декабря 2011

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

Что касается уровня дрожания, я не уверен, хотя меня удивило бы, если бы он переупорядочил назначение поля и вызов метода (именно таков конструктор). Будет ли компилятор действительно смотреть на метод и все его возможные пути выполнения, чтобы убедиться, что поле никогда не используется вызываемым методом?

Аналогично на уровне ЦП, я был бы удивлен, если бы переупорядочение происходило вокруг инструкции перехода, поскольку ЦП не может знать, является ли ветвь «подпрограммным вызовом», и поэтому вернется к следующей инструкции. Выполнение не по порядку допускает крайне некорректное поведение в случае «нестандартных» прыжков.

1 голос
/ 03 декабря 2011

У тебя нет надежды. Замените вашу консольную запись проверкой ошибок, установите дюжину копий Thread1 (), используйте машину с 4 ядрами, и вы обязательно найдете несколько частично созданных экземпляров Person. Используйте гарантированные методы, упомянутые в других ответах и ​​комментариях, чтобы обеспечить безопасность вашей программы.

И люди, которые пишут компиляторы, и люди, которые создают процессоры, в своих стремлениях к большей скорости сговариваются, чтобы ухудшить ситуацию. Без явных инструкций, чтобы сделать иначе, люди компилятора переупорядочат ваш код любым возможным способом, чтобы сэкономить наносекунду. Процессор люди делают то же самое. Последнее, что я прочитал, одно ядро, как правило, выполняет 4 инструкции одновременно. (А может даже если не сможет.)

В нормальных условиях у вас редко будут проблемы с этим. Однако я обнаружил, что незначительная проблема, которая появляется только раз в 6 месяцев, может быть действительно серьезной. И, что интересно, проблема «один раз в миллиард» может возникать несколько раз в минуту, что гораздо предпочтительнее. Я предполагаю, что ваш код попадет в последнюю категорию.

...