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

Нужно ли мне синхронизировать поток доступа к int?

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

private void RunReport()
{
   _reportsRunning++;

   try
   {
       //code to run the report
   }
   finally
   {
       _reportsRunning--;
   }
}

Это единственное место в коде, значение _reportsRunning изменено, и для этого метода требуется около секунды.

Иногда, когда у меня более шести или около того потоков, работающих вместе, итоговый результат для _reportsRunning может спуститься до -1. Если я переношу вызовы на _runningReports++ и _runningReports-- в блокировку, то поведение кажется правильным и последовательным.

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

4b9b3361

Ответ 1

Оператор A ++ не является атомарным в С# (и я сомневаюсь, что он гарантированно будет атомарным в С++), так что да, ваш учет зависит от условий гонки.

Используйте Interlocked.Increment и .Decrement

System.Threading.Interlocked.Increment(ref _reportsRunning);
try 
{
  ...
}
finally
{
   System.Threading.Interlocked.Decrement(ref _reportsRunning);
}

Ответ 2

Итак, на вопрос: когда я был изучение многопоточности в С++ я был учил, что вам не нужно синхронизировать вызовы для увеличения и декрементных операций, поскольку они были всегда одна инструкция по сборке и поэтому невозможно было нить должна быть отключена в середине вызова. Правильно ли я преподавал, и если да, то как что это не верно для С#?

Это невероятно неправильно.

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

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

Рассмотрим следующую инструкцию сборки x86:

inc [i]

Если я изначально равен 0, и код запускается на двух потоках на двух ядрах, значение после завершения обоих потоков может быть законным или 1 или 2, так как нет гарантии, что один поток завершит его чтение перед другим потоком завершает запись, или что одна запись потока будет даже видна до того, как будут прочитаны другие потоки.

Изменение этого на:

lock inc [i]

В результате получится окончательное значение 2.

Win32 InterlockedIncrement и InterlockedDecrement и .NET Interlocked.Increment и Interlocked.Decrement приводят к выполнению эквивалентного (возможно, одного и того же машинного кода) lock inc.

Ответ 3

Вас учили неправильно.

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

Ответ 4

Приращение int - это одна инструкция, но как насчет загрузки значения в регистре?

То, что i++ эффективно выполняет:

  • загрузить i в регистр
    • увеличить регистр
    • выгрузите регистр в i

Как вы можете видеть, есть 3 (это может быть иначе на других платформах) инструкции, которые на любой стадии могут переключать контекст в другой поток, оставляя вашу переменную в неизвестном состоянии.

Вы должны использовать Interlocked.Increment и Interlocked.Decrement, чтобы решить это.

Ответ 5

Нет, вам нужно синхронизировать доступ. В Windows вы можете сделать это легко с помощью InterlockedIncrement() и InterlockedDecrement(). Я уверен, что есть эквиваленты для других платформ.

EDIT: просто заметил тег С#. Сделайте то, что сказал другой парень. См. Также: Я слышал, что я ++ не является потокобезопасным, is ++ я потокобезопасным?

Ответ 6

Любая операция приращения/уменьшения на языке более высокого уровня (и да, даже C является более высоким уровнем по сравнению с машинными инструкциями) не является атомной по своей природе. Однако на каждой процессорной платформе обычно примитивы, поддерживающие различные атомные операции.

Если ваш лектор ссылался на машинные инструкции, операции Increment and Decrement, вероятно, будут атомарными. Тем не менее, это не всегда правильно на постоянно растущих многоядерных платформах сегодня, если они не гарантируют согласованность.

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

Ответ 7

x ++, вероятно, не является атомарным, но ++ x может быть (не уверенно, но если вы считаете разницу между пост-и предварительным приращением, должно быть понятно, почему предпосылка более поддается атомности).

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

Ответ 8

На однопроцессорной машине , если вы не используете виртуальную память, x ++ (игнорируется rvalue), скорее всего, преобразуется в одну атомную инструкцию INC на архитектуре x86 (если x длинный, операция выполняется только при использовании 32-разрядного компилятора). Кроме того, movsb/movsw/movsl являются атомарными способами перемещения байта/слова/слова; компилятор не может использовать их как обычный способ назначения переменных, но у него может быть функция утилиты атомного перемещения. Администратор виртуальной памяти мог бы быть написан таким образом, что эти инструкции будут вести себя атомарно, если ошибка страницы возникает при записи, но я не думаю, что это нормально гарантировано.

На многопроцессорной машине все ставки отключены, если не использовать явные блокированные инструкции (invokable через специальные вызовы библиотеки). Наиболее универсальная инструкция, которая обычно доступна, - CompareExchange. Эта команда изменяет местоположение памяти только в том случае, если она содержит ожидаемое значение; он вернет значение, которое он имел, когда решил, изменять или нет. Если вы хотите "xor" переменную с 1, можно сделать что-то вроде (в vb.net)

  Dim OldValue as Integer
  Do
    OldValue = Variable
  While Threading.Interlocked.CompareExchange(Variable, OldValue Xor 1, OldValue)  OldValue

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

Важные оговорки: (1) Держите петлю как можно короче; чем дольше цикл, тем больше вероятность того, что другая задача попадет в переменную во время цикла, и чем больше времени будет потрачено впустую каждый раз, когда это произойдет; (2) определенное количество обновлений, разделенных произвольно между потоками, всегда будет завершено, поскольку единственный способ, которым поток может принудительно повторно выполнить цикл, - это если какой-то другой поток сделал полезный прогресс; Однако, если некоторые потоки могут выполнять обновления без дальнейшего продвижения к завершению, код может стать заблокированным.