Доступ к переменной в C# атомарная операция?
Я был поднят, чтобы полагать, что если несколько потоков могут получить доступ к переменной, то все чтения и записи в эту переменную должны быть защищены кодом синхронизации, таким как оператор "lock", потому что процессор может переключиться на другой поток на полпути через запись.
однако, я смотрел через систему.Сеть.Безопасность.Членство с помощью рефлектора и нашел такой код:
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 ответов:
для окончательного ответа перейдите к спецификации. :)
раздел I, раздел 12.6.6 спецификации CLI гласит: "соответствующий CLI должен гарантировать, что доступ для чтения и записи к правильно выровненным ячейкам памяти, не превышающим размер собственного слова, является атомарным, когда все доступы для записи к местоположению имеют одинаковый размер."
таким образом, это подтверждает, что s_Initialized никогда не будет нестабильным, и что чтение и запись в типы primitve меньше 32 бит атомный.
в частности,
double
иlong
(Int64
иUInt64
) составляют не гарантированно быть атомарным на 32-разрядной платформе. Вы можете использовать методы наInterlocked
класса для защиты этих.кроме того, в то время как чтение и запись являются атомарными, существует условие расы с добавлением, вычитанием и увеличением и уменьшением примитивных типов, поскольку они должны быть прочитаны, обработаны и переписаны. Блокируемый класс позволяет защитить их используя
CompareExchange
иIncrement
методы.блокировка создает барьер памяти, чтобы процессор не переупорядочивал операции чтения и записи. Блокировка создает единственный необходимый барьер в этом примере.
Это (плохая) форма шаблона блокировки двойной проверки, который не потокобезопасность в C#!
есть одна большая проблема в этом коде:
s_Initialized не является изменчивым. Это означает, что записи в коде инициализации могут перемещаться после того, как s_Initialized имеет значение true, а другие потоки могут видеть неинициализированный код, даже если для них s_initialized имеет значение true. Это не относится к реализации Платформы Microsoft, потому что каждая запись является изменчивой писать.
но также в реализации 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, и теперь все в порядке.
Джо Даффи также написал статью об этой проблеме: сломанные варианты при двойной проверке блокировки
Hang about -- вопрос, который находится в названии, определенно не является реальным вопросом, который задает Рори.
реальный вопрос, который задает Рори, представлен гораздо позже и более уместен для примера, который он дает.
Почему читается поле s_Initialized снаружи замок?
ответ на это также прост, хотя и совершенно не связан с атомарностью переменного доступа.
s_initialized поле считывается вне блокировки, потому что замки стоят дорого.
Так как s_initialized поле по существу "написать один раз" он никогда не вернет ложное срабатывание.
это экономично, чтобы прочитать ее за пределами замка.
Это низкой стоимости деятельностью с высокий шанс иметь благо.
вот почему он читается за пределами замка - чтобы избежать оплаты стоимости использования замка, если он не указан.
Если бы замки были дешевыми, код был бы проще, и опустите эту первую проверку.
(edit:хороший ответ от Рори следует. Да, логические чтения очень атомарны. Если бы кто-то построил процессор с неатомными булевыми считываниями, они были бы представлены в 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 правильно. Теперь мы знаем, почему.
Edit: как указывает Томас Данекер, хотя доступ к полю является атомарным, 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, это процессорная вещь.
ОЖ на месте, что Джо Даффи является человек для такого рода информация. И "блокировка" - это отличный поисковый термин, который можно использовать, если вы хотите узнать больше.
"рваные чтения" могут возникать на любом значении, поля которого складываются больше, чем размер указателя.
@Leon
Я понимаю вашу точку зрения-то, как я спросил, а затем прокомментировал, вопрос позволяет его воспринимать по-разному.чтобы быть ясным, я хотел знать, безопасно ли иметь параллельные потоки для чтения и записи в логическое поле без какого-либо явного кода синхронизации, т. е. обращается к булевой (или другой примитивной) переменной atomic.
затем я использовал код членства, чтобы дать конкретный пример, но это ввело кучу отвлекающие факторы, такие как блокировка двойной проверки, тот факт, что s_Initialized устанавливается только один раз, и что я прокомментировал сам код инициализации.
мой плохой.
вы также можете украсить s_Initialized ключевым словом volatile и полностью отказаться от использования блокировки.
Это неверно. Вы все равно столкнетесь с проблемой второго потока, проходящего проверку, прежде чем первый поток имел возможность установить флаг, который приведет к многократному выполнению кода инициализации.
Я думаю, что вы спрашиваете, если
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 на слабо упорядоченном процессоре.
много мыслей в этой теме о том, является ли что-то атомарным или нет. Дело не в этом. Проблема в том порядок что записи вашего потока видны другим потокам. На слабо упорядоченных архитектурах запись в память происходит не по порядку, и это реальная проблема, а не соответствует ли переменная в шине данных.
EDIT: на самом деле, я смешиваю платформы в своих заявлениях. В C# спецификация CLR требует, чтобы записи были глобально видны, в порядке (при необходимости используя дорогостоящие инструкции для каждого магазина). Поэтому вам не нужно на самом деле иметь эту память барьер есть. Однако если бы это был C или C++, где нет такой гарантии глобального порядка видимости, и ваша целевая платформа может иметь слабо упорядоченную память, и она многопоточна, то вам нужно было бы убедиться, что записи конструкторов глобально видны, прежде чем вы обновите s_Initialized, который тестируется вне блокировки.
An
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; } }