确定托管资源与非托管资源

23

关于托管资源和非托管资源有许多问题。我理解了这两者的基本定义。然而,我很难知道何时资源或对象是托管的还是非托管的。

当我想到非托管资源时,我往往会想到不直接属于.NET的本机代码,例如pinvoke或封送资源。我通常会认为旨在与将使用硬件的某些东西进行接口的资源,例如文件句柄或网络连接也是非托管的。

那么,包装本机非托管资源的.NET对象呢,例如FileStream

FileStream必须使用非托管资源,但是当我实现IDisposable模式时,我应该将其视为托管资源还是非托管资源?

到目前为止,我一直认为如果对象实现了IDisposable,则它是托管的。那么我如何知道IntPtr应该被处理为非托管资源呢?

3个回答

17

一个FileStream必须使用非托管资源,但是当我实现IDisposable模式时,我应该将其视为托管还是非托管资源?

FileStream是一个托管资源

托管资源是包含(并必须管理)非托管资源的类。通常实际资源位于几个层级下。

到目前为止,我一直认为如果对象实现了IDisposable,那么它就是托管的。

正确。

我如何知道IntPtr应该被处理为非托管资源?

从您获取其值的API的文档中了解。但请注意,在实践中,大多数程序员从不直接处理非托管资源。当您确实需要时,请使用SafeHandle类将非托管资源转换为托管资源。


如果一个托管对象由GC处理,那么如果我没有在对象上调用Dispose(),GC是否仍然会释放所有封装的资源?我试图更好地理解可释放模式,以及为什么只有托管资源会在if(disposing)块中被释放,而不是通过终结器。 - galford13x
1
是的,垃圾回收器最终会释放资源,但有时太晚了。在这里,垃圾回收器非常像备用降落伞。 - H H
@HenkHolterman:如果事件“发布者”变得符合GC的条件,它将不再保持订阅者的活动状态,但是不能保证事件发布者在应用程序的生命周期内会变得符合GC的条件。如果发布者是像长期并发集合这样的东西,其枚举器请求更新通知(当集合被修改时,其枚举器无效将不会非常有用),那么很难想象可能会有无限数量的对象注册通知事件,然后被遗弃。 - supercat
@HenkHolterman:我刚意识到你的评论是针对除了我的答案之外的另一个答案的评论,但是我的先前评论适用于像请求其他对象的通知这样的IDisposable对象。 - supercat
我很想看到一个证明,即FileStream或Socket或DB连接应该在Dispose重写的“托管”部分中被处理(注意 - 只是这个问题 - socket.Dispose()应该放在哪里,而不是它是否是托管对象等)。有什么确凿的证据吗?(超越了 - 哎呀兄弟,显然) - Boppity Bop

11

这很简单,你永远不可能意外分配未受管理的资源。需要使用pinvoke调用进行分配,你会知道的。术语“对象”是重载的,但是不存在未受管理的对象,在.NET程序中的所有对象都是托管的。您可以与支持创建对象的另一种语言(如C ++)的代码进行交互。但你不能直接使用这样的对象,需要使用C ++ / CLI包装器,从而成为实现IDisposable的托管类。

如果您使用文档不好的库,则当您收到IntPtr时请注意。这是非常强烈的迹象,表示涉及未受管理的分配,可能是指向未受管理的内存或操作系统句柄的指针。 如果该库没有自动管理它,则该库应该为您提供释放它的方法。如果不确定如何正确处理,请联系库的所有者。

Microsoft的工作是在所有常见操作系统资源周围提供托管包装器类,例如FileStream,Socket等。这些类几乎总是实现IDisposable。当将这样的类对象存储在自己的类中时,您唯一需要在代码中执行的操作就是实现IDisposable,并调用Dispose()方法。或者在方法中将其用作局部变量时使用using语句。


我明白了。每次我实现IDisposable接口时,我都必须引用标准模式,主要是因为它对我来说不太合理。我的直觉告诉我,即使是托管资源,在指定终结器时也应该进行处理。当然,我只在处理c/c++库时遇到过这种情况。 - galford13x
3
不,编写终结器在99.9%的情况下都是错误的。从“标准模式”中可以看出,当disposing参数为false时,您从不执行任何操作。 - Hans Passant
有许多种未受管理的资源,其中一些可以完全存在于托管代码中。如果放弃一个资源会导致其被清理(通常借助Finalize方法),则该资源是“托管”的;如果放弃它会泄漏,则是未受管理的资源,无论它在哪里。例如,如果在迭代器持有锁时放弃它,则锁将永远不会被释放。锁完全存在于托管代码世界中,但是它们是未受管理的资源,因为它们无法知道获取它们的实体是否仍然存在。 - supercat

1

在这个上下文中,“资源”最有用的解释是“一个对象请求另一个对象代表它执行某些操作,直到另行通知,而这会对其他人造成损害”。如果放弃一个对象会导致垃圾回收器通知该对象被放弃,并且该对象反过来指示任何代表其行事的东西停止这样做,则该对象构成“托管资源”。未封装在托管资源中的资源称为“非托管资源”。

如果某个对象Foo分配了一个非托管内存句柄,它会请求内存管理器授予它独占使用一些内存区域,使其对于可能想要使用它的任何其他代码不可用,直到Foo告知内存管理器不再需要该内存并应将其用于其他目的。使句柄成为非托管资源的原因不是它通过API接收到的事实,而是即使所有有意义的引用都被放弃,内存管理器仍将永远继续授予内存的独占使用权给一个不再需要它(很可能也不存在)的对象。

虽然API句柄是最常见的非托管资源,但还有无数其他类型的非托管资源。像监视器锁和事件这样的东西完全存在于.net的托管代码世界中,但仍然可能代表非托管资源,因为在等待代码时获取锁并放弃可能导致该代码永远等待,而订阅长期对象的事件的短暂对象如果在被放弃之前未取消订阅,则可能导致该长期对象继续无限期地携带事件引用(如果只有一个订阅者被放弃,则负担很小,但如果创建并放弃了无限数量的订阅者,则负担是无限的)。

附录 垃圾收集器的一个基本假设是,当对象X持有对对象Y的引用时,是因为X“感兴趣”于Y。然而,在某些情况下,引用可能是由于X希望Y保持对它的引用,即使Y不关心。这种情况在通知事件处理程序中经常发生。对象Y可能希望每次发生某些事情时都得到通知。尽管X必须保留对Y的引用以便执行此类通知,但X本身并不关心通知。它只执行它们是因为假定某个根对象可能关心Y是否收到它们。

在某些情况下,可以使用所谓的“弱事件模式”。不幸的是,虽然在 .net 中有许多弱事件模式,但由于缺乏适当的“WeakDelegate”类型,它们都具有怪癖和限制。此外,虽然弱事件很有帮助,但它们不是万能药。例如,假设 Y 已要求长时间存活的对象 X,在发生某些事情时通知它,对 Y 的唯一现有引用是 X 用于此类通知的引用,Y 对此类通知所做的唯一操作是增加某个对象 Z 中的属性,而设置该属性不会修改 Z 外部的任何内容。在这种情况下,即使对象 Z 将是宇宙中唯一关心对象 Y 的事物,Z 也不会持有任何与 Y 相关的引用,因此垃圾收集器将无法将 Y 的生命周期与 Z 的生命周期相联系。如果 X 持有对 Y 的强引用,则后者即使没有任何人感兴趣也将保持活动状态。如果 X 仅持有弱引用,则即使 Z 对其感兴趣,也可能对 Y 进行垃圾回收。垃圾收集器没有机制可以自动推断 Z 对 Y 的感兴趣。

事实上,我有时会忘记注销事件。为什么这会导致内存泄漏呢?如果GC可以处理托管资源,那么为什么会出现这种情况呢?我认为在.NET对象上创建的事件应该是受管理的。 - galford13x
@galford13x:请看附录。事件模式可能是 .net 中最大的内存泄漏源头;各种“弱事件”模式可以提供帮助,但根本问题在于没有简便的方法告诉 GC,从 XY 的引用应该被视为 YX 感兴趣(在 .net 4.0 之前是不可能的;在 .net 4.0 中虽然有可能,但并不方便)。 - supercat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接