【问题标题】:Readonly fields becomes null when disposing from finalizer从终结器处理时,只读字段变为空
【发布时间】:2015-11-10 07:37:15
【问题描述】:

我有以下课程。现在有时 lock 语句会抛出 ArgumentNullException,在这种情况下,我可以在调试器中看到 disposelock 对象确实为空。

我可以看到 disposing 是错误的,我知道该方法是从 Finalizer 触发的。

但这怎么会发生呢?它被定义为只读,并在创建对象时获取其值。

PS:我知道这不是一个好的模式,但它是给定代码的一部分,我无法解释为什么它会变为 null

public abstract class DisposableMarshalByRefObject : MarshalByRefObject, IDisposable
{
    private readonly object disposeLock = new object();


   /// </summary>
   ~DisposableMarshalByRefObject()
   {
       Dispose(false);
   }

   /// <summary>
   /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
   /// </summary>
   public void Dispose()
   {
       Dispose(true);
       GC.SuppressFinalize(this);
   }

   protected void Dispose(bool disposing) //disposing = false,=> finalizer
   {
       lock (disposeLock) //ArgumentNull Exception !
       {
           ....
       }
   }
}           

【问题讨论】:

  • 请展示一个简短但完整的程序来说明问题。这在我看来似是而非。
  • 有终结器吗?也许对象已经被释放,你需要抑制完成
  • 关于垃圾收集~DisposableMarshalByRefObject()) 该收集的顺序(this - 首先,然后是 -disposeLockdisposeLock 首先,然后是 this未确定
  • @BoasEnkler:readonly,其实是一种语言特性。运行时不会阻止修改只读字段。例如,您可以使用反射来设置readonly 字段值。
  • @JeppeStigNielsen 另一个可能触发锁的对象也可能符合垃圾回收条件,但引用了DisposableMarshalByRefObject 的实例。如果它的终结器触发了对不同线程上DisposableMarshalByRefObject 上的方法的调用,则需要锁定。

标签: c# .net garbage-collection


【解决方案1】:

垃圾收集上没有定义该收集的顺序:

  1. 第一个this被收集
  2. 下一个disposeLock被收集

或者

  1. 第一个disposeLock被收集
  2. 下一个this被收集

所以不要在Dispose(false); 上使用任何reference 字段(structsintbool 等是安全的)

protected virtual void Dispose(bool disposing) {
  if (disposing) {
    // Explicit disposing: it's safe to use disposeLock 
    lock (disposeLock) {
      ...
    }
  } 
  else {
    // Garbage collection: there's no guarantee that disposeLock has not been collected
  }
}

【讨论】:

  • 你确定吗?具有尚未触发的终结器的 AFAIK 会复活对象及其引用的所有对象。未定义的是调用终结器的顺序。虽然没有指定收集的顺序,但在终结器运行之前都不能收集任何对象,您只能通过弱引用观察这个未定义的顺序,示例中没有显示。
  • @CodesInChaos:disposeLock没有终结器,不能复活,所以可以收集
  • AFAIK 带有终结器的对象会暂时阻止收集(但不是终结)从它可以访问的所有对象。如果它永久地复活自己,那些被引用的对象也会永久地复活自己。
  • 收集对象时,其引用不会设置为空。这不能解释为什么该字段为空。由于锁只是一个object,GC 根本不会处理它,直到它真的无法访问。
  • 我同意 Usr:这个答案没有解释 ArgumentNullException 是如何发生的。
【解决方案2】:

除了反思答案之外的所有现有答案都是错误的。 GC 在收集对象时不会将引用设置为 null。对象访问不会因为 GC 而虚假失败。终结顺序未定义,但所有存在的对象引用继续存在且有效。

我对发生的事情的猜测:构造函数在字段初始化之前被中止。这离开了领域null。终结器后来发现它是这样的。

可以通过抛出异常或调用Thread.Abort 来中止构造函数,这是邪恶的。

在垃圾收集时,该收集的顺序未定义

collection 的顺序不是observable(除了通过弱引用...)。 finalization 的顺序是可观察到的,但不能通过 lock 语句观察到,因为对象在完成时不会失去同步能力。

【讨论】:

  • (dis)证明这个解释,在构造函数中插入一个日志调用(这表明对象已经完全构造),如果你得到一个抛出空引用异常的终结器,检查你的日志看看它的构造函数是否完成。
【解决方案3】:

既然你强调了readonly,那就稍微澄清一下。运行时不会阻止修改 readonly 字段。不管 C# 中的 readonly 在 IL 中变成 initonly

例如,您可以使用反射轻松修改readonly 字段:

class A
{
    private readonly int bar;

    public A()
    {
        bar = 1;
    }

    public void Foo()
    {
        Console.WriteLine(bar);
    }
}

var a = new A();

a.Foo(); // displays "1"
a.GetType().GetField("bar", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(a, 2);
a.Foo(); // displays "2"

当然,这并不意味着您应该每次都针对null 测试这些字段,但是当readonly 字段被修改时,可能其中之一)。

作为旁注。你真的需要终结器吗?我的意思是,是否有任何 true 非托管资源?

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-06-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-03-26
    相关资源
    最近更新 更多