在软件开发的中,资源的有效管理始终是保障程序健壮性与性能的核心课题。
资源分为:托管资源、非托管资源、混合型资源
托管资源,一般由垃圾回收器(Garbage Collector, GC)的自动化内存管理,开发人员通常不需要担心不再需要的对象,只要让这些对象的所有引用都超出作用域,并允许垃圾收集器在需要时释放内存即可。例如:
string
)、Array
)List
、Dictionary
)、不受CLR管理的资源,通常是通过调用外部库(如C++编写的库)或操作系统提供的接口来获取的资源。例如:
托管对象包装非托管资源,目的是为了方便在.NET环境中使用非托管资源,同时利用托管资源的管理机制。
示例
FileStream
(包装文件句柄)SqlConnection
(包装数据库连接)Bitmap
(包装图像资源)垃圾收集器不知道如何释放非托管的资源。这些资源需要手动释放,否则可能会导致资源泄露、程序崩溃或系统性能下降等问题。
HBRUSH
)、画笔(HPEN
)、设备上下文(HDC
)等GDI对象,它们由操作系统分配,需要手动释放,否则可能导致系统资源耗尽,影响图形界面的显示效果,甚至导致程序崩溃。在定义一个类时,可以使用两种机制来自动释放非托管的资源。使用托管对象包装非托管资源,目的是为了方便在.NET环境中使用非托管资源,同时利用托管资源的管理机制。
这些机制常常放在一起实现,因为每种机制都为问题提供了略为不同的解决方法。
我们都知道构造函数可以指定必须在创建类的实例时进行的某些操作。
相反,在垃圾收集器销毁对象之前,也可以调用析构函数。
定义方式和构造函数也很类似,一个没有返回值类型,签名和类名一致,没有入参的方法,不过多了一个前缀波形符(~)
class MyClass
{
~MyClass()
{
// Finalizer implementation
}
}
C#编译器在编译析构函数时,它会隐式地把析构函数的代码编译为等价于重写Finalize()
方法的代码,从而确保执行父类的Finalize()
方法。下面列出的C#代码等价于编译器为~MyClass()
析构函数生成的IL:
protected override void Finalize()
{
try
{
// Finalizer implementation
}
finally
{
base.Finalize();//执行父类的Finalize()方法
}
}
Finalizer特点:
非自动生成:必须显式定义
仅用于类:无法在结构中定义终结器。
唯一性:每个类只能有一个无参Finalizer
终结器不使用修饰符或参数。
不能继承或重载终结器。
执行不确定性:不能手动调用终结器,只能由GC自动触发,GC决定回收时机
在大多数情况下,通过使用System.Runtime.InteropServices.SafeHandle或派生类包装任何非托管句柄,可以免去编写终结器的过程。
这句话来源于Microsoft Learn,我的理解是SafeHandle实现了IDisposable接口+终结器的双重实现方式(下文有具体说明)。可查看源码safehandle.cs。
垃圾回收器(GC)的工作流程如下:
GC 定期检查托管堆中的对象,找出不再被任何引用的对象,标记为不可达后,
如果对象没有 Finalizer 方法,垃圾收集器在标记为不可达后,因为对象没有额外的清理任务需要在销毁前执行,所以垃圾收集器可以迅速且高效地回收其占用的内存。
如果对象有 Finalizer 方法,GC 会将这些对象放入“终结队列”(Finalization Queue)。
终结队列:
一个独立的终结线程(Finalizer Thread)会从终结队列中取出对象并调用它们的 Finalizer 方法。
在 Finalizer 方法执行完成后,对象才会被真正回收。
C++开发者常利用析构函数不仅进行资源清理,还用于调试信息传递和其他任务。相比之下,C#析构函数的使用频率远不及C++。
C#析构函数的实现会导致对象从内存中最终删除的时间被延迟。具体来说:
Finalize()
方法),但并不立即删除对象;第二次处理时才真正从内存中删除对象。此外,运行库使用一个单独的线程来执行所有对象的Finalize()
方法。如果析构函数被频繁使用,并且执行长时间的清理任务,将会对应用程序的性能产生显著影响。
在 C#中,推荐使用 System.IDisposable
接口替代析构函数。
IDisposable
接口定义了一种模式(具有语言级的支持),该模式为释放非托管的资源提供了确定的机制,并避免产生析构函数固有的与垃圾收集器相关的问题。
IDisposable
接口声明了一个 Dispose()
方法,它不带参数,返回 void
。如下所示:
class MyClass: IDisposable
{
public void Dispose()
{
// implementation
}
}
Dispose()
方法的实现代码显式地释放由对象直接使用的所有非托管资源。
这样,Dispose()
方法为何时释放非托管资源提供了精确的控制。
假定有一个 ResourceGobbler
类,它需要使用某些外部资源,且实现 IDisposable
接口。
var theInstance = new ResourceGobbler();
// do your processing
theInstance.Dispose();
但是,如果在处理过程中出现异常,这段代码就没有释放 theInstance
使用的资源,所以应使用 try
块,编写下面的代码:
ResourceGobbler theInstance = null;
try
{
theInstance = new ResourceGobbler();
// do your processing
}
finally
{
theInstance?.Dispose();
}
using
语句using
语句是.NET中用于确保资源在使用后被正确释放的语法糖。它会自动调用对象的Dispose()
方法,从而释放资源。
using (UnmanagedResource resource = new UnmanagedResource())
{
// 使用资源
}
// 资源在 using 块结束后自动释放
关键点说明:
using
语句的作用范围是using
块内部。一旦离开using
块,对象的Dispose()
方法会被自动调用。using
语句适用于实现了IDisposable
接口的对象。Dispose()
方法在某些情况下,可能需要手动调用Dispose()
方法来释放资源。例如,当资源不再需要时,可以立即释放,而不是等待using
块结束或对象被垃圾回收。
UnmanagedResource resource = new UnmanagedResource();
try
{
// 使用资源
}
finally
{
resource.Dispose(); // 手动释放资源
}
关键点说明:
finally
块中调用Dispose()
方法,可以确保即使在发生异常的情况下,资源也能被正确释放。前面讨论了自定义类所使用的释放非托管资源的两种方式:
Dispose()
方法。如果创建了终结器,就应该实现 IDisposable
接口。同时把实现析构函数作为一种安全机制,以防没有调用 Dispose()
方法。下面是一个双重实现的例子:
public class ResourceHolder : IDisposable
{
private bool _isDisposed = false;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_isDisposed)
{
if (disposing)
{
// Cleanup managed objects by calling their
// Dispose() methods.
}
// Cleanup unmanaged objects
_isDisposed = true;
}
}
~ResourceHolder()
{
Dispose(false);
}
}
Dispose(bool disposing)
方法:虚方法,真正完成清理工作的方法
disposing
参数,用于区分是否需要释放托管资源
true
true需要传参为时,表示可以释放托管资源;Dispose()
方法调用Dispose(bool disposing)
时,传参为true,此时明确进行释放。false
时,表示只释放非托管资源。在析构函数中调用Dispose(false)
,以确保非托管资源在对象被垃圾回收时被释放。Dispose()
方法:用于释放资源。
调用Dispose(true)
:释放托管资源和非托管资源
调用GC.SuppressFinalize(this)
:明确告诉垃圾回收器资源已经清理完毕,以防止垃圾回收器调用析构函数,同时符合设计规范。
在显式调用Dispose()
方法后,资源已经被清理了,再调用析构函数可能会导致以下问题:
Dispose(false)
:只释放非托管资源,确保非托管资源在对象被垃圾回收时被释放。如果资源可能被多个线程访问,需要确保资源释放的线程安全性。可以通过锁(如lock
)来避免并发问题。
使用专用对象作为同步锁是最佳实践,可以避免与其他锁的意外冲突。
多线程同时调用时,只有一个线程能执行实际清理
private readonly object _disposeLock = new object();
lock (_disposeLock)
{
if (!_isDisposed)
{
// 资源释放代码...
_isDisposed = true;
}
}
修改后:
public class ResourceHolder : IDisposable
{
private bool _isDisposed = false;
private readonly object _disposeLock = new object(); // 用于同步的锁对象
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
lock (_disposeLock) // 确保线程安全的互斥访问
{
if (!_isDisposed)
{
if (disposing)
{
// Cleanup managed objects by calling their
// Dispose() methods.
}
// Cleanup unmanaged objects
_isDisposed = true; // 在锁内确保原子性修改
}
}
}
~ResourceHolder()
{
Dispose(false);
}
}
如果清理操作非常轻量,可以用更轻量的Interlocked
代替锁:
private int _isDisposed = 0; // 0 = false, 1 = true
protected virtual void Dispose(bool disposing)
{
if (Interlocked.Exchange(ref _isDisposed, 1) == 0)
{
// 清理代码...
}
}
如果类中有其他需要线程安全的方法,建议:
public void SomeMethod()
{
lock (_disposeLock)
{
if (_isDisposed)
throw new ObjectDisposedException(...);
// 正常逻辑...
}
}
若需要支持并发调用且性能敏感,可考虑双重检查锁(Double-Check Locking)模式。
如果类定义了实现 IDisposable 的成员,该类也应该实现 IDisposable。
例如,类中包含了一个IDisposable
类型的对象作为字段,那么这个类本身也应该实现IDisposable
接口。
如果类中包含IDisposable
类型的成员,那么这些成员在类的生命周期内可能会占用资源。为了确保这些资源能够被正确释放,类本身需要实现IDisposable
接口,并在Dispose
方法中调用成员的Dispose
方法,以释放其占用的资源。
示例:
public class MyClass : IDisposable
{
private SomeDisposableResource _resource;
public MyClass()
{
_resource = new SomeDisposableResource();
}
public void Dispose()
{
// 释放资源
_resource.Dispose();
}
}
在这个例子中,MyClass
包含了一个实现了IDisposable
的成员_resource
,因此MyClass
也需要实现IDisposable
接口,并在Dispose
方法中调用_resource.Dispose()
,以确保_resource
所占用的资源能够被正确释放。
实现 IDisposable 并不意味着也应该实现一个终结器。
终结器会带来额外的开销,因为它需要创建一个对象,释放该对象的内存,需要 GC 的额外处理。
只在需要时才应该实现终结器,例如,发布本机资源。要释放本机资源,就需要终结器。
如果实现了终结器,也应该实现 IDisposable 接口。
这样,本机资源可以早些释放,而不仅是在 GC 找出被占用的资源时,才释放资源。
在终结器的实现代码中,不能访问已终结的对象了。
终结器的执行顺序是没有保证的。
5.如果所使用的一个对象实现了 IDisposable 接口,就需要在不再使用需对象时调用Dispose 方法。如果在方法中使用这个对象,using 语句比较方便。如果对象是类的一个成员,就让类也实现 Disposable。
try-finally
块或using
语句来保证。IDisposable
****接口:在包含非托管资源的类中实现IDisposable
接口,并在Dispose
方法中编写释放非托管资源的代码。using
****语句:在需要使用非托管资源的代码块中使用using
语句,以确保资源在代码块结束时被自动释放。Dispose
方法中调用GC.SuppressFinalize(this)
来阻止垃圾回收器调用析构函数。IDisposable
接口的类中,可以同时提供析构函数作为最后的保障措施。但请注意,析构函数中应只调用Dispose(false)
,以避免重复释放资源。lock
)来避免并发问题。