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