Нам нужно заблокировать .NET Int32 при чтении его в многопоточном коде? - PullRequest
14 голосов
/ 27 декабря 2008

Я читал следующую статью: 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. Вы?

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

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

И я также думаю, что «volatile» - самое элегантное решение.

Ответы [ 6 ]

24 голосов
/ 27 декабря 2008

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

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

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

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

7 голосов
/ 27 декабря 2008

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

Типичное использование как:

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

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

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

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

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

3 голосов
/ 27 декабря 2008

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

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

i++;

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

  1. чтение значения i
  2. добавление одного
  3. хранение i

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

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

i = 8;

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

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

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

2 голосов
/ 27 декабря 2008

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

2 голосов
/ 27 декабря 2008

Наличие только 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, чтобы предотвратить одновременное выполнение любого другого потока.

0 голосов
/ 27 декабря 2008

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

РЕДАКТИРОВАТЬ: Марк Брэкетт * Отличное резюме более подходящее:

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

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

...