静态方法内的局部变量是否线程安全?

29

如果我有一个静态类和一个静态方法,如果多个线程同时调用该方法,方法内的局部变量是否安全?

static class MyClass {

  static int DoStuff(int n) {

    int x = n; // <--- Can this be modified by another thread?

    return x++;     
  }
}
1个回答

66

这个问题的答案声称局部变量被存储在堆栈上,因此是线程安全的,这是不完整且可能非常错误的。

当执行静态方法时,线程是否创建自己的作用域?

你的问题包含一个常见的错误。在C#中,“作用域”仅是编译时概念;“作用域”是一个程序文本区域,在这个区域中一个特定的实体(如变量或类型)可以通过其未限定的名称引用。作用域有助于确定编译器如何将名称映射到名称所表示的概念。

线程在运行时不会创建“作用域”,因为“作用域”是纯粹的编译时概念。

局部变量的作用域与其生命周期松散相关。粗略地说,局部变量的运行时生命周期通常从控制线程进入对应作用域的代码开始,并在控制线程离开时结束。但是,如果编译器和运行时认为这样做是有效的或必要的,它们都给予了相当大的自由裁量权来缩短或延长该生存周期。

特别地,迭代器块中的局部变量和匿名函数中闭合的局部变量的生命周期会被延长到控制线程离开作用域之后。

然而,这些都与线程安全无关。因此,让我们放弃这个表述不当的问题并转而讨论一个更好的表述:

如果我有一个静态类和一个静态方法,如果多个线程在调用它,该方法内的实例变量是否安全?

你的问题有一个错误。实例变量是非静态字段。显然,在静态类中没有非静态字段。你将实例变量和局部变量混淆了。你想要问的问题是:

如果我有一个静态类和一个静态方法,如果多个线程在调用它,该方法内的局部变量是否安全?

我不直接回答这个问题,而是将其重新表述为两个更易回答的问题。

在什么情况下需要使用锁定或其他特殊的线程安全技术来确保对变量的安全访问?

如果有两个线程都可以访问变量,其中至少一个线程正在修改它,并且至少一个线程正在执行某些非原子操作,则需要这样做。

(我注意到可能还有其他因素在起作用。例如,如果您需要从每个线程看到的共享内存变量的一致观察值,即使所有操作都是原子的,您也需要使用特殊技术。C# 不能保证标记为volatile的变量的顺序一致性观察。)

太好了。让我们集中精力在“两个线程都可以访问变量”部分。在什么情况下,两个线程可以同时访问局部变量?

典型情况下,局部变量只能在声明它的方法中访问。每个方法激活将创建一个不同的

static class Foo
{
    private static Func<int> f;
    public static int Bar()
    {
        if (Foo.f == null)
        {
           int x = 0;
           Foo.f = ()=>{ return x++; };
        }
        return Foo.f();
    }
}

在这里,“Bar”有一个本地变量“x”。如果在多个线程上调用Bar,则首先竞争的线程会决定谁设置Foo.f。其中一个胜出。从现在开始,对多个线程上的Bar的调用都不安全地操作由获胜线程创建的委托捕获的相同本地变量x。

作为本地变量并不能保证线程安全。

第三,迭代器块中的本地变量也有同样的问题:

static class Foo
{
    public static IEnumerable<int> f;
    private static IEnumerable<int> Sequence()
    {
        int x = 0;
        while(true) yield return x++;
    }
    public static Bar() { Foo.f = Sequence(); }
}
如果有人调用Foo.Bar(),并从两个不同的线程访问Foo.f,那么单个本地变量x可能会在两个不同的线程上不安全地被改变。(当然,运行迭代器逻辑的机制也不是线程安全的。)
第四,对于标记为“不安全”的代码,本地变量可以通过在多个线程之间共享指向该本地变量的指针而被共享。如果将代码块标记为“不安全”,则您需要确保如果需要的话代码是线程安全的。除非您知道自己在做什么,否则不要关闭安全系统。

哦,我不理解你最后一个有关迭代器的示例。两个线程都不会获得一个新的IEnumerator<int>实例吗?该实例封装了模拟迭代器方法的状态机的私有实例?此外,我在这里尝试模拟:http://paste2.org/p/1610193 我一定是误解了什么,对我的无知表示抱歉。 - chrisaut
1
@Steven:如果在两个不同的线程上调用Bar,则每个线程都会获得自己的状态机。如果从一个线程调用Bar,然后两个不同的线程访问迭代器,则Bar的本地变量x将不安全地被两个不同的线程访问。我的观点是,仅仅因为一个变量是本地的,并不意味着它自动是线程安全的。 - Eric Lippert
@EricLippert:关于您的声明“如果您需要共享内存变量的一致观察值...”假设我在方法中调用Task.Factory.StartNew(() => DoSomethingWith(value_type_local_var, reference_type_local_var)),那么这个声明是否意味着如果我想确保后台线程看到与主线程相同的内存,我应该锁定?如果主线程在调用后台线程后改变或不改变局部变量,是否有任何不同? - Buu
总之,如果该方法没有ref/out参数,并且未访问方法定义块外定义的任何内容,则对该方法的每次调用的激活记录将实现其自己版本的在方法内定义的局部变量。正确吗? - Frank Thomas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接