在.net终结器中安全处置

本文关键字:安全 net | 更新日期: 2023-09-27 18:25:52

我想要一种方法来打破IDisposable链,在这个链中,您突然依赖的某个嵌套类现在实现了IDisposable,并且您不希望该接口在复合层中产生波纹基本上,我通过"SubscribeWeakly()"对IObservable<T>的订阅很弱,我想在离开时清理这些订阅,以免泄露包装实例,以防Observable永远不会触发。这就是我的动机,但我也把它用于其他事情

另一篇帖子也有类似的问题,答案基本上是说你仍然可以访问终结器中的一次性用品。但是,不能保证终结器的运行顺序,所以处理可能会有问题。

因此,我需要一种方法来保证一次性的保持活动,这样我就可以在我的终结器中调用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进行内存管理。)

因此,如果我需要决定性地调用Dispose()等,那么在实现IDisposable时我会谨慎地使用它

有人看到其他问题了吗?这种技术有效吗?

在.net终结器中安全处置

我想你可能误解了IDispoable——IDisposable对象上的典型模式是:

void Dispose() { Dispose(true); }
void Dispose(bool disposing)
{
    if (disposing)
    {
        // Free managed resources
    }
    // always free unmanaged resources
}
~Finalizer() { Dispose (false); }

因为终结器应该始终处理非托管资源,所以如果您等待它运行(这将在将来某个时候,当内存约束触发垃圾收集时,或者它是手动触发的),则不应该泄漏。如果您想对这些资源何时释放具有确定性,那么您必须在类层次结构中公开IDispoable

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

考虑到缺乏这样的模式,封装缓冲文件的类能够确保在数据被放弃时能够写出数据,而不会在文件被关闭之前关闭,唯一的方法就是在某个地方保留对文件的静态引用(可能使用ConcurrentDictionary(bufferedWrapper, fileObject)的静态实例),并确保在清理时,它将销毁该静态引用,将其数据写入该文件,然后关闭该文件。请注意,只有当包装器对象对其包装的对象保持独占控制时,才应该使用这种方法,并且它需要非常注意细节。终结器有很多奇怪的角落案例,很难正确处理它们,如果不能正确处理模糊的角落案例很可能会导致Heisenbugs。

PS继续ConcurrentDictionary方法,如果你使用类似事件的东西,你的主要关注点往往是(1)确保如果一个对象被放弃,事件不会引用任何"大"的东西;(2) 确保CCD_ 21中被丢弃对象的数量在没有绑定的情况下不能增长。第一个问题可以通过确保从事件到任何重要的互连对象"森林"没有"强"参考路径来处理;如果一个多余的订阅只包含对总计100字节左右对象的引用,并且如果任何事件发生,这些引用都会被清除,那么即使是一千个被放弃的订阅也会代表一个非常小的问题[前提是数量是有界的]。第二个问题可以通过让每个订阅请求轮询字典中的一些项目来处理(无论是按请求还是分期),看看它们是否被放弃,如果被放弃了,就进行清理。如果一些事件被放弃,并且从未启动,如果没有添加该类型的新事件,这些事件可能会无限期地存在,但它们是无害的。事件可能具有重大危害的唯一方法是,如果它们引用了大对象(使用弱引用可以避免),可以添加和放弃无限数量的事件而不进行清理(如果添加新事件导致被放弃的事件被清理,则不会发生这种情况),或者如果这样的事件可能会持续浪费CPU时间(如果在关心它们的对象消失后第一次尝试触发它们会导致它们被清理,则不会发生这种情况)。