静态实例不在线程之间共享

static instance is not shared between threads

我正在使用一个单例 class,它假设有一个静态实例,如下所示:

    private static ISingletonClass _instance = null;

    public static ISingletonClass GetInstance(string id = null)
    {
        if (_instance == null)
        {
            if (id != null)
            {
                _instance = new SingletonClass(id);
            }
            else
            {
                throw new NullReferenceException("id is missing!");
            }
        }

        if (id != null && _instance.Id != id)
        {
            _instance = new SingletonClass(id); // changing instance
        }

        return _instance;
    }

class 中的所有其他代码都不是静态的(包括 Id 属性)。 在运行的早期,当仍然有一个线程时,我用一些 id 初始化单例,如下所示:

SingletonClass.GetInstance(<some_not_null_id>);

_instance 设置为不为空(已检查)。 后来我创建了一些线程来完成一些任务,这些任务需要从 SingletonClass 读取信息(不写)。 根据我发现的任何文档和 Whosebug 中的答案,同一个实例应该对所有线程可用(我没有使用 [ThreadStatic] 或任何其他类似机制)。

但是,当尝试从线程内部不带参数的 GetInstance() 时,我得到 NullException(_instance 成员为 Null)。

我正在使用 .NET 4.5 版,并使用 VS2012。

有什么想法吗?

我相信您想要一个具有可更新值的真正单例。此更新需要是线程安全的。创建应该是一个独立于获取的方法。

private static readonly MyContainerClass _instance = new MyContainerClass(); //true singleton

private sealed class MyContainerClass //this is the singleton
{
   private ISingletonClass _value = null;

   public ISingletonClass Value //threadsafe access to your object
   {
      get { lock(this){ return _value; } }
   }

   public ISingletonClass CreateOrUpdateValue(string id) //threadsafe updating of your object
   {
      if (id==null) throw new ArgumentNullException("id is missing!");
      lock(this)
      {
        var instance = _instance.Value;

        if (instance == null || instance.Id != id)
          _instance.Value = new SingletonClass(id);

        return _instance.Value;
       }
    }
}

public static void CreateOrUpdateInstance(string id)
{
    _instance.CreateOrUpdateValue(id);
}

public static ISingletonClass GetInstance()
{
    var instance = _instance.Value;

    if (instance == null)
       throw new Exception("Instance has not been created");

    return _instance;
}

// this is like your original method if you really want it
public static ISingletonClass GetInstance(string id)
{
    return _instance.CreateOrUpdateValue(id);
}

首先,您关于抛出异常的假设是不正确的:

NullException (the _instance member is Null).

从您的 GetInstance() 方法中抛出的唯一 NullReferenceException 是您自己抛出的那个。假设您在其他任何地方都没有将 _instance 值重置为 null 的代码,该方法取消引用 _instance 值的唯一位置是在无法确保 [=15] 的情况下无法访问的语句=] 被初始化为一些非空值。

至于更广泛的问题,恕我直言,最大的问题是你有一个定义不明确的问题,以及一个错误的实现。

即使忽略 "singleton"(它不是真正的单例,但为了争论起见,我们暂时称它为单例)是否曾经改变的问题,初始化也不是线程安全的。您有以下潜在竞争(假设单个 CPU 核心以使说明简单):

Thread 1                   Thread 2
--------                   --------
call GetInstance()
if (_instance == null)
--> preempted <--
                           call GetInstance()
                           if (_instance == null)
                           ...
                           _instance = new SingletonClass(id);
                           ...
                           return _instance;
                           --> preempted <--
if (_instance == null)
...
_instance = new SingletonClass(id);
...
return _instance;

正如您在上面的例子中看到的,按照现在代码的编写方式,每个线程都可以独立尝试检索实例,将当前值视为 null,并创建一个新实例要返回的对象。

对于真正的单例,最好的实现方式是使用 Lazy<T> class:

private static readonly Lazy<SingletonClass> _instance =
   new Lazy<SingletonClass>(() => new SingletonClass());

public static SingletonClass Instance { get { return _instance.Value; } }

在这种情况下,Lazy<T> class 处理所有初始化工作,包括确保它以线程安全的方式完成。

在你的情况下,如果你没有真正的单例,以上方法将不起作用。 Lazy<T> 模式只适用于只初始化一次的东西,但您希望能够即时更改它。鉴于此,您需要更像这样的东西:

private static ISingletonClass _instance = null;
private static readonly object _lock = new object();

public static ISingletonClass GetInstance(string id = null)
{
    lock (_object)
    {
        if (_instance == null || (id != null && _instance.Id != id))
        {
            if (id == null)
            {
                throw new ArgumentNullException("id");
            }

            _instance = new SingletonClass(id);
        }

        return _instance;
    }
}

以上将确保防止线程同时初始化该字段。他们只能争先恐后地锁定,然后保证一个线程是唯一一个初始化对象的线程,假设每个线程都为 id.

传递相同的值

也就是说,这只能修复代码中的基本线程安全问题。还有一些更大的问题。

首先,如果一个线程检索当前实例,然后其他线程在第一个线程使用它检索的实例完成之前更改当前实例,您希望代码做什么?我并不是说这在本质上是错误的,但它至少非常脆弱,您绝对需要考虑这种情况并自行决定在这种情况下正确的行为应该是什么。

其次,这是单例模式的一个非常脆弱的突变。真正的单例只有一个对象,在进程的生命周期内只分配一次。这确保了设计的简单性和行为的可预测性。

您所拥有的实现必然会使理解代码在任何给定点的作用变得更加困难。当一些错误出现并且他们试图追踪错误的实际原因时,几乎可以保证为某些开发人员的一天增加大量时间,无论是你的还是其他人的。

这个 class 的实例绑定到字符串 ID 的事实表明,更好的方法可能是维护 Dictionary<string, SingletonClass> 个对象,要求所有调用者始终 指定 ID,并使用该 ID 检索(当然可能是惰性初始化)线程当时需要的对象。

我强烈推荐不同的设计。但至少,如果您决定必须朝这个方向发展,请确保您已经考虑了线程事件的所有各种组合,并且不仅决定了每个给定场景中的正确行为,而且添加代码以确保任何假设约束。