Доступ к переменной в C# атомарная операция?


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

однако, я смотрел через систему.Сеть.Безопасность.Членство с помощью рефлектора и нашел такой код:

public static class Membership
{
    private static bool s_Initialized = false;
    private static object s_lock = new object();
    private static MembershipProvider s_Provider;

    public static MembershipProvider Provider
    {
        get
        {
            Initialize();
            return s_Provider;
        }
    }

    private static void Initialize()
    {
        if (s_Initialized)
            return;

        lock(s_lock)
        {
            if (s_Initialized)
                return;

            // Perform initialization...
            s_Initialized = true;
        }
    }
}

почему s_initialized поле считывается вне блокировки? Не могу другой поток пытается написать к нему в то же время? являются ли чтения и записи переменных атомарными?

16 63

16 ответов:

для окончательного ответа перейдите к спецификации. :)

раздел I, раздел 12.6.6 спецификации CLI гласит: "соответствующий CLI должен гарантировать, что доступ для чтения и записи к правильно выровненным ячейкам памяти, не превышающим размер собственного слова, является атомарным, когда все доступы для записи к местоположению имеют одинаковый размер."

таким образом, это подтверждает, что s_Initialized никогда не будет нестабильным, и что чтение и запись в типы primitve меньше 32 бит атомный.

в частности, double и long (Int64 и UInt64) составляют не гарантированно быть атомарным на 32-разрядной платформе. Вы можете использовать методы на Interlocked класса для защиты этих.

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

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

Это (плохая) форма шаблона блокировки двойной проверки, который не потокобезопасность в C#!

есть одна большая проблема в этом коде:

s_Initialized не является изменчивым. Это означает, что записи в коде инициализации могут перемещаться после того, как s_Initialized имеет значение true, а другие потоки могут видеть неинициализированный код, даже если для них s_initialized имеет значение true. Это не относится к реализации Платформы Microsoft, потому что каждая запись является изменчивой писать.

но также в реализации Microsoft чтение неинициализированных данных может быть переупорядочено (т. е. предварительно настроено процессором), поэтому, если s_initialized истинно, чтение данных, которые должны быть инициализированы, может привести к чтению старых, неинициализированных данных из-за попадания в кэш (т. е. чтения переупорядочены).

например:

Thread 1 reads s_Provider (which is null)  
Thread 2 initializes the data  
Thread 2 sets s\_Initialized to true  
Thread 1 reads s\_Initialized (which is true now)  
Thread 1 uses the previously read Provider and gets a NullReferenceException

перемещение чтения s_Provider перед чтением s_Initialized совершенно законно, потому что нет волатильного чтения везде.

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

Джо Даффи также написал статью об этой проблеме: сломанные варианты при двойной проверке блокировки

Hang about -- вопрос, который находится в названии, определенно не является реальным вопросом, который задает Рори.

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

Почему читается поле s_Initialized снаружи замок?

ответ на это также прост, хотя и совершенно не связан с атомарностью переменного доступа.

s_initialized поле считывается вне блокировки, потому что замки стоят дорого.

Так как s_initialized поле по существу "написать один раз" он никогда не вернет ложное срабатывание.

это экономично, чтобы прочитать ее за пределами замка.

Это низкой стоимости деятельностью с высокий шанс иметь благо.

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

Если бы замки были дешевыми, код был бы проще, и опустите эту первую проверку.

(edit:хороший ответ от Рори следует. Да, логические чтения очень атомарны. Если бы кто-то построил процессор с неатомными булевыми считываниями, они были бы представлены в DailyWTF.)

правильный ответ: "Да, в основном."

  1. ответ Джона, ссылающийся на спецификацию CLI, указывает, что доступ к переменным не более 32 бит на 32-разрядном процессоре является атомарным.
  2. дальнейшее подтверждение из спецификации C#, раздел 5.5,атомарность ссылок на переменные:

    чтение и запись следующих типов данных являются атомарными: bool, char, byte, sbyte, short, ushort, uint, int, float и ссылочный тип. Кроме того, чтение и запись типов перечислений с базовым типом в предыдущем списке также являются атомарными. Операции чтения и записи других типов, включая long, ulong, double и decimal, а также пользовательские типы, не являются атомарными.

  3. код в моем примере был перефразирован из класса Membership, как написано ASP.NET команда сама, поэтому всегда можно было с уверенностью предположить, что способ доступа к полю s_Initialized правильно. Теперь мы знаем, почему.

Edit: как указывает Томас Данекер, хотя доступ к полю является атомарным, s_Initialized действительно должен быть отмечен летучие чтобы убедиться, что замок не сломан процессор реорганизации читает и пишет.

функция инициализации неисправна. Это должно выглядеть примерно так:

private static void Initialize()
{
    if(s_initialized)
        return;

    lock(s_lock)
    {
        if(s_Initialized)
            return;
        s_Initialized = true;
    }
}

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

чтение и запись переменных, не являются атомарными. Вам нужно использовать API синхронизации для эмуляции атомарных операций чтения / записи.

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

" является ли доступ к переменной в C# атомарной операцией?"

Неа. И это не вещь C#, и даже не вещь .net, это процессорная вещь.

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

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

@Leon
Я понимаю вашу точку зрения-то, как я спросил, а затем прокомментировал, вопрос позволяет его воспринимать по-разному.

чтобы быть ясным, я хотел знать, безопасно ли иметь параллельные потоки для чтения и записи в логическое поле без какого-либо явного кода синхронизации, т. е. обращается к булевой (или другой примитивной) переменной atomic.

затем я использовал код членства, чтобы дать конкретный пример, но это ввело кучу отвлекающие факторы, такие как блокировка двойной проверки, тот факт, что s_Initialized устанавливается только один раз, и что я прокомментировал сам код инициализации.

мой плохой.

вы также можете украсить s_Initialized ключевым словом volatile и полностью отказаться от использования блокировки.

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

Я думаю, что вы спрашиваете, если s_Initialized может находиться в нестабильном состоянии при чтении вне замка. Короткий ответ-нет. Простое назначение / чтение будет сводиться к одной инструкции сборки, которая является атомной на каждом процессоре, о котором я могу думать.

Я не уверен, что имеет место для присвоения 64-битных переменных, это зависит от процессора, я бы предположил, что это не атомарный, но это, вероятно, на современных 32-битных процессорах и, конечно, на всех 64-битных процессорах. Назначение сложные типы значений не будут атомарными.

Я думал, что они были - я не уверен в точке блокировки в вашем примере, если вы не делаете что - то с s_Provider одновременно-тогда блокировка гарантирует, что эти вызовы произошли вместе.

это //Perform initialization комментарий обложки создания s_Provider? Например,

private static void Initialize()
{
    if (s_Initialized)
        return;

    lock(s_lock)
    {
        s_Provider = new MembershipProvider ( ... )
        s_Initialized = true;
    }
}

в противном случае это статическое свойство-get просто вернет null в любом случае.

возможно сблокированного дает подсказку. А иначе этот Я довольно хорошо.

Я бы не догадался, что их не атомные.

чтобы ваш код всегда работал на слабо упорядоченных архитектурах, вы должны поместить MemoryBarrier перед написанием s_Initialized.

s_Provider = new MemershipProvider;

// MUST PUT BARRIER HERE to make sure the memory writes from the assignment
// and the constructor have been wriitten to memory
// BEFORE the write to s_Initialized!
Thread.MemoryBarrier();

// Now that we've guaranteed that the writes above
// will be globally first, set the flag
s_Initialized = true;

записи в память, которые происходят в конструкторе MembershipProvider, и запись в s_Provider не гарантируется до записи в s_Initialized на слабо упорядоченном процессоре.

много мыслей в этой теме о том, является ли что-то атомарным или нет. Дело не в этом. Проблема в том порядок что записи вашего потока видны другим потокам. На слабо упорядоченных архитектурах запись в память происходит не по порядку, и это реальная проблема, а не соответствует ли переменная в шине данных.

EDIT: на самом деле, я смешиваю платформы в своих заявлениях. В C# спецификация CLR требует, чтобы записи были глобально видны, в порядке (при необходимости используя дорогостоящие инструкции для каждого магазина). Поэтому вам не нужно на самом деле иметь эту память барьер есть. Однако если бы это был C или C++, где нет такой гарантии глобального порядка видимости, и ваша целевая платформа может иметь слабо упорядоченную память, и она многопоточна, то вам нужно было бы убедиться, что записи конструкторов глобально видны, прежде чем вы обновите s_Initialized, который тестируется вне блокировки.

An If (itisso) { проверка на логическое является атомарным, но даже если это не было нет необходимости блокировать первую проверку.

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

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

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

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

private static void Initialize()
{
    if (s_Initialized)
        return;

    lock(s_lock)
    {
        if (s_Initialized)
            return;
        s_Provider = new MembershipProvider ( ... )
        s_Initialized = true;
    }
}

Ack, неважно... как уже отмечалось, это действительно неверно. Это не мешает второму потоку войти в раздел "инициализация" кода. Ба.

вы также можете украсить s_Initialized с помощью ключевого слова volatile и полностью отказаться от использования блокировки.