Почему volatile и MemoryBarrier не препятствуют переупорядочению операций?
Если я понимаю значение volatile и MemoryBarrier правильно, чем нижеприведенная программа никогда не сможет показать какой-либо результат.
Он ловит переупорядочение операций записи каждый раз, когда я его запускаю. Не имеет значения, запускаю ли я его в Debug или Release. Это также не имеет значения, если я запускаю его как 32-битное или 64-битное приложение.
Почему это происходит?
using System;
using System.Threading;
using System.Threading.Tasks;
namespace FlipFlop
{
class Program
{
//Declaring these variables as volatile should instruct compiler to
//flush all caches from registers into the memory.
static volatile int a;
static volatile int b;
//Track a number of iteration that it took to detect operation reordering.
static long iterations = 0;
static object locker = new object();
//Indicates that operation reordering is not found yet.
static volatile bool continueTrying = true;
//Indicates that Check method should continue.
static volatile bool continueChecking = true;
static void Main(string[] args)
{
//Restarting test until able to catch reordering.
while (continueTrying)
{
iterations++;
var checker = new Task(Check);
var writter = new Task(Write);
lock (locker)
{
continueChecking = true;
checker.Start();
}
writter.Start();
checker.Wait();
writter.Wait();
}
Console.ReadKey();
}
static void Write()
{
//Writing is locked until Main will start Check() method.
lock (locker)
{
//Using memory barrier should prevent opration reordering.
a = 1;
Thread.MemoryBarrier();
b = 10;
Thread.MemoryBarrier();
b = 20;
Thread.MemoryBarrier();
a = 2;
//Stops spinning in the Check method.
continueChecking = false;
}
}
static void Check()
{
//Spins until finds operation reordering or stopped by Write method.
while (continueChecking)
{
int tempA = a;
int tempB = b;
if (tempB == 10 && tempA == 2)
{
continueTrying = false;
Console.WriteLine("Caught when a = {0} and b = {1}", tempA, tempB);
Console.WriteLine("In " + iterations + " iterations.");
break;
}
}
}
}
}
Ответы
Ответ 1
Я не думаю, что это переупорядочение.
Этот фрагмент кода просто не является потокобезопасным:
while (continueChecking)
{
int tempA = a;
int tempB = b;
...
Я думаю, что этот сценарий возможен:
-
int tempA = a;
выполняется со значениями последнего цикла (a == 2)
- Существует контекстный переключатель в поток записи
-
b = 10
, и цикл останавливается
- Существует контекстный переключатель в поток проверки
-
int tempB = b;
выполняется с помощью b == 10
Я замечаю, что вызовы MemoryBarrier() повышают шансы на этот сценарий. Вероятно, потому что они вызывают большее переключение контекста.
Ответ 2
Вы не очищаете переменные между тестами, поэтому (для всех, кроме первого) изначально a
есть 2
, а b
- 20
- до Write
имеет сделал что-нибудь.
Check
может получить это начальное значение a
(так tempA
есть 2
), а затем Write
может войти, дойти до изменения b
до 10
.
Теперь Check
читает b
(поэтому tempB
is 10
).
Et voila. Не требуется повторного заказа для воспроизведения.
Reset a
и b
до 0
между прогонами, и я ожидаю, что оно исчезнет.
изменить: подтверждено; "как есть" Я получаю проблему почти сразу (< 2000 итераций); но добавив:
while (continueTrying)
{
a = b = 0; // reset <======= added this
он затем петли на любое количество времени без каких-либо проблем.
Или как поток:
Write A= B= Check
(except first run) 2 20
int tempA = a;
a = 1; 1 20
Thread.MemoryBarrier();
b = 10; 1 10
int tempB = b;
Ответ 3
Результат не имеет ничего общего с переупорядочением, с памятью или с изменчивым. Все эти конструкции необходимы, чтобы избежать эффектов компиляции или переупорядочения процессором инструкций.
Но эта программа даст тот же результат, даже если предположить полностью согласованную модель памяти с одним процессором и оптимизацию компилятора.
Прежде всего, обратите внимание, что параллельно будут запущены несколько задач Write()
. Они выполняются последовательно из-за lock() внутри Write(), но метод signle Check()
может читать a
и b
, созданные разными экземплярами задач Write()
.
Поскольку функция Check()
не имеет синхронизации с функцией Write
- она может читать a
и b
в два произвольных и разных момента. В коде нет ничего, что предотвращало бы Check()
от чтения a
, созданного предыдущим Write()
, в один момент, а затем чтение b
, созданное следующим образом Write()
в другой момент. Прежде всего вам нужна синхронизация (блокировка) в Check()
, а затем вы можете (но, вероятно, не в этом случае) нуждаться в барьерах памяти и нестабильны для борьбы с проблемами модели памяти.
Это все, что вам нужно:
int tempA, tempB;
lock (locker)
{
tempA = a;
tempB = b;
}
Ответ 4
-
Если вы используете MemoryBarrier
в writer
, почему бы вам не сделать это в checker
? Поместите Thread.MemoryBarrier();
до int tempA = a;
.
-
Вызов Thread.MemoryBarrier();
так много раз блокирует все преимущества метода. Вызовите его только один раз до или после a = 1;
.