考虑下面的代码示例,变量i
是不可变的int
类型。递增i
后,新值将存储在同一存储位置。因此,如果正在从该位置读取多个线程,则在进行写入时,其中一些线程可能会损坏数据。那么这个不可变类型线程如何安全?有内部的CLR逻辑可以解决这个问题吗?
public class Test
{
int i = 10;
public unsafe int Run()
{
fixed (int* ip = &i)
{
Console.WriteLine($"address of i before updation: {((IntPtr)ip).ToString()}");
}
i = i + 1;
fixed (int* ipNew = &i)
{
Console.WriteLine($"address of i after updation: {((IntPtr)ipNew).ToString()}");
}
return i;
}
}
更新:
我根据之前的评论(基于注释)更新了代码。现在,如果类Test
由客户端启动一次并且Run
方法由多线程调用,该怎么办? i
被认为是线程安全的吗?
答案 0 :(得分:1)
要回答您的更新,没有代码不是线程安全的。您正在读取i,递增值,然后写入i。这三个步骤不是逻辑单元,其他线程可以在读写步骤之间与i交互。
例如,您有三个线程,即A B和C。某些事情使B的运行速度比其他线程慢。
A: Read i to thread local memory location A the value 10
B: Read i to thread local memory location B the value 10
A: Add 1 to thread local memory location A
A: Write 11 to i from thread local memory location A
B: Add 1 to thread local memory location B
C: Read i to thread local memory location C the value 11
C: Add 1 to thread local memory location C
C: Write 12 to i from thread local memory location C
B: Write 11 to i from thread local memory location B
由于这3个操作不能在3个线程之间“原子”地完成B所必须执行的3个步骤之间的工作,所以这将导致您得出错误的结束值。
处理此问题的正常方法是锁定3个操作,因此只有一个线程可以一次执行该操作,
lock(someObject)
{
i = i + 1;
}
使用使操作原子化的工具
Interlocked.Increment(ref i);
或检查i的值在读取的开始与您要执行的写入之间是否未更改,并且如果确实更改了,请重试该操作。
int iOriginal, iNew;
do
{
iOriginal = i;
iNew = iOriginal + 1;
} while(iOriginal != Interlocked.CompareExchange(ref i, iNew, iOriginal)
人们说不可变值是线程安全的原因是,它们是指将引用的副本传递给另一个函数,一旦创建了该引用的副本,您就不必担心另一个线程会更改该值。对象,而您正在使用它。但是,如果不复制参考(例如在示例中使用了函数范围之外的共享变量),则会遇到线程间使用的参考不可变性的问题。 / p>
简单来说,值10是不可变的,名称为i
的变量不是。如果跨线程共享变量(我不是在谈论变量的对象引用/值,而是变量本身),那么您正在使用可变对象。