安全地处理.net终结器

时间:2013-08-01 12:49:57

标签: c# garbage-collection idisposable finalizer

我想要一种方法来打破IDisposable链,你突然依赖的一些嵌套类现在实现了IDisposable,你不希望那个接口连接你的复合层。 基本上,我通过'SubscribeWeakly()'IObservable<T>的弱订阅我希望在我离开时不要泄漏包装器实例,以防Observable永远不会触发。这是动机,但我也将它用于其他东西。

另一篇文章有​​类似的问题,而answer基本上表示你仍然可以在终结器中访问一次性用品。但是,您无法保证终结器的运行顺序,因此处理可能会出现问题。

因此,我需要一种方法来保证一次性用品保持活着,这样我就可以在终结器中拨打Dispose()。所以我查看了GCHandle,它允许C ++通过将它们和聚合物拉到应用程序句柄中来保持(并保持活动)托管对象,以使它们保持活动状态,直到句柄被释放并且复合生命周期返回到控制.NET的内存管理器。来自C ++,我认为类似于std::unique_ptr的行为会很好,所以我想出了类似于AutoDisposer的东西。

public class AutoDisposer
{
    GCHandle _handle;

    public AutoDisposer(IDisposable disposable)
    {
        if (disposable == null) throw new ArgumentNullException();

        _handle = GCHandle.Alloc(disposable);
    }

    ~AutoDisposer()
    {
        try 
        {
            var disposable = _handle.Target as IDisposable;
            if (disposable == null) return;
            try 
            {
                disposable.Dispose();
            }
            finally 
            {
                _handle.Free();
            }
        }
        catch (Exception) { }
    }
}

在需要在资源消失时处理资源的类中,我只需指定一个类似_autoDisposables = new AutoDisposer(disposables)的字段。在包含类的同时,这个AutoDisposer将被垃圾收集器清理。但是,我想知道这种技术会出现什么问题。现在我可以想到以下几点:

  • 通过使用终结器来对垃圾收集器进行额外开销
  • .NET必须从托管内存中将项目提取到应用程序句柄并返回它们的额外开销
  • 不可单元测试(我似乎无法预测资源何时返回到.NET进行内存管理。)

因此,如果我需要确定性地调用IDisposable,或等等,那么在实施Dispose()时,我会谨慎使用它,这不是太大的负担。

有人看到任何其他问题吗?这种技术甚至有效吗?

2 个答案:

答案 0 :(得分:3)

我认为你可能会误解IDispoable - IDisposable对象的典型模式是松散的:

void Dispose() { Dispose(true); }

void Dispose(bool disposing)
{
    if (disposing)
    {
        // Free managed resources
    }
    // always free unmanaged resources
}

~Finalizer() { Dispose (false); }

因为终结器应该总是处理非托管资源,如果你等待它运行(将来某个时候内存约束会触发垃圾收集,或者它被手动触发),你就不应该泄漏。如果您希望确定性关于何时释放这些资源,那么您必须在类层次结构中公开IDispoable

答案 1 :(得分:3)

可以设计类,以便它们可以相互协调它们的最终化行为。例如,一个对象可以接受类型为Action(bool)的构造函数参数,并指定如果为非空,则将其作为Dispose(bool)的第一步调用[可以使用{{读取支持字段] 1}}以确保委托最多被调用一次]。如果是一个类,例如封装了包含这样一个特性的文件,并被封装在一个类中,该类用额外的缓冲封装文件,该文件将通知缓冲类它即将关闭,缓冲类因此可以确保写入所有必要的数据。不幸的是,这种模式在框架中并不常见。

鉴于缺少这样的模式,封装缓冲文件的类的唯一方法可以确保如果它被放弃就会设法写出它的数据,而不会在文件被关闭之前关闭它所以,可以在文件的某处保留一个静态引用(可能使用Interlocked.Exchange(ref theField, null)的静态实例),并确保在清理它时,它会破坏该静态引用,将其数据写入文件,以及然后关闭文件。请注意,只有在包装器对象对其包装的对象保持独占控制时才应使用此方法,并且需要特别注意细节。终结器有许多奇怪的角落情况,很难正确处理它们,并且任何未能正确处理模糊角落的情况都可能导致Heisenbugs。

PS 继续ConcurrentDictionary(bufferedWrapper, fileObject)方法,如果您正在使用类似事件的事情,那么您的主要关注点应该是(1)确保如果某个对象被放弃了不要提及任何事情&#34; big&#34 ;; (2)确保ConcurrentDictionary中被遗弃物体的数量无法无限增长。第一个问题可以通过确保没有强大的&#34;来解决。从事件到任何重要的&#34;森林的参考路径&#34;相互关联的物体;如果多余的订阅仅包含对总共100个字节左右的对象的引用,并且如果任何事件发生,它们将被清除,即使一千个被遗弃的订阅也会代表一个非常小的问题[如果数量有限] 。第二个问题可以通过让每个订阅请求轮询字典中的某些项目(基于每个请求或摊销)来查看它们是否被放弃并清除它们(如果是这样)。如果某些事件被放弃并且从未触发,并且如果没有添加该类型的新事件,那些事件可能会无限期地停留,但它们将是无害的。事件可能是非常有害的唯一方法是,如果他们持有对大对象的引用(可以使用弱引用来避免),可以添加和放弃无限数量的事件而不会被清理(这不会被取消)如果添加新事件导致被遗弃的事件被清理掉,或者如果这样的事件可能会不断地浪费CPU时间(如果在关心它们的对象消失之后第一次尝试触发它们会导致它们失败)会发生这种情况得到清理)。