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

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

Однако я просматривал System.Web.Security.Membership с помощью Reflector и нашел такой код:

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

Ответов (15)

Решение

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

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

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

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

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

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

Чтобы ваш код всегда работал на слабо упорядоченных архитектурах, вы должны установить 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 на слабо упорядоченном процессоре.

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

РЕДАКТИРОВАТЬ: На самом деле я смешиваю платформы в своих заявлениях. В C# спецификация CLR требует, чтобы операции записи были глобально видимыми и упорядоченными (при необходимости с использованием дорогостоящих инструкций для каждого хранилища). Следовательно, вам не обязательно иметь этот барьер памяти. Однако, если бы это был C или C++, где такой гарантии глобального порядка видимости не существует, и ваша целевая платформа может иметь слабо упорядоченную память и многопоточность, вам нужно будет убедиться, что записи конструкторов видны глобально, прежде чем обновлять s_Initialized. , который проверяется вне замка.

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

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

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

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

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

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

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

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

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

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

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

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

В приведенном выше примере процедура инициализации ошибочна, так как это может привести к многократной инициализации. Вам нужно будет проверить 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;
    }
}

Возможно, Interlocked подскажет. А в остальном этот у меня неплохой.

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

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

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

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

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

Неа. И это не относится к C# и даже не к .NET, а к процессору.

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

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

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

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

Кажется, что правильный ответ: «Да, в основном».

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

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

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

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

Погодите - вопрос, который находится в заголовке, определенно не тот вопрос, который задает Рори.

Главный вопрос имеет простой ответ «Нет» - но это совсем не помогает, когда вы видите настоящий вопрос - я не думаю, что кто-то дал простой ответ.

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

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

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

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

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

Экономно читать вне замка.

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

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

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

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

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

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

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

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

Например:

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, а также инициализации Provider не разрешено перемещаться после того, как s_Initialized имеет значение true, и теперь все в порядке.

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