Подтвердить что ты не робот

Чтение int, которое обновляется с помощью блокировки на других потоках

(Это повторение: Как правильно прочитать поле Interlocked.Increment'ed int?, но, прочитав ответы и комментарии, я все еще не уверен в правильном ответе.)

Там есть код, который у меня нет, и не могу изменить использование блокировок, которые увеличивают счетчик int (numberOfUpdates) в нескольких разных потоках. Все вызовы используют:

Interlocked.Increment(ref numberOfUpdates);

Я хочу прочитать numberOfUpdates в моем коде. Теперь, поскольку это int, я знаю, что он не может порвать. Но какой лучший способ обеспечить, чтобы я получил самую последнюю ценность? Кажется, мои варианты:

int localNumberOfUpdates = Interlocked.CompareExchange(ref numberOfUpdates, 0, 0);

или

int localNumberOfUpdates = Thread.VolatileRead(numberOfUpdates);

Будут ли работать (в смысле предоставления последней возможной ценности независимо от оптимизации, переупорядочения, кеширования и т.д.)? Один из них предпочтительнее другого? Есть ли третий вариант, который лучше?

4b9b3361

Ответ 1

Я твердо убежден в том, что если вы используете блокировку для увеличения общих данных, вы должны использовать блокировку везде, где вы получаете доступ к этим общим данным. Аналогичным образом, если вы используете вставку для вашего любимого примитива синхронизации здесь, чтобы увеличить общие данные, тогда вы должны использовать вставляемый вами любимый примитив синхронизации здесь, где вы обращаетесь к этим общим данным.

int localNumberOfUpdates = Interlocked.CompareExchange(ref numberOfUpdates, 0, 0);

Дает вам именно то, что вы ищете. Как говорили другие, взаимосвязанные операции являются атомарными. Поэтому Interlocked.CompareExchange всегда будет возвращать самое последнее значение. Я использую это все время для доступа к простым общим данным, таким как счетчики.

Я не так хорошо знаком с Thread.VolatileRead, но я подозреваю, что он также вернет самое последнее значение. Я бы придерживался взаимосвязанных методов, хотя бы ради того, чтобы быть последовательным.


Дополнительная информация:

Я бы порекомендовал взглянуть на ответ Джона Скита, почему вы можете уклониться от Thread.VolatileRead(): Thread.VolatileRead Implementation

Эрик Липперт обсуждает волатильность и гарантии, сделанные моделью памяти С# в своем блоге на http://blogs.msdn.com/b/ericlippert/archive/2011/06/16/atomicity-volatility-and-immutability-are-different-part-three.aspx. Прямо из уст лошадей: "Я не пытаюсь написать код с низким уровнем блокировки, за исключением самых тривиальных способов блокировки операций. Я оставляю использование" изменчивых "для реальных экспертов".

И я согласен с Хансом в том, что значение всегда будет устаревшим, по крайней мере, на несколько нс, но если у вас есть прецедент, когда это неприемлемо, возможно, он плохо подходит для сбора собранных мусором языка, таких как С# ОС реального времени. У Джо Даффи есть хорошая статья о своевременности взаимосвязанных методов здесь: http://joeduffyblog.com/2008/06/13/volatile-reads-and-writes-and-timeliness/

Ответ 2

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;
}

Это обеспечит ваши потребности в атомарности и волатильности, не вникая в более неясные, относительно низкоуровневые примитивы с несколькими потоками.

Ответ 3

Оба будут работать (в смысле предоставления последней возможной ценности независимо от оптимизации, переупорядочения, кеширования и т.д.)?

Нет, значение, которое вы получаете, всегда устарело. Насколько устаревшее значение может быть совершенно непредсказуемым. Подавляющее большинство времени он будет устаревшим на несколько наносекунд, дайте или возьмите, в зависимости от того, как быстро вы действуете на величину. Но нет разумной верхней границы:

  • ваш поток может потерять процессор, когда он контекст - переключает другой поток на ядро. Типичные задержки составляют около 45 мс без твердой верхней границы. Это не означает, что другой поток в вашем процессе также отключается, он может продолжать движение и продолжать изменять значение.
  • как и любой код пользовательского режима, ваш код также подвержен ошибкам страницы. Поступают, когда процессор нуждается в ОЗУ для другого процесса. На сильно загруженной машине, которая может и будет выходить из активного кода. Как иногда бывает с кодом драйвера мыши, например, оставляя замороженный курсор мыши.
  • управляемые потоки подчиняются случайным случайным сбоям мусора. Как правило, это меньшая проблема, так как вероятно, что другой поток, который изменяет значение, также будет приостановлен.

Что бы вы ни делали со значением, нужно учитывать это. Излишне говорить, может быть, это очень, очень сложно. Практических примеров трудно найти..NET Framework - это очень большой кусок коллизионного кода. Вы можете увидеть перекрестную ссылку на использование VolatileRead из Справочного источника. Количество показов: 0.

Ответ 4

Хорошо, любое значение, которое вы читаете, всегда будет несколько устаревшим, как сказал Ханс Пассант. Вы можете контролировать только то, что другие общие значения согласуются с тем, который вы только что прочитали, используя заграждения памяти в середине кода, считывая несколько общих значений без блокировок (то есть: находятся в той же степени "устаревание" )

Заборы также влияют на победу над некоторыми оптимизациями компилятора и переупорядочением, что предотвращает непредвиденное поведение в режиме выпуска на разных платформах.

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 имеет эффект испускания "половины" забора. (т.е. он блокирует чтение/запись от перемещения до считывания на него и блокирует чтение/запись из перемещения после записи на него).