Нам нужно заблокировать .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" - самое изящное решение.
Ответы
Ответ 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.