这是线程安全的吗?

时间:2009-10-09 07:05:26

标签: c# .net thread-safety

据我所知,以下代码是线程安全的,但需要注意的是我不是在寻找单例模式(我不关心多个线程是否因为MyObject是不可变的而获得MyObject的不同实例。创建多个实例的并发线程之间的潜在争用,后续线程将获得相同的intance)。

private static MyObject myObject;
...
public static MyObject GetMyObject()
{
    if (myObject == null)
    {
        myObject = new MyObject(...);
    }
    return myObject;
}

以下显然不是线程安全的,因为多个线程可能会在完全初始化之前尝试访问myObject字段:

private static MyObject myObject;
...
public static MyObject GetMyObject()
{
    if (myObject == null)
    {
        myObject = new MyObject();
        myObject.Initialize(...);
    }
    return myObject;
}

以下是线程安全的,还是myObject字段需要volatile关键字? 看起来是安全的,因为在对象完全初始化之前不会分配myObject字段。但我担心JITter可能能够内联LoadMyObject方法并基本上重新实现它作为上面的代码,这不是线程安全的。

private static MyObject myObject;
...
private static MyObject LoadMyObject()
{
    MyObject myObject = new MyObject();
    myObject.Initialize(...);
    return myObject;
}

public static MyObject GetMyObject()
{
    if (myObject == null)
    {
        myObject = LoadMyObject();
    }
    return myObject;
}

上面的背景是我想重构一些多线程代码,并通过调用工厂方法替换对构造函数的调用。我想知道这样做是否会破坏线程安全。

修改

重申一下,我不关心是否创建了多个MyObject实例,只是没有办法访问未完全初始化的实例。作为一个具体的例子,想一下加载了配置文件内容的只读XmlDocument。我想要一个对内存中可用的XmlDocument的静态引用,以避免在每次访问时从磁盘加载的开销。但我不在乎两个线程是否碰巧并发运行并且都从磁盘加载文件。

EDIT2

如果我理解正确,C#语言规范中的以下语句似乎暗示JITter不会重新排序我的代码,因此我的上一个样本应该是线程安全的。我理解正确吗?

  

3.10执行顺序   ...   数据依赖保留在一个   执行的线程。那就是   每个变量的值计算为   如果线程中的所有语句都是   以原始程序顺序执行

9 个答案:

答案 0 :(得分:2)

在某些情况下,易失性可能是一种性能选择,以避免使用锁的额外命中,但它不能解决竞争条件的线索安全问题,这是你的第三个例子(有问题的例子)。

为确保未初始化的实例未被另一个线程检索,在完全初始化实例之前,不应将其分配给静态变量。然后,一旦他们看到非空,他们就知道这很好。 已编辑 :(更正)虽然我认为避免冗余工作更为理想(下图),但我认为允许重新生成和替换静态参考(如您所知,或Guffa的版本)仍然是“安全的” “对于你的用例,因为每次引用GetMyObject()都会抓住当时的单个对象。

我会建议这样的事情:

private static object s_Lock = new object();
private static volatile MyObject s_MyObject;
private static MyObject LoadMyObject()
{
    MyObject myObject = new MyObject();
    myObject.Initialize();
    return myObject;
}
public static MyObject GetMyObject()
{
    if (s_MyObject == null)
    {
        lock (s_Lock)
        {
            if (s_MyObject == null) // Check again now that we're in the lock.
                s_MyObject = LoadMyObject(); // Only one access does this work.
        }
    }
    return s_MyObject; // Once it's set, it's never cleared or assigned again.
}

这样做的好处是只执行一次init工作,但也避免了锁争用开销,除非它实际需要......同时仍然是线程安全的。 (你可以使用如上所述的volatile,如果你想确保它传播出去,但是如果没有它,它也应该“工作”;在看到s_MyObject的新内容之前,它更可能需要锁定,这取决于在架构上。所以我认为volatile对这种方法很有帮助。)

我保留了LoadMyObjectGetMyObject方法,但是如果是单个工厂方法,你也可以将逻辑组合到内部的then子句中。

答案 1 :(得分:1)

在某种意义上说,你将获得一个正确初始化的对象是安全的,但是有几个线程可能同时创建对象,而一个线程可能会返回一个刚刚创建的另一个线程的对象。

如果编译器内联该方法,它仍然不会与第一个代码相同。由于对象的引用保存在局部变量中,直到它被初始化,它仍然是安全的。

您可以自己内联方法,不需要在单独的方法中使用代码来确保安全:

public static MyObject GetMyObject() {
   if (myObject == null) {
      MyObject newObject = new MyObject();
      newObject.Initialize(...);
      myObject = newObject;
   }
   return myObject;
}

如果要防止由不同的线程创建多个对象,则需要使用锁。

答案 2 :(得分:1)

要回答第二次编辑,如果初始化方法需要一段时间,如果对象不为null,则有可能另一个线程可能为k,然后拉出其未初始化状态。我建议你使用一个锁进行空检查和初始化,以保证只在完全初始化时才检索对象。

如果您希望它完全是线程安全的,这可以提供帮助:

private static MyObject myObject;
private object lockObj = new object();
...
public static MyObject GetMyObject()
{
    lock(lockObj)
    {
        if (myObject == null)
        {
            myObject = new MyObject(...);
        }
    }
    return myObject;
}

答案 3 :(得分:1)

  

但我会担心JITter   也许可以内联   LoadMyObject方法和本质上   重新实现它作为上面的代码,   这不是线程安全的。

即使该方法是内联的,您的本地也不会被优化掉,并且对该字段的写入仍然只会发生一次。

所以你的最后一个例子是线程安全的

作为一项规则,当对变量有多个访问权限时,本地人不会被优化并被全局变量替换。

答案 4 :(得分:0)

如果我可以将clippy传达一分钟:“看起来你正试图实现Singleton模式”。请参阅Jon Skeet's canonical article on the subject

答案 5 :(得分:0)

为什么不将Initialize方法移动到MyObject的c'tor并通过一次调用初始化它?

答案 6 :(得分:0)

即使没有编译器优化,它仍然不是线程安全的。在这种情况下,易失性无济于事 - 它仅用于保护变量不被“隐藏”修改。想象一下两个线程在同一时间通过空检查 - 你最终会调用两次“LoadMyObject”,在这种情况下可能没问题,或者可能没有。这是TOCTOU错误(检查时间/使用时间)。您基本上必须使整个GetMyObject方法的主体安全,包括检查(即通过使该调用同步)。

答案 7 :(得分:0)

以下是我的看法:

public class TestClass
{
    private static TestClass instance = LoadObject();

    private TestClass(){ }

    private static TestClass LoadObject()
    {
        var t = new TestClass();
        //do init

        return t;
    }

    public static  TestClass GetObject()
    {
        return instance;
    }
}

答案 8 :(得分:0)

在我看来,这似乎是一种优化。而不是深入了解编译器正在做的事情的微妙之处,而是选择最合适的一个:

  • 一个实例(单身)
  • 每个线程一个实例(线程静态)
  • 每个来电者一个实例(根本没有特殊代码)

这一切都取决于你想要支付的罚款。对于单身人士,您需要支付性能损失。对于其他两个选项,您需要支付内存罚金。

要使其成为线程静态,请执行以下操作:

[ThreadStatic]
private static MyObject myObject;
...
public static MyObject GetMyObject()
{
    if (myObject == null)
    {
        myObject = new MyObject(...);
    }
    return myObject;
}