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

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

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

Ответ 1

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

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

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

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

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

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

Ответ 2

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

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

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

Но также и в реализации 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, и теперь все в порядке.

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

Ответ 3

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

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

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

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

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

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

Так как s_Initialized field по существу "пишет один раз", он никогда не вернет ложный положительный результат.

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

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

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

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

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

Ответ 4

Правильный ответ кажется "Да, в основном".

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

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

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

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

Ответ 5

Неисправна функция Initialize. Он должен выглядеть следующим образом:

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

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

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

Ответ 6

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

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

Ответ 7

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

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

Ответ 8

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

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

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

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

Ответ 9

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

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

Ответ 10

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

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

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

Мой плохой.

Ответ 11

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

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

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

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

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

Ответ 12

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

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

Ответ 13

Возможно, Interlocked дает ключ. И в противном случае этот довольно неплохо.

Я бы предположил, что их не атомный.

Ответ 14

Чтобы ваш код всегда работал на слабо упорядоченных архитектурах, вы должны поместить 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: На самом деле, я смешиваю платформы в своих заявлениях. В С# спецификация CLR требует, чтобы записи были глобально видимыми, упорядоченными (при необходимости, используя дорогие инструкции по хранению для каждого магазина). Поэтому вам не нужно иметь этот барьер памяти. Однако, если это C или С++, где нет такой гарантии глобального порядка видимости, и ваша целевая платформа может иметь слабо упорядоченную память и многопоточную, тогда вам нужно будет убедиться, что записи конструкторов глобально видны до того, как вы обновите s_Initialized, который проверяется вне замка.

Ответ 15

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

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

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

Ответ 16

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

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