Когда я должен использовать volatile/Thread.MemoryBarrier() для безопасности потоков?
Когда использовать "volatile" или "Thread.MemoryBarrier()" в потоковом коде блокировки? (С#)
Ответ 1
Вы используете volatile
/Thread.MemoryBarrier()
, когда хотите получить доступ к переменной через потоки без блокировки.
Переменные, которые являются атомарными, например, int
, всегда читаются и записываются целиком одновременно. Это означает, что вы никогда не получите половину значения, прежде чем другой поток изменит его, а другую половину после его изменения. Из-за этого вы можете безопасно читать и записывать значение в разных потоках без синхронизации.
Однако компилятор может оптимизировать некоторые чтения и записи, которые вы предотвращаете с помощью ключевого слова volatile
. Если вы, например, имеете такой цикл:
sum = 0;
foreach (int value in list) {
sum += value;
}
Компилятор может фактически выполнять вычисления в регистре процессора и только записывать значение в переменную sum
после цикла. Если вы создаете переменную sum
volatile
, компилятор будет генерировать код, который считывает и записывает переменную для каждого изменения, так что это значение обновляется на протяжении всего цикла.
Ответ 2
Что случилось с
private static readonly object syncObj = new object();
private static int counter;
public static int NextValue()
{
lock (syncObj)
{
return counter++;
}
}
?
Это делает все необходимые блокировки, барьеры памяти и т.д. для вас. Это хорошо понятное и читаемое, чем любой пользовательский код синхронизации на основе volatile
и Thread.MemoryBarrier()
.
ИЗМЕНИТЬ
Я не могу думать о сценарии, в котором я бы использовал volatile
или Thread.MemoryBarrier()
. Например
private static volatile int counter;
public static int NextValue()
{
return counter++;
}
не эквивалентен вышеприведенному коду и не потокобезопасен (volatile
не делает ++
волшебным образом потокобезопасным).
В таком случае:
private static volatile bool done;
void Thread1()
{
while (!done)
{
// do work
}
}
void Thread2()
{
// do work
done = true;
}
(который должен работать) Я бы использовал ManualResetEvent, чтобы сигнализировать, когда Thread2 сделан.
Ответ 3
В принципе, если вы используете какую-либо другую синхронизацию, чтобы сделать ваш код потокобезопасным, вам не нужно.
Большинство механизмов блокировки (включая блокировку) автоматически подразумевают барьер памяти, так что несколько процессоров могут получить правильную информацию.
Volatile и MemoryBarrier в основном используются в сценариях без блокировки, где вы пытаетесь избежать штрафа за блокировку.
Изменить: Вы должны прочитать эту статью Джо Даффи о модели памяти CLR 2.0, в ней разъясняется много вещей (если вам действительно интересно, вы должны прочитать ВСЕ статью от Джо Даффи, которая по большому счету является самым экспертом в parallelism в .NET)
Ответ 4
Поскольку название подразумевает volatile гарантирует, что значение кэша будет сброшено в память, так что все потоки будут видеть одно и то же значение. Например, если у меня есть целое число, последняя запись которого сохраняется в кеше, другие потоки могут этого не видеть. Они могут даже увидеть свою кеш-копию этого целого. Маркировка переменной как изменчивой позволяет ее считывать непосредственно из памяти.
Шриванта Шри Аравинда Аттанайяке