Я был убежден, что если несколько потоков могут обращаться к переменной, то все операции чтения и записи в эту переменную должны быть защищены кодом синхронизации, например, оператором «блокировки», поскольку процессор может переключиться на другой поток в середине процесса. написать.
Тем не менее, я просматривал 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 гласит: «Соответствующий интерфейс командной строки должен гарантировать, что доступ для чтения и записи к правильно выровненным ячейкам памяти, не превышающим размер собственного слова, является атомарным, когда все обращения к записи в расположении имеют одинаковый размер «.
Это подтверждает, что s_Initialized никогда не будет нестабильным, и что чтение и запись в типы примитивов, меньшие 32 бита, являются атомарными.
В частности, double
и long
( Int64
и UInt64
) не гарантируется атомарность на 32-битной платформе. Вы можете использовать методы Interlocked
класса для их защиты.
Кроме того, хотя чтение и запись являются атомарными, существует условие состязания с сложением, вычитанием, увеличением и уменьшением примитивных типов, поскольку они должны быть прочитаны, обработаны и переписаны. Сблокированы класс позволяет защитить их , используя CompareExchange
и Increment
методы.
Блокировка создает барьер памяти для предотвращения переупорядочивания процессором операций чтения и записи. Блокировка создает единственный необходимый барьер в этом примере.
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
. Я думал, что они - я не уверен в смысле блокировки в вашем примере, если вы не делаете что-то одновременно с 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.
Функция Initialize неисправна. Это должно выглядеть больше так:
private static void Initialize()
{
if(s_initialized)
return;
lock(s_lock)
{
if(s_Initialized)
return;
s_Initialized = true;
}
}
Без второй проверки внутри блокировки возможно, что код инициализации будет выполнен дважды. Таким образом, первая проверка предназначена для того, чтобы сэкономить ненужную блокировку, а вторая - для случая, когда поток выполняет код инициализации, но еще не установил s_Initialized
флаг, и поэтому второй поток пройдет первую проверку и ждать в замке.
То, что вы спрашиваете, является ли доступ к полю в методе, многократно атомарном - на что ответ нет.
В приведенном выше примере подпрограмма инициализации является неисправной, поскольку она может привести к многократной инициализации. Вам нужно будет проверить 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;
}
}
Возможно, Interlocked дает подсказку. А в остальном этот мне довольно хорош.
Я бы догадался, что они не атомные.
Я думаю, вы спрашиваете, s_Initialized
может ли быть в нестабильном состоянии при чтении за пределами блокировки. Короткий ответ: нет. Простое присваивание / чтение будет сводиться к одной инструкции по сборке, которая является атомарной на каждом процессоре, о котором я могу думать.
Я не уверен, как обстоят дела с присвоением 64-битных переменных, это зависит от процессора, я бы предположил, что он не атомарный, но, вероятно, на современных 32-битных процессорах и, конечно, на всех 64-битных процессорах. Присвоение сложных типов значений не будет атомарным.
Чтение и запись переменных не являются атомарными. Вам необходимо использовать API синхронизации для эмуляции атомарного чтения / записи.
Чтобы получить отличную ссылку на этот и многие другие вопросы, связанные с параллелизмом, убедитесь, что вы взяли копию последнего спектакля Джо Даффи . Это потрошитель!
"Является ли доступ к переменной в C # атомарной операцией?"
Неа. И это не вещь C #, и даже не вещь .net, это вещь процессора.
О.Дж. говорит о том, что Джо Даффи - парень, к которому можно обратиться за такой информацией. ANd "interlocked" - это отличный термин для поиска, если вы хотите узнать больше.
«Torn reads» может встречаться для любого значения, чьи поля в сумме превышают размер указателя.
Ack, неважно ... как указано, это действительно неправильно. Это не мешает второму потоку войти в раздел кода «initialize». Ба.
Вы также можете украсить s_Initialized ключевым словом volatile и полностью отказаться от использования блокировки.
Вы также можете украсить s_Initialized ключевым словом volatile и полностью отказаться от использования блокировки.
Это не правильно. Вы все еще столкнетесь с проблемой, когда второй поток пройдет проверку, прежде чем первый поток сможет установить флаг, что приведет к многократному выполнению кода инициализации.
Правильный ответ: «Да, в основном».
- Ответ Джона, ссылающийся на спецификацию CLI, указывает на то, что доступ к переменным не более 32 бит на 32-битном процессоре является атомарным.
Дальнейшее подтверждение из спецификации C #, раздел 5.5, Атомность ссылок на переменные :
Чтение и запись следующих типов данных являются атомарными: типы bool, char, byte, sbyte, short, ushort, uint, int, float и reference. Кроме того, чтение и запись перечислимых типов с базовым типом в предыдущем списке также являются атомарными. Чтение и запись других типов, включая long, ulong, double и decimal, а также определяемые пользователем типы, не гарантированно являются атомарными.
Код в моем примере был перефразирован из класса Membership, как написано самой командой ASP.NET, поэтому всегда можно было предположить, что способ доступа к полю s_Initialized является правильным. Теперь мы знаем почему.
Редактирование: Как указывает Томас Данекер, хотя доступ к полю является атомарным, s_Initialized должен быть действительно помечен как энергозависимый, чтобы убедиться, что блокировка не нарушена процессором, переупорядочивающим операции чтения и записи.
Подожди - вопрос в названии определенно не является реальным вопросом, который задает Рори.
Титульный вопрос имеет простой ответ «Нет» - но это совсем не поможет, когда вы видите реальный вопрос - на который, я думаю, никто не дал простого ответа.
Реальный вопрос, который задает Рори, изложен гораздо позже и более соответствует его примеру.
Почему поле s_Initialized читается вне блокировки?
Ответ на этот вопрос также прост, хотя и совершенно не связан с атомарностью доступа к переменным.
Поле s_Initialized читается вне блокировки, потому что блокировки дороги .
Поскольку поле s_Initialized по сути является «однократной записью», оно никогда не вернет ложное срабатывание.
Это экономно читать за пределами замка.
Это низкозатратная деятельность с высокой вероятностью получения выгоды.
Вот почему он читается вне блокировки - чтобы избежать оплаты за использование блокировки, если это не указано.
Если бы блокировки были дешевыми, код был бы проще, и эту первую проверку пропустите.
(правка: следует хороший ответ от Рори. Да, логические чтения очень атомарны. Если кто-то построит процессор с неатомарными логическими чтениями, они будут представлены в DailyWTF.)
@ Леон,
я понимаю твою точку зрения - то, как я спросил, а затем прокомментировал, вопрос позволяет ответить на него несколькими способами.
Чтобы было ясно, я хотел знать, было ли безопасно иметь параллельные потоки, читающие и записывающие в логическое поле без какого-либо явного кода синхронизации, т. Е. Обращающиеся к логической (или другой примитивной) переменной atomic.
Затем я использовал код Membership, чтобы привести конкретный пример, но он привел к множеству отвлекающих факторов, таких как блокировка двойной проверки, тот факт, что s_Initialized устанавливается только один раз, и что я закомментировал сам код инициализации.
Виноват.
Это (плохая) форма шаблона блокировки двойной проверки, которая не является поточно-ориентированной в 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, и теперь все в порядке.
Джо Даффи также написал статью об этой проблеме: сломанные варианты по двойной проверке блокировки
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; `s_Initialized
имеет правильное значение в Provider
свойстве. Это было бы иначе, если бы s_Initialized
просто не было там, и только s_Provider
было бы проверено - но, к сожалению, это не так здесь. Еще один способ исправить это положить Thread.MemoryBarrier()
перед s_Initialized = true;
. Обратите внимание, что атомарность в этом случае не имеет значения - речь идет о переупорядочении инструкций. Чтобы ваш код всегда работал на слабо упорядоченных архитектурах, вы должны установить 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
проверку. Это немного сбивает с толку, не глядя на историю редактирования.lock
. Речь идет не о том, как выполнить двойную проверку блокировки, а о атомарном доступе к памяти и о том, можно ли записывать логическое значение одновременно с чтением.