当使用只有静态方法而没有变量的C#类时,是否会出现并发问题

本文关键字:是否 类时 并发 问题 静态方法 变量 | 更新日期: 2023-09-27 18:19:31

我是否正确理解了所有线程都在自己的堆栈中有方法变量的副本,这样当从不同线程调用静态方法时就不会出现问题?

当使用只有静态方法而没有变量的C#类时,是否会出现并发问题

是和否。如果参数是值类型,那么是的,它们有自己的副本。或者,如果引用类型是不可变的,那么它就不能更改,也就没有问题了。然而,如果参数是可变的引用类型,那么传入的参数仍然可能存在线程安全问题

这有道理吗?如果将引用类型作为参数传递,则它的引用是"按值"传递的,因此它是一个引用回旧对象的新引用。因此,两个不同的线程可能会以非线程安全的方式更改同一对象。

如果这些实例中的每一个都是在使用它们的线程中创建并仅使用的,那么您获得bit的可能性很低,但我只想强调的是,仅仅因为您使用的是只有locals/参数的静态方法,并不能保证线程安全性(当然,Chris提到的实例也是如此)。

我是否正确地理解了所有线程都在自己的堆栈中有方法变量的副本,这样当从不同的线程调用静态方法时就不会有问题了?

没有。

首先,"所有线程在自己的堆栈中都有一个方法的局部变量的副本"是错误的。只有当局部变量的生存期较短时,才会在堆栈上生成;如果局部变量(1)在外部变量上闭合,(2)在迭代器块中声明,或(3)在异步方法中声明,则局部变量可能具有任意长的生存期。

在所有这些情况下,通过激活一个线程上的方法创建的局部变量稍后可以被多个线程突变。这样做是不安全的。

其次,当从不同的线程调用静态方法时,可能会出现很多问题。有时在堆栈上分配局部变量这一事实并不能神奇地使静态方法对共享内存的访问突然正确起来。

当使用只有静态方法而没有变量的C#类时,是否会出现并发问题?

我假设你的意思是"没有静态变量",而不是"没有局部变量"。

绝对有。例如,这里有一个程序,没有静态变量,没有非静态方法,没有在第二个线程之外创建的对象,只有一个局部变量来保存对该线程的引用。除了cctor之外,没有任何其他方法实际上做任何事情这个程序死锁你不能因为你的程序非常简单就认为它不包含线程错误!

读者练习:描述为什么这个看起来不包含锁的程序实际上会死锁。

class MyClass
{
  static MyClass() 
  {
      // Let's run the initialization on another thread!
      var thread = new System.Threading.Thread(Initialize);
      thread.Start();
      thread.Join();
  }
  static void Initialize() 
  { /* TODO: Add initialization code */ }
  static void Main() 
  { }
}

听起来你正在寻找某种神奇的方式来知道你的程序没有线程问题。除了让它成为单线程之外,没有什么神奇的方法可以知道这一点。您必须分析线程和共享数据结构的使用情况。

除非所有变量都是不可变的引用类型或值类型,否则没有这样的保证。

如果变量是可变的引用类型,则需要执行适当的同步。

EDIT:只有在线程之间共享可变变量时,才需要同步可变变量——不需要同步未在方法外部公开的本地声明的可变变量。

是的,除非方法只使用局部范围变量,而没有任何全局变量,所以任何方法都不会影响任何对象的状态,如果这是true,那么在多线程中使用它就没有问题。我想说,即使在这种情况下,static他们与否也不相关。

如果它们是方法的局部变量,那么是的,您无需担心。只需确保您没有通过引用传递参数,也没有访问全局变量并在不同的线程中更改它们。那你就有麻烦了。

static方法可以引用static字段中的数据——无论是在其类中还是在其类外——这可能不是线程安全的。

所以最终你的问题的答案是"不",因为可能会有问题,尽管通常不会。

两个线程应该仍然能够对同一个对象进行操作,要么通过将对象作为参数传递给不同线程上的方法,要么如果可以通过Singleton等全局访问对象,则所有赌注都将取消

标记

作为关于为什么静态方法不一定是线程安全的答案的补充,值得考虑为什么它们可能是,以及为什么它们经常是。

我认为,它们可能是的第一个原因是,你所想到的那种情况:

public static int Max(int x, int y)
{
  return x > y ? x : y;
}

这个纯函数是线程安全的,因为它无法影响任何其他线程上的代码,局部xy对它们所在的线程保持本地,不会存储在共享位置、在委托中捕获或以其他方式离开纯本地上下文。

值得注意的是,线程安全操作的组合可能是非线程安全的(例如,对并发字典是否有一个键进行线程安全读取,然后对该键的值进行线程安全的读取,这不是线程安全的,因为状态可能在这两个线程安全操作之间发生变化)。为了避免这种情况,静态成员往往不是可以以这种非线程安全的方式组合的成员。

静态方法也可以保证它自己的线程安全:

public object GetCachedEntity(string key)
{
  object ret;  //local and remains so.
  lock(_cache) //same lock used on every operation that deals with _cache;
    return _cache.TryGetValue(key, out ret) ? ret : null;
}

或:

public object GetCachedEntity(string key)
{
  object ret;
  return _cache.TryGetValue(key, out ret) ? ret : null; //_cache is known to be thread-safe in itself!
}

当然,在这里,这与实例成员没有什么不同,它保护自己免受其他线程的破坏(通过与处理它们共享的对象的所有其他代码协同操作)。

不过,值得注意的是,静态成员是线程安全的,而实例成员不是线程安全的是非常常见的。FCL的几乎每个静态成员都保证文档中的线程安全,几乎每个实例成员都不禁止某些专门为并发使用而设计的类(即使在某些情况下,实例成员实际上是线程安全的)。

原因有两个:

  1. 对静态成员最常用的操作是纯函数(例如,Math类的大多数静态成员)或读取其他线程不会更改的静态只读变量。

  2. 很难将自己的同步带到第三方的静态成员中。

第二点很重要。如果我有一个对象的实例成员不是线程安全的,那么假设调用不会影响不同实例之间共享的非线程安全数据(可能,但几乎肯定是一个糟糕的设计),那么如果我想在线程之间共享它,我可以提供我自己的锁定来实现这一点。

然而,如果我处理的是线程不安全的静态成员,那么我就很难做到这一点。事实上,考虑到我可能不仅使用自己的代码,而且使用其他方的代码,这可能是不可能的。这将使任何这样的公共静态成员几乎毫无用处。

具有讽刺意味的是,静态成员倾向于线程安全的原因并不是使它们更容易实现(尽管这确实涵盖了纯函数),而是更难实现!事实上,这太难了,以至于代码的作者必须为用户做这件事,因为用户无法自己做。