我正在创建两个线程,并向它们传递一个函数,该函数执行下面显示的代码,10,000,000次。
大多数情况下,“5”会打印到控制台。有时它是“3”或“4”。很清楚为什么会发生这种情况。
但是,它也打印“6”。这怎么可能?
class Program
{
private static int _state = 3;
static void Main(string[] args)
{
Thread firstThread = new Thread(Tr);
Thread secondThread = new Thread(Tr);
firstThread.Start();
secondThread.Start();
firstThread.Join();
secondThread.Join();
Console.ReadLine();
}
private static void Tr()
{
for (int i = 0; i < 10000000; i++)
{
if (_state == 3)
{
_state++;
if (_state != 4)
{
Console.Write(_state);
}
_state = 3;
}
}
}
}
这是输出:
答案 0 :(得分:44)
我认为我已经找出导致此问题的事件序列:
主题1输入if (_state == 3)
上下文切换
线程2输入if (_state == 3)
线程2递增状态(state = 4
)
上下文切换
线程1 读取 _state
为4
上下文切换
线程2设置_state = 3
线程2输入if (_state == 3)
上下文切换
线程1执行_state = 4 + 1
上下文切换
线程2将_state
读为5
线程2执行_state = 5 + 1
;
答案 1 :(得分:17)
这是典型的race condition。编辑:事实上,有多种竞争条件。
它可能发生在_state
为3并且两个线程都刚刚超过if
语句的任何时间,可以通过单个内核中的上下文切换同时进行,也可以在多个内核中同时并行进行。< / p>
这是因为++
运算符首先读取_state
然后递增它。在第一个if
语句之后,有可能会有足够的时间让它保持5分甚至6分。
编辑:如果你为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)
。
如果你想保持竞争条件,你应该declare _state
as volatile
,否则你会冒险在每个线程本地看到_state
而没有来自其他线程的更新。
或者,您可以使用System.Threading.Volatile.Read
和System.Threading.Volatile.Write
,以防您将实现切换为_state
作为变量,Tr
作为捕获该变量的闭包,因为局部变量不能(and won't be able to be)声明volatile
。在这种情况下,即使初始化也必须使用易失性写入。
编辑:如果我们通过扩展每一次读取来稍微改变代码,也许竞争条件会更明显:
// 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次,每次打印一次,就像 op 发布的图像一样。请记住,线程可能无法同步运行,通常是由于抢先上下文切换,缓存停顿或核心速度差异(由于省电或临时turbo速度):
这个类似于原始版本,但它使用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
块中递减。但是,an asynchronous exception might be thrown (e.g. ThreadAbortException
)
异步异常可能会抛出到意外的位置,可能是每个机器指令:ThreadAbortException,StackOverflowException和OutOfMemoryException。
另一种方法是将currentState
初始化为低于3的值并在finally
块中有条件地递减。但同样,在Interlocked.Increment
返回和currentState
分配给结果之间,可能会发生异步异常,因此即使currentState
成功,Interlocked.Increment
仍可能具有初始值