在这种情况下不可变变量线程安全吗?
Is immutable variable thread safe in this case?
考虑以下代码示例,变量 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;
}
}
更新:
我根据之前不清楚的评论更新了代码。现在如果 class Test
由客户端发起一次并且 Run
方法被多个线程调用怎么办。 i
会被认为是线程安全的吗?
为了回答您的更新,不,代码不是线程安全的。您正在读取 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 个操作不是 "atomic" 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)
人们说不可变值是线程安全的原因是他们指的是将引用的副本传递给另一个函数,一旦创建了该引用的副本,您就不必担心另一个线程会更改该值当您使用该对象时。但是,如果您没有制作引用的副本(例如在您的示例中,您在函数范围之外使用了共享变量),那么您 运行 就会遇到您正在使用的引用的不可变性问题线程之间。
简单来说,值 10 是不可变的,名称为 i
的变量不是。如果你跨线程共享变量(我不是在谈论对象refrence/value变量而是变量本身)那么你正在使用一个可变对象。
考虑以下代码示例,变量 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;
}
}
更新:
我根据之前不清楚的评论更新了代码。现在如果 class Test
由客户端发起一次并且 Run
方法被多个线程调用怎么办。 i
会被认为是线程安全的吗?
为了回答您的更新,不,代码不是线程安全的。您正在读取 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 个操作不是 "atomic" 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)
人们说不可变值是线程安全的原因是他们指的是将引用的副本传递给另一个函数,一旦创建了该引用的副本,您就不必担心另一个线程会更改该值当您使用该对象时。但是,如果您没有制作引用的副本(例如在您的示例中,您在函数范围之外使用了共享变量),那么您 运行 就会遇到您正在使用的引用的不可变性问题线程之间。
简单来说,值 10 是不可变的,名称为 i
的变量不是。如果你跨线程共享变量(我不是在谈论对象refrence/value变量而是变量本身)那么你正在使用一个可变对象。