Является ли доступ к переменной в C# атомарной операцией?
-
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.)
Правильным ответом будет: «Да, в основном».
- Ответ Джона, ссылающийся на спецификацию CLI, указывает, что доступ к переменным размером не более 32 бит на 32-битном процессоре является атомарным.
Дальнейшее подтверждение из спецификации C#, раздел 5.5, Атомарность ссылок на переменные:
Чтение и запись следующих типов данных являются атомарными:bool, char, byte, sbyte, short, ushort, uint, int, float и ссылочные типы.Кроме того, операции чтения и записи перечисляемых типов с базовым типом из предыдущего списка также являются атомарными.Чтение и запись других типов, включая long, ulong, double и decimal, а также определяемых пользователем типов, не гарантированно являются атомарными.
Код в моем примере был перефразирован из класса 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 и полностью отказаться от использования блокировки.