【问题标题】:Accessing ConcurrentDictionary in destructor在析构函数中访问 ConcurrentDictionary
【发布时间】:2015-03-19 02:59:59
【问题描述】:

我有一个 Wrapper<T> where T : class 包裹着我的对象。我将WeakReference<Wrapper<T>> 存储在ConcurrentDictionary 中,以便为不可变对象实现弱引用线程安全缓存,当其他对象需要内存时,这些对象会自动清理。我需要在Wrapper 析构函数中调用ConcurrentDictionary.TryRemove 来释放字典中不再指向有效对象的弱引用。

众所周知,我们不应该在析构函数中使用任何锁定,因为存在死锁的风险。所以我想知道,我可以在析构函数中安全地使用ConcurrentDictionary.TryRemove 吗?恐怕它可能是使用SpinLock 或其他工具实现的,因此在析构函数中使用时仍然存在死锁风险。

【问题讨论】:

    标签: c# .net multithreading concurrency


    【解决方案1】:

    您可以在 location 看到 ConcurrentDictionary 的实现,而 TryRemove 实现使用“lock(...)”。

    您可以在析构函数中执行的操作是使用线程池从字典中删除项目。您仍然需要将包装器实例标记为不再有效,这样如果在终结器运行和线程池删除它之间对其任何公共方法进行调用,您可以检测到这一点并拒绝调用。

    【讨论】:

    • 谢谢,我害怕那个。你能建议一些解决方法吗?一种方法是使用Interlocked 设置需要清除集合的标志,然后在每个公共方法中使用CompareExchange 设置标志,但这不是很干净的解决方案。我可以使用一些愚蠢的东西,比如在析构函数中启动一个线程来在 GC 完成后清理集合吗?我不确定在 GC 完成之前从析构函数启动的线程是否处于挂起状态,或者它们是否同时运行。
    • 更新了关于使用线程池删除项目的建议。
    • 所以如果我在线程池(Task.Factory.StartNew,或除new Thread() 之外的其他线程池)上安排一个任务,那么线程只会在所有垃圾收集完成后启动?我担心通过在终结器中启动任何类型的线程,该线程可能会与终结线程同时运行。当我在 Wrapper 终结器中时,可能碰巧字典也被垃圾收集了,所以启动一个新线程来清理刚刚被收集的集合可能是个坏主意。
    • 我不知道垃圾收集器是否会在运行时停止所有其他托管线程。
    • 好吧,我只是做了一些实验,如果你在终结器中启动一个线程,它会与终结器线程同时运行。
    【解决方案2】:

    因此,您不要想要在析构函数中使用锁定的原因是,析构函数可能会被一个单独的线程中的 FinalizerWorker 调用,同时停止所有线程上的执行。

    因此,如果一个线程在 FinalizerWorker 启动时处于 ConcurrencyDictionary 操作的中间,如果析构函数试图锁定 ConcurrencyDictionary,则可能会出现死锁(这可能是一个很难重现的死锁)。

    自旋锁对您没有帮助,因为如果任何当前正在执行的线程锁定了 ConcurrencyDictionary 或 Spinner 变量,它不会在 FinalizerWorker 完成之前释放它,因为它会自旋/锁定永远。

    您的主要选择是使用 SuppressFinalize(this) 调用实现 IDisposable 接口,因为您的对象将抑制终结器工作程序,不会发生死锁并且 ConcurrencyDictionary 操作是安全的!

    因此,如果您使用 object.Dispose() 抢占 Finalizer,您应该可以安全地使用 ConcurrencyDictionary,否则请勿在您的 Finalizer Dispose(false) 调用中使用任何类型的锁,否则您将在某个时候出现死锁。

    // Design pattern for a base class.
    public class Base: IDisposable
    {
        private bool disposed = false;
    //Implement IDisposable.
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
    
    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            //Disposing outside the FinalizerWorker (SAFE)
            if (disposing)            
                m_pDictionary.TryRemove(this);            
    
            disposed = true;
        }
    }
    
    // Use C# destructor syntax for finalization code.
    ~Base()
    {
        // Simply call Dispose(false).
        Dispose (false);
    }
    

    【讨论】:

    • 阅读问题,问题是can I use ConcurrentDictionary.TryRemove safely in a destructor?
    • 如果它可以抛出一个在析构函数中是不可以的异常,为什么还要尝试。它通常不利于在析构函数中清理外部资源。 IDisposable 的存在是有原因的
    • @GaryKaizer 你意识到 MSDN 推荐的 IDisposable 实现模式在析构函数中调用 Dispose 对吗?
    • 你说得对,我有点太习惯于管理 C++ 有区别的地方,我会修改我的代码,只包括我一次性检查 bDisposing。
    • 我认为你在这里混合了两种不同的东西:a)FinalizerWorker 线程; b) 垃圾收集器线程。 FinalizerWorker 与任何其他线程没有太大区别,而 GC 线程可能会“停止世界”,因此锁定技术可能会导致死锁。幸运的是,你不能在 GC 线程中实现任何逻辑,因为它完全由运行时实现。
    【解决方案3】:

    这里的答案会让你误入歧途。不鼓励在析构函数/终结器中使用锁,因为它很容易导致死锁,尤其是在“手动”实现而不是使用并发集合时,但有时这是必要的。即使在“停止世界”的 GC 实现中,终结器也运行在一个单独的终结器线程上,该线程与您的应用程序同时运行。

    第一件事是第一件事 - 非常罕见的是,您所建议的是实现所需功能的理想方式,以至于我非常有信心它不是。首先,WeakReferences 不适合在缓存中使用,因为它们被收集的频率远远超过“需要内存”的时候。一个适当的缓存实现会监控具有强引用的内存级别,并在内存使用率过高时根据需要释放它们。

    即使在像 WeakValueDictionary 这样的实现中,您不希望集合保留可以收集的值,但实现仍然不会接收对象集合通知。相反,只要您偶然发现一个已收集的条目,您只需删除该条目,或者您每隔 X 次操作或每隔 Y 秒扫描整个集合以查找死条目,等等。

    也就是说,如果您确实遇到了在收集对象时需要做某事的情况,您可以使用并发收集就好了。

    假设您没有做任何愚蠢的事情,在通知队列中排队一个 ID 或从并发字典中删除一个项目是安全的,因为这些操作速度很快,并且只会在很短的时间内阻塞,而您的应用程序不会在你的终结器运行时被阻止。

    这是您应该尽可能避免的事情,但有时这是实现某事的最佳且唯一的方法。只需确保锁快速且尽可能少地使用,而不是任何容易意外出错和死锁的多级锁定方案的一部分。

    【讨论】:

      猜你喜欢
      • 2016-10-23
      • 2015-05-14
      • 1970-01-01
      • 2019-09-03
      • 1970-01-01
      • 1970-01-01
      • 2014-02-23
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多