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

StackOverflow https://stackoverflow.com/questions/9666

  •  08-06-2019
  •  | 
  •  

Вопрос

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

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

Это было полезно?

Решение

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

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

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

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

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

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

Другие советы

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

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

s_Initialized не является энергозависимым.Это означает, что записи в коде инициализации могут перемещаться после того, как для s_Initialized установлено значение true, и другие потоки могут видеть неинициализированный код, даже если для них s_Initialized имеет значение true.Это не относится к реализации Microsoft 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, и теперь все в порядке.

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

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

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

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

Почему S_Initialized поле читается за пределами замка?

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

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

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

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

Это бюджетный деятельность с высокий шанс получить выгоду.

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

Если бы блокировки были дешевыми, код был бы проще и первая проверка была бы опущена.

(редактировать:Далее следует приятный ответ от Рори.Да, логические операции чтения очень атомарны.Если бы кто-то создал процессор с неатомарными логическими операциями чтения, он был бы представлен на 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 правильный.Теперь мы знаем, почему.

Редактировать:Как указывает Томас Данекер, хотя доступ к полю является атомарным, 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, а в процессоре.

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

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

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

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

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

Виноват.

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

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

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

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

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

Ан 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;
    }
}

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top