首页 > 解决方案 > 在这种情况下,不可变变量线程安全吗?

问题描述

考虑以下代码示例,该变量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被认为是线程安全的吗?

标签: c#multithreadingimmutability

解决方案


要回答您的更新,不,代码不会是线程安全的。您正在读取 i,增加值,然后写入 i。这三个步骤不是一个逻辑单元,其他线程可以在读写步骤之间与 i 交互。

例如,您有三个线程,AB 和 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)

人们说不可变值是线程安全的原因是他们指的是将引用的副本传递给另一个函数,一旦创建了该引用的副本,您就无需担心另一个线程会在您更改该对象的值时正在使用它。但是,如果您没有制作引用的副本(例如在您的示例中,您在函数范围之外使用了共享变量),您就会遇到线程之间使用的引用的非不变性问题。

简单来说,值 10 是不可变的,同名的变量i不是。如果您跨线程共享变量(我不是在谈论变量具有的对象引用/值,而是变量本身),那么您正在使用一个可变对象。


推荐阅读