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

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

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

Interlocked.Increment(ref numberOfUpdates);

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

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

Or

int localNumberOfUpdates = Thread.VolatileRead(numberOfUpdates);

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


person Michael Covelli    schedule 17.07.2014    source источник
comment
Кажется, это частый вопрос, см. stackoverflow.com/questions/516863/. Но мне все еще не ясно, лучше ли Thread.VolatileRead или Interlocked.CompareExchange или это не имеет значения.   -  person Michael Covelli    schedule 18.07.2014
comment
Стоит задуматься над одним вопросом: что происходит дальше, в зависимости от получения «последнего» значения numberOfUpdates, и как это взаимодействует с тем, что увеличивает его? Каковы последствия, если вы прочитаете немного устаревшее кешированное значение?   -  person Dan Bryant    schedule 22.07.2014
comment
Ничего такого. Если я получу старое значение, это будет нормально, если в конечном итоге я увижу правильное значение. Я не слишком беспокоюсь о чтении int и получении значения 100 мс. Меня больше беспокоит то, что что-то оптимизируется или кэшируется в регистре и т. Д., Так что мое чтение никогда не видит обновленное значение. Я не понимаю, как такое могло случиться. Но я просто подумал, что мне следует прочитать int наилучшим, наиболее идиоматическим способом C #. И я не был уверен, лучше ли Interlocked.CompareExchange или Thread.VolatileRead или нет никакой разницы.   -  person Michael Covelli    schedule 23.07.2014
comment
Лучший способ - вообще этого не делать. Использование нескольких потоков - плохая идея. Если у вас должно быть несколько потоков, общая память - плохая идея. Если вам необходимо совместно использовать память, уклонение от блокировки - плохая идея. Блокируйте каждый доступ к общей памяти. Если это оказывается неприемлемым бременем для производительности, и вы не можете устранить разногласия, или бремя существует даже без разногласий, только затем следует рассмотреть возможность оценки решения с низким уровнем блокировки.   -  person Eric Lippert    schedule 23.07.2014
comment
Мне не нужна производительность, поэтому нет цели использовать код с низким уровнем блокировки. Другие разработчики уже изменяют это целое число, используя только Interlocked. * Вне каких-либо блокировок, поэтому блокировки для меня не вариант. Мне просто нужно прочитать этот int как можно лучше, учитывая, что он обновляется вне блокировок (хотя всегда с использованием Interlocked. *, Насколько я вижу). Если бы я начинал это с самого начала, я бы просто заблокировал все, чтобы было проще.   -  person Michael Covelli    schedule 23.07.2014


Ответы (4)


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

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-неизменяемость-разные-часть-три.aspx. Прямо из уст лошадей: «Я не пытаюсь писать какой-либо код с низким уровнем блокировки, за исключением самых тривиальных применений операций Interlocked. Я оставляю использование слова« volatile »для настоящих экспертов».

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

person Pat Hensel    schedule 22.07.2014
comment
Я согласен с вашим мнением о повсеместном использовании одного примитива синхронизации. И я видел множество статей людей, которые действительно знают, что они делают, в которых говорится, что volatile сбивает с толку с точки зрения того, что на самом деле означает. Так что я предпочитаю придерживаться Interlocked для всех операций чтения. Но мне не нравится, что я должен подделывать это, используя Interlocked.CompareExchange(ref foo,0,0) вместо Interlocked.Read(ref foo), который дает понять из кода, что все, что я хочу сделать, это прочитать. - person Michael Covelli; 23.07.2014
comment
Собственно, я только что посмотрел BCL для Interlocked.cs. И похоже, что Interlocked.Read for longs буквально только что реализован с Interlocked.CompareExchange(ref location,0,0). Так что я думаю, что делать Interlocked.CompareExchange(ref foo,0,0) для ints должно быть нормально. Если бы было int Interlocked.Read, то это было бы реализовано именно так. - person Michael Covelli; 23.07.2014
comment
@Michael +1 за проверку BCL. Раньше я заглядывал в Interlocked.cs, но никогда не доходил до конца страницы, где определено Interlocked.Read. Я всегда предполагал, что у него есть собственная внешняя реализация, как и у большинства других участников. Узнавайте что-то новое каждый день. - person Pat Hensel; 23.07.2014
comment
Это хороший совет. Что касается изменчивых чтений: изменчивое чтение даст вам самое актуальное доступное значение однако язык C # не дает никаких гарантий, что будет соблюдаться единый канонический порядок всех изменчивых чтений и записей по всем темам. Помните, что существует большая разница между тем, что я только что прочитал последнее значение, и тем, что чтение одной переменной не будет переупорядочено относительно записи другой переменной, но многие люди неявно предполагают, что они одинаковы; их совершенно нет! - person Eric Lippert; 23.07.2014
comment
Разве эти два предложения не противоречат друг другу: как говорили другие, взаимосвязанные операции атомарны. Таким образом, Interlocked.CompareExchange всегда будет возвращать самое последнее значение. Атомичность не гарантирует, что вы получите самое последнее значение, это гарантирует, что значение не разорвется. Это всегда работает на x86 и AMD64, но на ARM или IA64 вы можете получить старое значение из кеша, если просто используете Interlocked из потока diff. Если я чего-то не упускаю? - person markmnl; 13.05.2016
comment
@markmnl Совершенно верно. Все зависит от модели памяти, архитектуры процессора. Все, что делает Interlocked, - это возлагает ограждения на память вокруг операций. На процессоре x86 (который имеет сильную модель памяти) это приводит к возврату последнего значения. Попробуйте использовать Interlocked на архитектурах с более слабыми моделями памяти (например, ARM, PowerPC), и в определенных сценариях могут произойти непредвиденные вещи. - person 0b101010; 10.06.2016
comment
32-битные чтения (например, int) всегда атомарны в .Net. Вот почему Interlocked.Read (ref long) существует, а Interlocked.Read (ref int) - нет. «Твердая вера» здесь явно не применима. Я удивлен, что, хотя в ответе Тимоти Шилдса это уже упоминалось, здесь никто не сказал. Конечно, единственный вопрос (как предлагает @EricLippert): просто выполнять обычное чтение или использовать Thread.VolatileRead, нет? - person Max Barraclough; 15.05.2018
comment
Я думаю, что ошибся - похоже, имеет смысл использовать CompareExchange для принудительного выполнения ожидаемого поведения чтения, особенно при ориентации на Mono: stackoverflow.com / a / 11159244 - person Max Barraclough; 19.05.2018

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

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

person Timothy Shields    schedule 17.07.2014
comment
Но я думаю, что Interlocked.Read () работает только с длинными? Не для бронзовых. - person Michael Covelli; 17.07.2014
comment
Похоже, есть две проблемы: предотвращение разрывов и получение актуального значения. Interlocked.Read () решит оба (насколько я понимаю), но только надолго. У меня нет проблемы с разрывом (независимо от x86 или x64), так как у меня есть int. Но у меня еще второй выпуск чтения в последнем значении. - person Michael Covelli; 17.07.2014
comment
@MichaelCovelli Я почти уверен, что Interlocked.Read(ref int) не существует, потому что чтение Int32 уже всегда атомарно на уровне процессора. Даже чтение 64-битного int является атомарным на уровне процессора - но только на 64-битных процессорах. Вот почему существует этот метод. Я упустил из виду, что вы использовали int, а не long. Что именно вы имеете в виду под последним значением? - person Timothy Shields; 17.07.2014
comment
Я хочу убедиться, что я не читаю кешированное значение, которое устарело, потому что счетчик увеличивается на другом процессоре. - person Michael Covelli; 17.07.2014
comment
@MichaelCovelli Тогда Thread.VolatileRead - это то, что вы хотите использовать. Считывание вашего Int32 по умолчанию уже будет атомарным, а Thread.VolatileRead обеспечит волатильность. Я обновил ответ. - person Timothy Shields; 17.07.2014
comment
Но должен ли CompareExchange работать? Я читал, что это создаст полный забор. - person Michael Covelli; 17.07.2014
comment
Барьеры памяти, создаваемые VolatileRead, являются противоположностью тому, чего ожидают многие люди. В частности, если один поток выполняет VolatileWrite или блокированную запись переменной, а другой поток выполняет VolatileRead или блокированное чтение, тогда любые другие переменные, записанные первым потоком до записи, будут видны второму после прочитанного. - person supercat; 19.07.2014
comment
Также обратите внимание, что VolatileRead может привести к полной очистке всех кешей ядра, что несколько снизит производительность многопоточного кода. Могут быть лучшие альтернативы, такие как блокировка. - person Lasse V. Karlsen; 22.07.2014
comment
@ LasseV.Karlsen Но разве Interlocked.CompareExchange не требует такой же полной промывки? - person Michael Covelli; 23.07.2014
comment
@TimothyShields Я согласен с тем, что блокировка - это хороший способ, если я буду контролировать весь код. Мне не нужна производительность, поэтому нет цели использовать код с низким уровнем блокировки. Но другие уже изменяют это целое число, используя только Interlocked.* вне каких-либо блокировок, поэтому блокировки для меня не вариант. Мне просто нужно прочитать это int как можно лучше, учитывая, что он обновляется вне блокировок (хотя всегда с использованием Interlocked.*) - person Michael Covelli; 23.07.2014
comment
@MichaelCovelli На x86 Interlocked.CompareExchange реализован как CMPXCHG с полным забором для предотвращения переупорядочения. Он очистит буфер хранилища, но НЕ кеш. Кэш использует MESI, чтобы гарантировать, что он не читает грязное значение. Насчет других архитектур я не уверен. - person Pat Hensel; 23.07.2014
comment
Это правильный ответ, многие считают, что Interlocked - это все, что им нужно, но он предоставляет только атомарность, которая предотвращает разрыв значения - это не гарантирует, что вы получите последнее значение. - person markmnl; 13.05.2016

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

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

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

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

person Hans Passant    schedule 22.07.2014
comment
Мой вопрос был неправильно сформулирован. Я не ожидаю и не требую жесткой верхней границы устаревания с точки зрения времени. Практически, даже простое чтение int без Interlocked или Volatile не вызывает проблем, когда я запускаю dev. Для меня не так уж и плохо получить значение, которое устарело на 100 миллисекунд. Меня больше беспокоит какой-то странный переупорядочение или оптимизация, из-за которых мое чтение int кэшируется, а не обновляется. Поэтому я просто хотел бы выбрать лучший, наиболее идиоматический способ чтения int в C #, учитывая, что он обновляется только с помощью Interlocked.Increment и Interlocked.Decrement. - person Michael Covelli; 23.07.2014
comment
OP кажется мне ясным: в момент чтения значение является самым последним int, которое будет отражать все обновления, сделанные до этого, то есть не в то время, когда мы используем значение - мы все знаю, что для этого нет теоретической верхней границы. - person markmnl; 13.05.2016

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

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

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

person Raif Atef    schedule 22.07.2014