Я создаю два потока и передаю функцию, которая выполняет алгоритм, показанный ниже 10 000 000 раз. В основном он пишет "5" в консоли, а иногда пишет "3" или "4". Это совершенно очевидно, почему это похоже. Но здесь возникает запутанная часть: зачем она пишет "6" в консоли?
Ответ 2
Это типичный состояние гонки. EDIT: На самом деле, есть несколько условий гонки.
Это может произойти в любое время, когда _state
равно 3, и оба потока достигают сразу за оператором if
, либо одновременно путем переключения контекста в одном ядре, либо одновременно параллельно в нескольких ядрах.
Это связано с тем, что оператор ++
сначала считывает _state
, а затем увеличивает его. Возможно, что после первого оператора if
вы заработали достаточно времени, чтобы прочитать 5 или даже 6.
EDIT: если вы обобщили этот пример для N потоков, вы можете увидеть число до 3 + N + 1.
Это может быть правильно, когда потоки начинают работать, или когда вы только что установили _state
в 3.
Чтобы избежать этого, используйте блокировку вокруг оператора if
или используйте Interlocked
для доступа к _state
, например if (System.Threading.Interlocked.CompareAndExchange(ref _state, 3, 4) == 3)
и System.Threading.Interlocked.Exchange(ref _state, 3)
.
Если вы хотите сохранить условие гонки, вы должны объявить _state
как volatile
, иначе вы рискуете, чтобы каждый поток видел _state
локально без обновлений из других потоков.
В качестве альтернативы вы можете использовать System.Threading.Volatile.Read
и System.Threading.Volatile.Write
, если вы включите реализацию, чтобы иметь _state
в качестве переменной и Tr
как замыкание, которое фиксирует эту переменную, поскольку локальные переменные не могут быть (и не сможет быть) объявлен volatile
. В этом случае даже инициализация должна выполняться с помощью volatile write.
EDIT: возможно, условия гонки более очевидны, если мы немного изменим код, расширив каждое прочитанное:
// Without some sort of memory barrier (volatile, lock, Interlocked.*),
// a thread is allowed to see _state as if other threads hadn't touched it
private static volatile int _state = 3;
// ...
for (int i = 0; i < 10000000; i++)
{
int currentState;
currentState = _state;
if (currentState == 3)
{
// RACE CONDITION: re-read the variable
currentState = _state;
currentState = currentState + 1:
// RACE CONDITION: non-atomic write
_state = currentState;
currentState = _state;
if (currentState != 4)
{
// RACE CONDITION: re-read the variable
currentState = _state;
Console.Write(currentState);
}
_state = 3;
}
}
Я добавил комментарии в местах, где _state
может отличаться от предполагаемых предыдущими инструкциями чтения переменных.
Здесь длинная диаграмма, которая показывает, что даже можно печатать 6 дважды подряд, один раз в каждом потоке, как изображение, которое было опубликовано оп. Помните, что потоки могут не работать синхронно, как правило, из-за упреждающего переключения контекста, кэширования или разницы в скорости ядра (из-за экономии энергии или временной турбоскоростной скорости):
![Race condition prints 6]()
Это похоже на оригинал, но он использует класс volatile
, где state
теперь является переменной, захваченной закрытием. Количество и порядок волатильных доступов становятся очевидными:
static void Main(string[] args)
{
int state = 3;
ThreadStart tr = () =>
{
for (int i = 0; i < 10000000; i++)
{
if (Volatile.Read(ref state) == 3)
{
Volatile.Write(ref state, Volatile.Read(state) + 1);
if (Volatile.Read(ref state) != 4)
{
Console.Write(Volatile.Read(ref state));
}
Volatile.Write(ref state, 3);
}
}
};
Thread firstThread = new Thread(tr);
Thread secondThread = new Thread(tr);
firstThread.Start();
secondThread.Start();
firstThread.Join();
secondThread.Join();
Console.ReadLine();
}
Некоторые поточно-безопасные подходы:
private static object _lockObject;
// ...
// Do not allow concurrency, blocking
for (int i = 0; i < 10000000; i++)
{
lock (_lockObject)
{
// original code
}
}
// Do not allow concurrency, non-blocking
for (int i = 0; i < 10000000; i++)
{
bool lockTaken = false;
try
{
Monitor.TryEnter(_lockObject, ref lockTaken);
if (lockTaken)
{
// original code
}
}
finally
{
if (lockTaken) Monitor.Exit(_lockObject);
}
}
// Do not allow concurrency, non-blocking
for (int i = 0; i < 10000000; i++)
{
// Only one thread at a time will succeed in exchanging the value
try
{
int previousState = Interlocked.CompareExchange(ref _state, 4, 3);
if (previousState == 3)
{
// Allow race condition on purpose (for no reason)
int currentState = Interlocked.CompareExchange(ref _state, 0, 0);
if (currentState != 4)
{
// This branch is never taken
Console.Write(currentState);
}
}
}
finally
{
Interlocked.CompareExchange(ref _state, 3, 4);
}
}
// Allow concurrency
for (int i = 0; i < 10000000; i++)
{
// All threads increment the value
int currentState = Interlocked.Increment(ref _state);
if (currentState == 4)
{
// But still, only one thread at a time enters this branch
// Allow race condition on purpose (it may actually happen here)
currentState = Interlocked.CompareExchange(ref _state, 0, 0);
if (currentState != 4)
{
// This branch might be taken with a maximum value of 3 + N
Console.Write(currentState);
}
}
Interlocked.Decrement(ref _state);
}
Этот бит немного отличается, он принимает последнее известное значение _state
после приращения для выполнения чего-то:
// Allow concurrency
for (int i = 0; i < 10000000; i++)
{
// All threads increment the value
int currentState = Interlocked.Increment(ref _state);
if (currentState != 4)
{
// Only the thread that incremented 3 will not take the branch
// This can happen indefinitely after the first increment for N > 1
// This branch might be taken with a maximum value of 3 + N
Console.Write(currentState);
}
Interlocked.Decrement(ref _state);
}
Обратите внимание, что примеры Interlocked.Increment
/Interlocked.Decrement
небезопасны, в отличие от примеров lock
/Monitor
и Interlocked.CompareExchange
, поскольку нет надежного способа узнать, было ли приращение успешным или нет.
Один общий подход заключается в том, чтобы увеличивать, а затем следовать с try
/finally
, где вы уменьшаетесь в блоке finally
. Однако может быть выбрано асинхронное исключение (например, ThreadAbortException
)
Асинхронные исключения могут быть выброшены в неожиданные местоположения, возможно, каждая машинная инструкция: ThreadAbortException, StackOverflowException и OutOfMemoryException.
Другой подход заключается в инициализации currentState
чем-то ниже 3 и условном уменьшении в блоке finally
. Но опять же, между Interlocked.Increment
return и currentState
назначается результат, может возникнуть асинхронное исключение, поэтому currentState
может все еще иметь начальное значение, даже если Interlocked.Increment
преуспел.