【问题标题】:Can a readonly field in .NET become null?.NET 中的只读字段可以变为空吗?
【发布时间】:2009-12-08 07:07:15
【问题描述】:

我在多线程应用程序中有一个奇怪的错误:

public class MyClass
{
   private readonly Hashtable HashPrefs;
   public MyClass(int id)
  {
     HashPrefs = new Hashtable();
  } 

  public void SomeMethodCalledFromAnotherThread(string hashKey,string hashValue)
 {
    if (HashPrefs.Contains(hashKey))  // <-- throws NullReferenceException
    {

    }
 }
}

一个线程做:

 SomeQueue.Add(new MyClass(1));

另一个线程会这样做:

 SomeQueue.Dequeue().SomeMethodCalledFromAnotherThread(SomeClass.SomeMethod(),"const value"); 

但是第二个线程如何在构造函数完成之前调用方法呢?

编辑:我添加了带有函数参数的部分,因为这似乎是相关的。 据我所知,传递的 hashKey 不能为 null,因为 SomeMethod() 总是返回一个相关的字符串。

正如其他人指出的那样,如果问题是传递给 Contains() 的 null haskKey 参数,则异常将是 ArgumentNullException。

【问题讨论】:

  • 欢迎来到多线程地狱...
  • 哦,我已经从事多线程工作几年了 :-( 。这只是让我头晕目眩的特殊错误之一。
  • 您的实例变量可能正在从特定于线程的缓存中读取。请参阅我编辑的答案。
  • 无法调试。仅在正常运行数小时后(〜随机)在生产代码中发生。多线程适合你
  • 我为此感到害怕。您可以尝试使用 Windows 调试工具来尝试转储问题。运行“adplus -p -crash”,它会连接一个调试器,当发生未处理的异常时创建转储。

标签: c# multithreading


【解决方案1】:

反射是执行此操作的一种方法 (SetField)。

获得这种好奇心的第二种方法是,如果您过早放弃引用,通过将this(或涉及隐式this 的某些内容,例如捕获到匿名方法/ lambda 中的字段)传递出去.ctor:

public MyClass(int id)
{
    Program.Test(this); // oopsie ;-p
    HashPrefs = new Hashtable();
}

或者更有可能(给定问题):

SomeQueue.Add(this);

另一个相关问题可能是 - 不能一开始就分配吗?答案是肯定的,尤其是在您使用序列化的情况下。与流行的看法相反,你实际上可以绕过构造函数,如果你有一个理由; DataContractSerializer 就是一个很好的例子……

以下将创建一个带有空字段的MyClass

MyClass obj = (MyClass)
    System.Runtime.Serialization.FormatterServices.GetUninitializedObject(
        typeof(MyClass));

【讨论】:

  • 序列化的好点。只是出于好奇:如果在对象被序列化时哈希表在那里,反序列化不会创建一个具有正确哈希表的对象吗?
  • 在给出的示例中,它可能会(假设该字段在序列化时存在,并且尚未重命名)。如果您正在使用自定义序列化程序做一些时髦的事情,也许不会。我承认这些都是边缘情况,但是,嘿!这是一个疯狂的世界。当然,GetUninitializedObject 方法将“按原样”工作。
  • WCF 对象反序列化以这种方式创建对象。为了解决这个问题,我将这些只读字段设置为只读属性,在首次访问时初始化基础字段。
【解决方案2】:

是的,只读字段可以通过反射访问并且它们的值发生了变化。所以你的问题的答案是肯定的,这是可能的。

但是,还有许多其他事情也可能给您带来问题。多线程代码难以编写,可能出现的问题也难以诊断。


附带说明一下,您确定您没有收到ArgumentNullException 吗?如果somekeynullHashtable.Contains 方法将抛出ArgumentNullException。也许这就是问题所在?

【讨论】:

  • 有那么一刻我想相信这是一个 ArgumentNullException。很棒的电话,但没有!这是一个 NullReference :-(
【解决方案3】:

首先,在构造函数完成之前,其他线程无法访问实例,因为在构造函数完成之前不会分配实例本身。话虽如此,另一个线程可以在构造函数完成之前访问保存实例的 variable,但那时它会是null。这将生成NullReferenceException,但它来自访问实例的方法,而不是来自实例内部的方法。

话虽如此,我能看到的唯一真正的解决方案是使用外部锁来同步检索实例的代码。如果您正在访问 Hashset(或任何其他类型的实例成员不能保证是线程安全的),则无论如何都需要围绕操作执行锁定。

将以下类视为包含对 MyClass 的引用和其他线程的创建者的类:

public class MasterClass
{
    private MyClass myClass;
    private object syncRoot = new object(); // this is what we'll use to synchronize the code

    public void Thread1Proc()
    {
        lock(syncRoot)
        {
            myClass = new MyClass();
        }
    }

    public void Thread2Proc()
    {
        lock(syncRoot)
        {
            myClas.SomeMethodCalledFromAnotherThread();
        }
    }
}

这是一个非常(可能过于)简单化的同步应该如何发生的观点,但这种方法的总体思路是将所有与共享对象交互的代码封装在 lock 块内。您确实应该研究一下如何编写多线程进程,以及旨在在多线程之间共享资源的各种方法。

编辑

一个完全不同的可能性是实例变量的值以特定于线程的方式被缓存。如果您没有锁定(这会产生内存屏障),请尝试将实例变量标记为 volatile,这将确保读取和写入以正确的顺序发生。

【讨论】:

    【解决方案4】:

    您的指令可能会重新排序,以便在完全构造之前将对 MyClass 实例的引用添加到队列中。 Here's 一篇关于双重检查锁定的文章涉及到这个主题。来自文章:

    CLR JIT 团队的开发主管 Vance 解释说,问题在于 CLR 内存模型……本质上,内存模型允许对非易失性读\写进行重新排序,只要从这一点上看不到这种变化单个线程的视图。

    看看System.Threading.Thread.MemoryBarrier.你可能需要做这样的事情:

     MyClass temp = new MyClass(1);
     System.Threading.Thread.MemoryBarrier();
     SomeQueue.Add(temp);
    

    MemoryBarrier 确保在继续之前执行所有指令。

    【讨论】:

      【解决方案5】:

      您确定是 HashPrefs.Contains 引发了异常,而不是“somekey”代表的任何内容吗?如果不是,能否请您发布其 ToString 方法返回的完整异常详细信息?

      【讨论】:

        【解决方案6】:

        做吧

        HashPrefs = Hashtable.Synchronized(new Hashtable());
        

        无需重新发明轮子。

        【讨论】:

        • +1。我一般不使用旧的非泛型集合类型,但这是一个很好的发现!
        • 很可能会导致他走错路,因为我强烈怀疑在检查Contains() 之后,false 的分支会添加一个元素。当然,“同步”Hashtable 不会将这两个调用作为一个单元同步。
        • 我担心这个问题不一定与队列的同步有关,就像我的 Hastable 在某些时候变为 null 一样
        【解决方案7】:

        你确定HashPrefs.Contains 正在抛出 NPE 吗?我认为SomeQueue.Dequeue().SomeMethodCalledFromAnotherThread() 更有可能是候选人......因为当您尝试将元素出列时,构造函数和添加操作可能尚未完成。

        事实上,在出列元素之前检查SomeQueue.Count* 可能不是一个坏主意。

        编辑: 或者更好的是,在调用 SomeMethodCalledFromAnotherThread 之前进行空检查,假设您的其他线程正在循环检查队列......因为如果不是,lock 在添加和出队操作之前的队列。

        MyClass mine = SomeQueue.Dequeue();
        
        if (null != mine)
            mine.SomeMethodCalledFromAnotherThread();
        

        *.NET 3.5 中添加的扩展方法。

        【讨论】:

        • 这是一个多线程问题,这意味着您无法在出队之前检查 someQueue.Count。这个计数没有任何意义,因为在 Count 属性返回和调用 Dequeue 之间,队列可能是空的。
        • 在这种情况下,他可以存储 Dequeued 元素并在对其调用方法之前检查它是否为 null。
        【解决方案8】:

        您可以尝试几件事:

        1. 尝试将 Hashtable 初始化为成员变量(在构造函数之外),并使用同名的公共属性,查看是否有人调用它进行赋值,如下所示:

          public class MyClass {
          private readonly Hashtable hashPrefs = new Hashtable();
          
          public MyClass(int id)
          {
          
          }
          
          public Hashtable HashPrefs
          {
              set
              {
                  throw new InvalidOperationException("This shouldn't happen");
              }
          }
          

          }

        2. 其次,“SomeQueue”是什么类型的?这只是一个普通的 List 吗?或者它是一些特殊的自我实现的队列,它依赖于一些 XML/二进制序列化?如果是这样,您是否将 HashPrefs 标记为 [Serializable]?还是作为 [DataMember]?

        【讨论】:

          【解决方案9】:

          我想到了几个想法:

          • 正在同时修改哈希表,导致内部状态不一致。
          • 对 contains 的调用执行相等性检查,尝试取消引用具有空字段的值。 `somekey` 是什么类型的?

          【讨论】:

          • 我编辑了这个问题。 somekey 是作为参数传递的字符串
          猜你喜欢
          • 1970-01-01
          • 2012-03-14
          • 1970-01-01
          • 1970-01-01
          • 2020-07-16
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多