C# 替换成员变量的值是线程安全的吗?
在我的应用程序(用C#编写)中,我有一个类的实例,其中一个成员变量指向另一个类的实例。第二个实例是只读的,因此该实例的状态在创建后永远不会更改。但是在某些情况下,我想用一个新的和更新的实例替换这个实例。在多线程环境中,在第一个实例中替换此引用是否安全?或者这会导致问题吗?引用替换分别是32位环境中的32位整数写入操作和64位环境中的64位整数写入操作。写操作实际上分两步执行:C# 替换成员变量的值是线程安全的吗?,c#,multithreading,thread-safety,C#,Multithreading,Thread Safety,在我的应用程序(用C#编写)中,我有一个类的实例,其中一个成员变量指向另一个类的实例。第二个实例是只读的,因此该实例的状态在创建后永远不会更改。但是在某些情况下,我想用一个新的和更新的实例替换这个实例。在多线程环境中,在第一个实例中替换此引用是否安全?或者这会导致问题吗?引用替换分别是32位环境中的32位整数写入操作和64位环境中的64位整数写入操作。写操作实际上分两步执行: 读取新值 写一个新值 我认为默认情况下它不是线程安全的不,它不是线程安全的,因为缓存(某些体系结构需要这样的内存屏障
- 读取新值
- 写一个新值
我认为默认情况下它不是线程安全的不,它不是线程安全的,因为缓存(某些体系结构需要这样的内存屏障)、编译器优化和并发访问。
您有以下选择:
- 将该字段声明为volatile
- 使用
替换该值Interlocked.Exchange()
我想我必须弄清楚我的意思(许多人似乎认为原子是线程安全的同义词): 在多线程环境中,在第一个实例中替换此引用是否安全 不,严格地说它不是线程安全的。你不知道会发生什么,所以不安全 或者这会引起问题吗
这取决于上下文。如果一个线程不能使用正确的套接字连接(或闭合的),那么这将是一个大问题。如果一个线程不使用正确的颜色格式化其输出,那么它可能不会成为问题。简单的写入操作本身是线程安全的 所以这没关系:
x.y = new Y(); // OK
但以下情况并非如此:
if (x.y == null)
x.y = new Y(); // might overwrite a non-null x.y
因此,这取决于您想要如何使用它,以及线程的期望。但是原子性意味着在x.y
中永远不会有无效的(半写的)引用是的,它是“安全的”,因为引用读/写是原子的。您的客户端代码将始终获得有效实例
显然,您不能保证客户机代码将立即获得新实例 首先替换此引用是否安全 多线程环境 很抱歉来到这里,但这取决于您如何定义“安全”
- 它是安全的,因为它不会腐蚀国家。这是因为引用分配是原子的
- 这是不安全的,因为它可能导致意外行为
- 先读后写的组合可能导致竞争条件
- 由于编译器和硬件优化,一个线程可能无法观察到另一个线程所做的更改
- 一个线程可以观察到参考变量的两个不同读数之间的变化
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是否会发生撕裂