包含非托管对象的 ConcurrentBag 的终结

Finalization of ConcurrentBag containing unmanaged objects

我在使用包含非托管对象的 ConcurrentBag 正确处理 Dispose/Finalization 时遇到问题。 运行 下面的代码(通常)会在调用 TryTake().

时生成 ObjectDisposedException (Cannot access a disposed object.Object name: 'The ThreadLocal object has been disposed.'.)

大概在这种情况下,垃圾收集器在调用 A 的终结器之前销毁了 ConcurrentBag。我原以为只有 ConcurrentBag 本身实现了终结器才会出现这种情况。是不是在完成路径中永远不应该接触托管对象?

class A : IDisposable
{
    private readonly ConcurrentBag<object> _collection = new ConcurrentBag<object>();

    public A(string value)
    {
        if (value == null) throw new ArgumentNullException();
    }

    ~A() 
    {
        Dispose(false);
    }

    public void Dispose() => Dispose(true);

    private void Dispose(bool disposing)
    {
        if (disposing) {}

        object value;
        while (_collection.TryTake(out value))
        {
            // Cleanup value
        }
    }
}

触发异常:

void Main()
{
    var a = new A(null);
}

以下似乎可以解决这个特定问题,但我不确定这是否安全。这种情况是否有完全安全的实施方式?

while (_collection.IsEmpty == false)
{
    object value;
    _collection.TryTake(out value);
    // Cleanup value
}

对象处置异常的完整堆栈跟踪是

   at System.Threading.ThreadLocal`1.GetValueSlow()
   at System.Threading.ThreadLocal`1.get_Value()
   at System.Collections.Concurrent.ConcurrentBag`1.GetThreadList(Boolean forceCreate)
   at System.Collections.Concurrent.ConcurrentBag`1.TryTakeOrPeek(T& result, Boolean take)
   at System.Collections.Concurrent.ConcurrentBag`1.TryTake(T& result)
   at A.Dispose(Boolean disposing)
   at A.Finalize()

这意味着已处置的对象位于 ConcurrentBag 中,这很奇怪,因为 ConcurrentBag 不是 IDisposable。通过 source of ConcurrentBag 挖掘表明它有一个 ThreadLocal 是 IDisposable 并且在 GetThreadList 方法中使用。奇怪的是,如果你使用一个普通的旧 foreach 循环,你会避免 ThreadLocal 并且看起来一切都按照你期望的方式工作。

foreach (var value in _collection)
{
   // Cleanup value
}

尽管进行了挖掘,但我无法解释 ThreadLocal 是如何处理的。

当代码从终结器执行时(disposingfalse),您唯一可以做的事情是使用没有状态的静态方法、函数局部变量和字段从 CriticalFinalizerObject 继承(除非你在 CriticalFinalizerObject 的终结器中,否则你不能使用它们)。

因为 ConcurrentBag 没有继承自 CriticalFinalizerObject,所以当你自己的终结器是 运行 时,你不能指望它没有被终结。当 this 变得不可访问时,this_collection.m_locals 变量 都同时被放入终结队列。处理队列的顺序是不确定的。

有一篇很棒的文章“IDisposable: What Your Mother Never Told You About Resource Deallocation”深入探讨了某物最终确定时实际发生的情况,并提供了一些比 Microsoft 推荐的传统 private void Dispose(bool disposing) 模式更好的模式。