C# 替换成员变量的值是线程安全的吗?

C# 替换成员变量的值是线程安全的吗?,c#,multithreading,thread-safety,C#,Multithreading,Thread Safety,在我的应用程序(用C#编写)中,我有一个类的实例,其中一个成员变量指向另一个类的实例。第二个实例是只读的,因此该实例的状态在创建后永远不会更改。但是在某些情况下,我想用一个新的和更新的实例替换这个实例。在多线程环境中,在第一个实例中替换此引用是否安全?或者这会导致问题吗?引用替换分别是32位环境中的32位整数写入操作和64位环境中的64位整数写入操作。写操作实际上分两步执行: 读取新值 写一个新值 我认为默认情况下它不是线程安全的不,它不是线程安全的,因为缓存(某些体系结构需要这样的内存屏障

在我的应用程序(用C#编写)中,我有一个类的实例,其中一个成员变量指向另一个类的实例。第二个实例是只读的,因此该实例的状态在创建后永远不会更改。但是在某些情况下,我想用一个新的和更新的实例替换这个实例。在多线程环境中,在第一个实例中替换此引用是否安全?或者这会导致问题吗?

引用替换分别是32位环境中的32位整数写入操作和64位环境中的64位整数写入操作。写操作实际上分两步执行:

  • 读取新值
  • 写一个新值

我认为默认情况下它不是线程安全的

不,它不是线程安全的,因为缓存(某些体系结构需要这样的内存屏障)、编译器优化和并发访问。
您有以下选择:

  • 将该字段声明为volatile
  • 使用
    Interlocked.Exchange()
    替换该值
阅读MSDN文档,选择更适合您的(快速:volatile应该更快,但您不能在一个原子操作中保存旧值并写入新值)

已编辑
我想我必须弄清楚我的意思(许多人似乎认为原子是线程安全的同义词):

在多线程环境中,在第一个实例中替换此引用是否安全

不,严格地说它不是线程安全的。你不知道会发生什么,所以不安全

或者这会引起问题吗


这取决于上下文。如果一个线程不能使用正确的套接字连接(或闭合的),那么这将是一个大问题。如果一个线程不使用正确的颜色格式化其输出,那么它可能不会成为问题。

简单的写入操作本身是线程安全的

所以这没关系:

x.y = new Y();   // OK
但以下情况并非如此:

if (x.y == null)
    x.y = new Y();   // might overwrite a non-null x.y
因此,这取决于您想要如何使用它,以及线程的期望。但是原子性意味着在
x.y

中永远不会有无效的(半写的)引用是的,它是“安全的”,因为引用读/写是原子的。您的客户端代码将始终获得有效实例


显然,您不能保证客户机代码将立即获得新实例

首先替换此引用是否安全 多线程环境

很抱歉来到这里,但这取决于您如何定义“安全”

  • 它是安全的,因为它不会腐蚀国家。这是因为引用分配是原子的
  • 这是不安全的,因为它可能导致意外行为
意外行为可分为三大类

  • 先读后写的组合可能导致竞争条件
  • 由于编译器和硬件优化,一个线程可能无法观察到另一个线程所做的更改
  • 一个线程可以观察到参考变量的两个不同读数之间的变化
案例#1:因此在第一种情况下,两个线程可能会在虚假的借口下竞相分配任务。最好的例子是创建一个单例

public static Singleton GetSingleton()
{
  // More than one thread could read null and attempt instantiation here.
  if (instance == null)
  {
    instance = new Singleton();
  }
  return instance;
} 
Case#2:在第二种情况下,一个线程可以继续读取旧实例,即使另一个线程发布了新实例。考虑这个例子。

public class Example
{
  private SuperHero hero = new Superman();

  void ThreadA()
  {
    while (true)
    {
      Thread.Sleep(1000);
      hero = new CaptainAmerica();
      Thread.Sleep(1000);
      hero = new GreenLantern();
      Thread.Sleep(1000);
      hero = new IronMan();
    }

  void ThreadB()
  {
    while (true)
    {
      hero.FightTheBadGuys();
    }
  }
}
问题是编译器或硬件可能会通过在循环外部“提升”对
hero
的读取来优化
ThreadB
,如下所示

void ThreadB()
{
  SuperHero local = hero;
  while (true)
  {
    local.FightTheBadGuys();
  }
}
不难看出,我们的一位超级英雄会感到非常疲惫。再说一次,他是一个超级英雄,所以可能根本没有问题。我想在这里指出的一点是,根据您的情况,这个陈旧的工件可能是问题,也可能不是问题

情况#3:在第三种情况下,线程可能错误地认为变量将在两次读取之间引用同一实例

void SomeThread()
{
  var result1 = instance.DoSomething();
  // The variable instance could get changed here!
  var result2 = instance.DoSomethingElse();
  // Are result1 and result2 in a mutually consistent state here?
}

在上述示例中,
result1
result2
可能不处于相互一致的状态。这是因为它们是从在两个不同实例上运行的操作派生的。同样,这可能是问题,也可能不是问题,具体取决于您的具体场景。

在要使用新实例更新对象的地方,请使用以下代码。这是线程安全代码

MyClass obj = null;
Interlocked.CompareExchange(ref obj, new MyClass(), null);

引用分配是原子的。线程安全并不意味着原子。一条指令可以是原子的,但不是线程安全的。一个线程安全的代码块可能不是(也可能不是)原子的。即使是一个简单的赋值(例如在IA64上)也不是线程安全的,而且因为可能的编译器优化。“显然,你不能保证客户端代码会立即获得新实例”,那么你怎么能说它是线程安全的呢?这比例外情况更危险!因为代码是否仍然有一个旧实例可能无关紧要。即使使用volatile,客户机代码也可以缓存或临时复制引用。这种线程同步不在问题的范围之内,也在问题的范围之内。线程安全至少意味着代码实现了预期的功能。要调用事件处理程序,通常需要将实例复制为线程安全的,但如果访问另一个对象,则不是线程安全的,因为程序可能会执行一些意外操作(如果在分配后对象被释放怎么办?)。对于volatile,至少编译器(或CPU架构的一些肮脏细节)不会这样做(因此用户对此负责)。正如我在回答中所说的,代码可能会得到旧值,但它不会因为引用替换而“爆炸”。OP基本上是询问re是否会发生撕裂