Чтение int, который обновляется Interlocked в других темах
(Это повторение: как правильно прочитать int-поле Interlocked.Increment? Но после прочтения ответов и комментариев я все еще не уверен в правильном ответе.)
Есть некоторый код, которым я не владею и не могу изменить на использование блокировок, которые увеличивают счетчик int (numberOfUpdates) в нескольких разных потоках. Все звонки используют:
Interlocked.Increment(ref numberOfUpdates);
Я хочу прочитать numberOfUpdates в моем коде. Теперь, так как это int, я знаю, что он не может порваться. Но как лучше всего добиться того, чтобы я получил самое последнее значение? Похоже, мои варианты:
int localNumberOfUpdates = Interlocked.CompareExchange(ref numberOfUpdates, 0, 0);
Или же
int localNumberOfUpdates = Thread.VolatileRead(numberOfUpdates);
Будет ли работать оба (в смысле предоставления последней возможной стоимости независимо от оптимизации, переупорядочения, кэширования и т. Д.)? Один предпочтительнее другого? Есть ли третий вариант, который лучше?
6 ответов
Я твердо верю в то, что если вы используете блокировку для увеличения общих данных, то вы должны использовать блокировку везде, где у вас есть доступ к этим общим данным. Аналогичным образом, если вы используете вставляемый вами любимый примитив синхронизации здесь для увеличения общих данных, то вам следует использовать вставленный вами любимый примитив синхронизации здесь везде, где вы получаете доступ к этим совместно используемым данным.
int localNumberOfUpdates = Interlocked.CompareExchange(ref numberOfUpdates, 0, 0);
Даст вам именно то, что вы ищете. Как уже говорили другие, взаимосвязанные операции являются атомарными. Поэтому Interlocked.CompareExchange всегда будет возвращать самое последнее значение. Я использую это все время для доступа к простым общим данным, таким как счетчики.
Я не так хорошо знаком с Thread.VolatileRead, но я подозреваю, что он также вернет самое последнее значение. Я бы придерживался взаимосвязанных методов, хотя бы ради того, чтобы быть последовательным.
Дополнительная информация:
Я бы посоветовал взглянуть на ответ Джона Скита о том, почему вы можете отказаться от Thread.VolatileRead(): реализация Thread.VolatileRead.
Эрик Липперт обсуждает изменчивость и гарантии, которые дает модель памяти C#, в своем блоге по адресу http://blogs.msdn.com/b/ericlippert/archive/2011/06/16/atomicity-volatility-and-immutability-are-different-part-three.aspx. Прямо из уст лошадей: "Я не пытаюсь писать какой-либо код с низким уровнем блокировки, кроме самых тривиальных способов использования операций с блокировкой. Я оставляю использование" volatile "для настоящих экспертов".
И я согласен с точкой зрения Ганса, что значение всегда будет устаревшим, по крайней мере, на несколько нс, но если у вас есть случай использования, где это недопустимо, оно, вероятно, не очень хорошо подходит для языка сборки мусора, такого как C# или нереального время ОС. Джо Даффи имеет хорошую статью о своевременности взаимосвязанных методов здесь: http://joeduffyblog.com/2008/06/13/volatile-reads-and-writes-and-timeliness/
Thread.VolatileRead(numberOfUpdates)
это то, что вы хотите. numberOfUpdates
является Int32
так что у вас уже есть атомарность по умолчанию, и Thread.VolatileRead
обеспечит волатильность
Если numberOfUpdates
определяется как volatile int numberOfUpdates;
Вы не должны делать это, поскольку все чтения этого будут уже изменчивыми чтениями.
Там, кажется, путаница о том, Interlocked.CompareExchange
более уместно. Рассмотрим следующие два отрывка из документации.
От Thread.VolatileRead
документация:
Читает значение поля. Значение является последним, записанным любым процессором в компьютере, независимо от количества процессоров или состояния кэша процессора.
От Interlocked.CompareExchange
документация:
Сравнивает два 32-разрядных целых числа со знаком на равенство и, если они равны, заменяет одно из значений.
С точки зрения заявленного поведения этих методов, Thread.VolatileRead
явно более уместно. Вы не хотите сравнивать numberOfUpdates
на другое значение, и вы не хотите заменить его значение. Вы хотите прочитать его значение.
Лассе делает хороший комментарий в своем комментарии: вам может быть лучше использовать простую блокировку. Когда другой код хочет обновить numberOfUpdates
это делает что-то вроде следующего.
lock (state)
{
state.numberOfUpdates++;
}
Когда вы хотите прочитать это, вы делаете что-то вроде следующего.
int value;
lock (state)
{
value = state.numberOfUpdates;
}
Это обеспечит ваши требования атомарности и изменчивости, не углубляясь в более неясные, относительно низкоуровневые многопоточные примитивы.
Будет ли работать оба (в смысле предоставления последней возможной стоимости независимо от оптимизации, переупорядочения, кэширования и т. Д.)?
Нет, ценность, которую вы получаете, всегда устарела. Насколько устаревшее значение может быть, совершенно непредсказуемо. В подавляющем большинстве случаев она будет устаревать на несколько наносекунд, отдавать или брать, в зависимости от того, насколько быстро вы действуете на величину. Но нет разумной верхней границы:
- ваш поток может потерять процессор, когда он переключает контекст другого потока на ядро. Типичные задержки составляют около 45 мсек без четкой верхней границы. Это не означает, что другой поток в вашем процессе также отключается, он может продолжать движение и продолжать изменять значение.
- Как и любой код пользовательского режима, ваш код также подвержен ошибкам страницы. Возникает, когда процессору требуется оперативная память для другого процесса. На сильно загруженной машине, которая может выгрузить активный код. Как иногда случается с кодом драйвера мыши, например, оставляя замороженный курсор мыши.
- управляемые потоки подвергаются почти случайным паузам сборки мусора. Как правило, это меньшая проблема, так как вполне вероятно, что другой поток, который изменяет значение, будет также приостановлен.
Что бы вы ни делали со значением, это нужно учитывать. Нет нужды говорить, что это очень и очень сложно. Практические примеры трудно найти..NET Framework - это очень большой кусок кода с боевыми шрамами. Вы можете увидеть перекрестную ссылку на использование VolatileRead из справочного источника. Количество просмотров: 0.
Что ж, любая ценность, которую вы прочитали, всегда будет несколько устаревшей, как сказал Ганс Пассант. Вы можете только контролировать гарантию того, что другие общие значения согласуются с тем, который вы только что прочитали, используя заборы памяти в середине кода, считывающие несколько общих значений без блокировок (то есть: с той же степенью "устаревания")
Ограждения также приводят к поражению некоторых оптимизаций компилятора и переупорядочению, таким образом предотвращая непредвиденное поведение в режиме выпуска на разных платформах.
Thread.VolatileRead вызовет заполнение полной памяти, так что никакие чтения или записи не могут быть переупорядочены вокруг вашего чтения int (в методе, который читает это). Очевидно, что если вы читаете только одно общее значение (и вы не читаете что-то еще совместно используемое, а порядок и последовательность их обоих важны), то это может показаться необязательным...
Но я думаю, что вам все равно это понадобится для того, чтобы победить некоторые оптимизации компилятором или процессором, чтобы вы не получили чтение "устаревшего", чем необходимо.
Фиктивный Interlocked.CompareExchange будет делать то же самое, что и Thread.VolatileRead (полное отклонение и оптимизация, побеждающая поведение).
В структуре используется шаблон, используемый CancellationTokenSource http://referencesource.microsoft.com/#mscorlib/system/threading/CancellationTokenSource.cs#64
//m_state uses the pattern "volatile int32 reads, with cmpxch writes" which is safe for updates and cannot suffer torn reads.
private volatile int m_state;
public bool IsCancellationRequested
{
get { return m_state >= NOTIFYING; }
}
// ....
if (Interlocked.CompareExchange(ref m_state, NOTIFYING, NOT_CANCELED) == NOT_CANCELED) {
}
// ....
Ключевое слово volatile создает "половинный" забор. (то есть: блокирует чтение / запись от перемещения до чтения в него и блокирует чтение / запись от перемещения после записи в него).
Похоже, мои варианты:
int localNumberOfUpdates = Interlocked.CompareExchange(ref numberOfUpdates, 0, 0);
Или
int localNumberOfUpdates = Thread.VolatileRead(numberOfUpdates);
Начиная с .NET Framework 4.5 есть третий вариант:
int localNumberOfUpdates = Volatile.Read(ref numberOfUpdates);
The методы налагают полные заборы, в то время какметоды накладывают полузаборы¹. Таким образом, использование статических методов класса является потенциально более экономичным способом атомарного чтения последнего значения объекта.int
переменная или поле.
В качестве альтернативы, если это поле класса, вы можете объявить его как. Чтение поля эквивалентно его чтению с помощьюVolatile.Read
метод.
Я должен упомянуть еще один вариант, который состоит в том, чтобы просто прочитатьnumberOfUpdates
непосредственно, без помощиInterlocked
или . Мы не должны этого делать, но демонстрация реальной проблемы, вызванной этим, может оказаться невозможной. Причина в том, что модели памяти наиболее часто используемых процессоров сильнее, чем модель памяти C#. Так что, если на вашей машине такой процессор (например, x86 или x64), вы не сможете написать программу, которая выйдет из строя в результате непосредственного чтения поля. Тем не менее, лично я никогда не использую эту опцию, потому что я не эксперт в архитектуре ЦП и протоколах памяти, и у меня нет желания им стать. Поэтому я предпочитаю использовать либоVolatile
класс илиvolatile
ключевое слово, как удобнее в каждом конкретном случае.
¹ За некоторыми исключениями, такими как чтение/запись
Int64
или
double
на 32-битной машине.
Не уверен, почему никто не упомянул
Interlocked.Add(ref localNumberOfUpdates, 0)
, но мне кажется самый простой способ...