包含非托管对象的ConcurrentBag的最终化

时间:2016-08-12 17:50:42

标签: c# idisposable finalizer

我在使用包含非托管对象的ConcurrentBag正确处理Dispose / Finalization时遇到问题。运行以下代码(通常)会在ObjectDisposedException的调用中产生Cannot access a disposed object.Object name: 'The ThreadLocal object has been disposed.'.TryTake())。

据推测,在这种情况下,垃圾收集器在调用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
}

2 个答案:

答案 0 :(得分:3)

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

由于ConcurrentBag未从CriticalFinalizerObject继承,因此您无法在运行自己的终结器时依赖它。 this_collection.m_locals变量Sign mentions in his answer都会在this无法访问的同时进入终结队列。处理队列的顺序不确定。

有一篇很棒的文章&#34; IDisposable: What Your Mother Never Told You About Resource Deallocation&#34;深入了解当somthing最终确定时实际发生的事情,并提供比微软推荐的传统private void Dispose(bool disposing)模式更好的模式。

答案 1 :(得分:1)

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

   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表明它具有IDisposable ThreadLocal,并在GetThreadList方法中使用。奇怪的是,如果你使用一个普通的旧foreach循环,你可以避免使用ThreadLocal,看起来一切都按照你期望的方式工作。

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

尽管有这样的挖掘,我还没有解释ThreadLocal如何处置。