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

Нам нужно заблокировать .NET Int32 при чтении его в многопоточном коде?

Я читал следующую статью: http://msdn.microsoft.com/en-us/magazine/cc817398.aspx "Решение 11 вероятных проблем в многопоточном коде" Джо Даффи

И это подняло меня вопрос: "Нам нужно заблокировать .NET Int32 при чтении его в многопоточном коде?"

Я понимаю, что если это был Int64 в 32-битном SO, он мог бы порвать, как это объясняется в статье. Но для Int32 я представил следующую ситуацию:

class Test
{
  private int example = 0;
  private Object thisLock = new Object();

  public void Add(int another)
  {
    lock(thisLock)
    {
      example += another;
    }
  }

  public int Read()
  {
     return example;
  }
}

Я не вижу причины включать блокировку в метод Read. Вы?

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

  • Добавление в "int example" свойства "volatile"
  • Вставка Thread.MemoryBarrier(); перед фактическим чтением "int example"
  • Прочитайте "int example" внутри "lock (thisLock)"

И я также считаю, что "volatile" - самое изящное решение.

4b9b3361

Ответ 1

Блокировка выполняет две вещи:

  • Он действует как мьютекс, поэтому вы можете убедиться, что только один поток изменяет набор значений за раз.
  • Он обеспечивает барьеры памяти (семантика получения/выпуска), которая гарантирует, что записи в памяти, сделанные одним потоком, видны в другом.

Большинство людей понимают первый пункт, но не второй. Предположим, вы использовали код в вопросе из двух разных потоков: один поток вызывал Add несколько раз, а другой поток вызывал Read. Атомность сама по себе обеспечила бы, что вы только закончили чтение кратным 8 - и если бы были два потока, вызывающие Add, ваш замок обеспечит, чтобы вы не "потеряли" какие-либо дополнения. Однако вполне возможно, что ваш поток Read будет только читать 0, даже после того, как Add был вызван несколько раз. Без каких-либо барьеров памяти JIT может просто кэшировать значение в регистре и предположить, что он не изменился между чтениями. Точка барьера памяти должна либо убедиться, что что-то действительно записано в основную память, либо действительно прочитано из основной памяти.

Модели памяти могут стать довольно волосатыми, но если вы будете следовать простому правилу вынимать блокировку каждый раз, когда хотите получить доступ к общим данным (для чтения или записи), вы будете в порядке. Дополнительную информацию см. В разделе volatility/atomicity моего учебника по многопоточности.

Ответ 2

Все зависит от контекста. При использовании интегральных типов или ссылок вы можете использовать члены класса System.Threading.Interlocked.

Типичное использование, например:

if( x == null )
  x = new X();

Можно заменить вызовом Interlocked.CompareExchange():

Interlocked.CompareExchange( ref x, new X(), null);

Interlocked.CompareExchange() гарантирует, что сравнение и обмен выполняются как атомная операция.

Другие члены класса Interlocked, такие как Добавить(), Decrement(), Exchange(), Приращение ( ) и Read() выполняют свои операции атомарно. Прочтите документацию в MSDN.

Ответ 3

Это зависит от того, как вы собираетесь использовать 32-битное число.

Если вы хотите выполнить такую ​​операцию, как:

i++;

Это неявно разбивается на

  • чтение значения i
  • добавление одного
  • сохранение i

Если другой поток изменяет я после 1, но до 3, то у вас есть проблема, когда мне было 7, вы добавляете ее к ней, а теперь ее 492.

Но если вы просто читаете я или выполняете одну операцию, например:

i = 8;

тогда вам не нужно блокировать i.

Теперь, ваш вопрос говорит: "... нужно блокировать .NET Int32 при чтении..." но ваш пример включает чтение, а затем запись в Int32.

Итак, это зависит от того, что вы делаете.

Ответ 4

Только 1 блокировка потока ничего не выполняет. Цель блокировки - заблокировать другие потоки, но она не работает, если никто не проверяет блокировку!

Теперь вам не нужно беспокоиться о повреждении памяти с 32-битным int, потому что запись является атомарной, но это не обязательно означает, что вы можете заблокировать.

В вашем примере можно получить сомнительную семантику:

example = 10

Thread A:
   Add(10)
      read example (10)

Thread B:
   Read()
      read example (10)

Thread A:
      write example (10 + 10)

что означает, что ThreadB начал читать значение примера после того, как поток A начал обновление, но прочитал предварительно переопределенное значение. Я полагаю, что проблема или нет, зависит от того, что должен делать этот код.

Так как это пример кода, может быть трудно увидеть проблему там. Но представьте себе каноническую функцию счетчика:

 class Counter {
    static int nextValue = 0;

    static IEnumerable<int> GetValues(int count) {
       var r = Enumerable.Range(nextValue, count);
       nextValue += count;
       return r;
    }
 }

Затем следующий сценарий:

 nextValue = 9;

 Thread A:
     GetValues(10)
     r = Enumerable.Range(9, 10)

 Thread B:
     GetValues(5)
     r = Enumerable.Range(9, 5)
     nextValue += 5 (now equals 14)

 Thread A:
     nextValue += 10 (now equals 24)

NextValue увеличивается, но возвращенные диапазоны будут перекрываться. Значения 19 - 24 никогда не возвращались. Вы исправите это, заблокировав назначение var r и nextValue, чтобы предотвратить выполнение любого другого потока в одно и то же время.

Ответ 5

Блокировка необходима, если вам нужно, чтобы она была атомарной. Чтение и запись (как спаренная операция, например, когда вы выполняете я ++), 32-битное число не гарантируется атомарным из-за кэширования. Кроме того, индивидуальное чтение или запись необязательно подходит к регистру (волатильность). Обеспечение его волатильности не дает вам никакой гарантии атомарности, если у вас есть желание изменить целое число (например, чтение, приращение, запись). Для целых чисел мьютекс или монитор могут быть слишком тяжелыми (зависит от вашего варианта использования) и что для Interlocked class. Это гарантирует атомарность этих типов операций.

Ответ 6

в общем случае блокировки требуются только при изменении значения

EDIT: Mark Brackett отличное резюме более уместно:

"Замки необходимы, если вы хотите, чтобы иначе неатомная операция была атомарной"

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