Доступ к переменной в C # является атомарной операцией? - PullRequest
63 голосов
/ 13 августа 2008

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

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

Ответы [ 16 ]

35 голосов
/ 13 августа 2008

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

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

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

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

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

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

34 голосов
/ 16 сентября 2008

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

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

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

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

11 голосов
/ 15 августа 2008

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

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

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

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

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

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

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

Экономно читать за пределами замка.

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

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

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

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

7 голосов
/ 13 августа 2008

Правильный ответ: «Да, в основном».

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

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

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

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

2 голосов
/ 13 августа 2008

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

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

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

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

1 голос
/ 15 августа 2008

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

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

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

Мой плохой.

1 голос
/ 13 августа 2008

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

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

1 голос
/ 13 августа 2008

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

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

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

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

1 голос
/ 13 августа 2008

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

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

1 голос
/ 13 августа 2008

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

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

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