【问题标题】:static instance is not shared between threads静态实例不在线程之间共享
【发布时间】:2015-01-08 09:47:17
【问题描述】:

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

    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;
    }

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

SingletonClass.GetInstance(<some_not_null_id>);

_instance 设置为不为空(已检查)。 后来我创建了一些线程来执行一些任务,其中需要从 SingletonClass 读取信息(不写入)。 根据我找到的任何文档以及 StackOverflow 中的答案,同一个实例应该可用于所有线程(我没有使用 [ThreadStatic] 或任何其他类似机制)。

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

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

有什么想法吗?

【问题讨论】:

  • 旁注:NullReferenceException 是一个保留的异常,因此您不应该抛出它。
  • 你得到你的空异常,带有“id is missing!”的那个。留言?
  • 你可以使用ArgumentNullException
  • 另一个旁注:你实现它的方式不再是单例了。
  • 您的代码存在严重问题,无法将其视为 Singleton。当传递不同的 Id 时,它将创建不同的对象。它覆盖了以前编写的实例引用。

标签: c# .net multithreading thread-safety


【解决方案1】:

首先,您对抛出异常的假设不正确:

NullException(_instance 成员为 Null)。

从您的GetInstance() 方法中抛出的唯一NullReferenceException 是您自己抛出的那个。假设您在其他任何地方都没有将_instance 值重置为null 的代码,那么该方法取消引用_instance 值的唯一位置是在不确保_instance 初始化为某个非空值。

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

即使忽略“单例”(它不是真正的单例,但为了论证起见,我们暂时称其为单例)是否已更改的问题,初始化也不是线程安全的。您有以下潜在的竞争(假设单个 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&lt;T&gt; 类:

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

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

在这种情况下,Lazy&lt;T&gt; 类处理所有初始化工作,包括确保以线程安全的方式完成。

在您的情况下,如果您没有真正的单例,上述方法将不起作用。 Lazy&lt;T&gt; 模式仅适用于仅初始化一次的情况,但您希望能够即时更改它。鉴于此,您需要更多类似的东西:

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传递相同的值。

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

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

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

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

此类的实例与字符串 ID 相关联这一事实表明,更好的方法可能是维护 Dictionary&lt;string, SingletonClass&gt; 对象,要求所有调用者始终指定 ID,并使用该 ID 用于检索(当然可能是延迟初始化)线程此时需要的对象。

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

【讨论】:

  • 感谢您的全面回复。我需要做一些澄清:_instance 成员为空。这是事实。我调试了它。在这种情况下,您描述的场景是不可能的,因为就像我最初写的那样,实例在创建线程之前已成功设置(调试它)。线程仅从实例中读取,而不是写入或更改它。我确实需要在代码中添加一些线程安全机制,这是真的。
  • _instance 成员是null 的事实并不意味着NullReferenceException使用 _instance 值(即取消引用它)时发生。如果您只是意味着 null 的值导致您自己的代码显式抛出 NullReferenceException ,那么我误解了...我的意思是导致异常的是您自己的 throw new NullReferenceException... ,而不是实际使用`_instance_ 值。
【解决方案2】:

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

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);
}

【讨论】:

  • 感谢您的输入,总的来说 - 您是正确的。在这种情况下,就像我写的那样,实例是在创建(连续)更多线程之前创建的。并且线程不会尝试替换实例,只是为了获取它(不提供 id)。仍然 - 它没有解释问题 - 为什么静态成员不在线程之间共享?
  • 可能是因为您的代码不是线程安全的。你没有锁。
猜你喜欢
  • 2010-11-23
  • 2011-06-23
  • 1970-01-01
  • 2011-03-13
  • 2015-01-19
  • 2013-06-20
  • 1970-01-01
  • 2019-03-29
相关资源
最近更新 更多