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

13.08.2008 11:41:29
Для тех из вас, кто читает этот пост в том виде , в каком он существует в настоящее время , это немного сбивает с толку. Приведенный выше код был изменен для отображения правильного способа двойной проверки с помощью замка (внутри и снаружи). Однако текст вопроса относится к исходному образцу кода, который не включал в себя вторую s_initializedпроверку. Это немного сбивает с толку, не глядя на историю редактирования.
GEEF 31.10.2016 19:14:23
@GEEF Извините за путаницу, но вопрос относится к текущему коду, включая вторую проверку внутри lock. Речь идет не о том, как выполнить двойную проверку блокировки, а о атомарном доступе к памяти и о том, можно ли записывать логическое значение одновременно с чтением.
Rory MacLeod 22.11.2016 17:47:18
16 ОТВЕТОВ
РЕШЕНИЕ

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

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

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

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

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

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

36
7.01.2016 09:57:33
Хотя доступ к области памяти, не превышающей собственный размер слова, является атомарным, пример кода, приведенный в вопросе, не является потокобезопасным из-за переупорядочения чтения / записи. Смотрите мой ответ для более подробной информации.
Thomas Danecker 16.09.2008 22:02:30
Хорошее место, Томас. Каждый должен прочитать, обязательно прочитайте и ваш ответ.
John Richardson 24.09.2008 06:12:33
C # 4 спец 5.5 Atomicity of variable references Reads and writes of the following data types are atomic: bool, char, byte, sbyte, short, ushort, uint, int, float, and reference types. In addition, reads and writes of enum types with an underlying type in the previous list are also atomic. Reads and writes of other types, including long, ulong, double, and decimal, as well as user-defined types, are not guaranteed to be atomic. Aside from the library functions designed for that purpose, there is no guarantee of atomic read-modify-write, such as in the case of increment or decrement.
abatishchev 28.02.2013 01:04:45

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

0
13.08.2008 12:00:51
Комментарий «Выполнить инициализацию» заменяет все параметры чтения-чтения, создания экземпляров класса и настройки параметров, которые Membership делает для инициализации s_Provider, поэтому я понимаю, почему это в «замке» - так что это делается только один раз.
Rory MacLeod 16.09.2012 00:09:46

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

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

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

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

2
7.09.2014 20:07:38
Это не лучше, вы можете также удалить оператор за пределами блокировки. Кроме того, если единственное, что здесь делается, это установить для initialized значение true, тогда исходная «небезопасная» версия. было достаточно хорошо В худшем случае вы устанавливаете его дважды, единственная причина раннего возврата - производительность (не правильность).
Wedge 12.09.2008 19:48:49
Я сказал, что первая проверка на производительность. Замки очень дороги, так что всегда стоит делать. Во-вторых, я думаю, что разумно предположить, что более сложный код был опущен. Я почему-то сомневаюсь, что MS пошла бы на счет блокировки без необходимости.
John Richardson 13.09.2008 08:23:43
Ты прав. Реальный класс Членства имеет эту вторую проверку. Я оставил это, потому что меня действительно интересует, является ли первый вызов s_Initialized вне блокировки потокобезопасным.
Rory MacLeod 16.09.2012 00:13:07

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

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

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

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

0
13.08.2008 12:19:22

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

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

1
7.09.2014 20:08:17

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

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

1
13.08.2008 12:35:12

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

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

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

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

1
15.08.2008 12:46:02

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

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

-1
13.08.2008 15:21:04
Делать s_Initialized volatile не поможет. В частности, это не помешает двум процессорам прочитать одно и то же значение и ошибочно приступить к созданию объекта. Может показаться , что его нестабильность исправляет проблемы, поскольку доступ к изменчивым переменным может полностью сериализовать обращения к памяти, периодически оставляя память в несогласованном состоянии, но при этом скрывая ошибки достаточно хорошо, чтобы пройти тесты, но потерпеть неудачу в условиях реальной нагрузки.
doug65536 4.01.2013 19:52:58

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

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

1
7.09.2014 20:07:47

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

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

7
22.09.2008 19:32:47
Я понимаю вашу точку зрения, но код в моем примере точен в отношении того, как осуществляется блокировка в реальном классе Membership. Возможно, команда ASP.NET знает достаточно о своем компиляторе и целевом процессоре, чтобы быть уверенным, что переупорядочения не произойдет. Все то же самое ...
Rory MacLeod 22.09.2008 19:18:10
... Я согласен с Джо Даффи в статье, на которую вы ссылаетесь, когда он говорит, что мы всегда должны использовать «изменчивый» в таких ситуациях, чтобы быть в безопасности.
Rory MacLeod 22.09.2008 19:20:46
Если int является атомарным, почему Interlocked.Increment () / Decrement () принимает типы int?
core 5.10.2008 21:15:21
потому что Interlocked.Increment выполняет загрузку, приращение и сохранение в одной атомарной операции.
Thomas Danecker 8.01.2009 22:49:40

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

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

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

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

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

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

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

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

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

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

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

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

11
15.08.2008 14:17:07

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

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

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

Виноват.

1
15.08.2008 13:37:48

Это (плохая) форма шаблона блокировки двойной проверки, которая не является поточно-ориентированной в 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, и теперь все в порядке.

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

34
22.11.2019 12:02:41
Хорошая точка зрения. Читая статью, на которую вы ссылались, является ли код в Membership правильным или неправильным, кажется, зависит от точных деталей компилятора и процессора, но я согласен, что s_Initialized должен быть непостоянным, чтобы быть уверенным.
Rory MacLeod 22.09.2008 19:25:10
Это является неправильным в соответствии с .net своей памяти Modell. На некоторых процессорах и / или с определенными компиляторами вам может повезти, если он все еще работает. Джо Даффи написал еще одну статью: bluebytesoftware.com/blog/2008/07/17/…
Thomas Danecker 23.09.2008 14:30:52
Отсутствие переупорядочения записи характерно для x86 и x64, но не обязательно для Microsoft .NET, то есть я бы не стал полагаться на это, если бы Microsoft добавила поддержку других архитектур, где аппаратное переупорядочение записывает.
Jon Harrop 26.05.2009 01:35:25
Ну @ Томас Я не думаю, что это проблема здесь, потому что lockоператор уже сделал то, что вы блокируете с помощью volatile: оператор блокировки создает полный забор памяти, и это даже больше, чем то, что volatileделает, volatileсоздает полтора забор «приобретение и выпуск семантика» , а lockсоздает полный забор, поэтому из-за MemoryBarrierсозданный lockв s_Provider will be fresh here; to understand it more you could read the above code like: Thread.MemoryBarrier (); .... s_Initialized = true; Thread.MemoryBarrier (); return s_Provider; `
Jalal Said 26.05.2012 00:29:43
@ JalalAldeenSaa'd Вы правы насчет барьера памяти, но в этом случае только гарантия s_Initializedимеет правильное значение в Providerсвойстве. Это было бы иначе, если бы s_Initializedпросто не было там, и только s_Providerбыло бы проверено - но, к сожалению, это не так здесь. Еще один способ исправить это положить Thread.MemoryBarrier()перед s_Initialized = true;. Обратите внимание, что атомарность в этом случае не имеет значения - речь идет о переупорядочении инструкций.
atlaste 27.07.2015 08:25:56

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

0
2.09.2012 10:13:16

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

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

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

0
28.02.2013 01:35:36