此问题特定于 .NET 9 下的多线程应用程序。
我经常处理 COM Interop 场景,其中引用必须按特定顺序处理,而获取它们的顺序是不确定的。我还认为,在处理编程构造而不是业务逻辑的场景中,接口继承是比实现继承更好的方法。然而,这个问题与方法无关。
我使用
IDisposable
模式的具体、抽象实现作为顶级类的基础,需要对处理顺序进行精细控制。这是我第一次实现 IAsyncDispose
模式,并尝试遵守此处提供的文档。我仍然在思考这个问题,所以不确定问题标题是否合适。
internal abstract class Disposable:
IDisposable,
IAsyncDisposable
{
private bool Disposed = false;
private readonly List<SafeHandle?> SafeHandleList = [];
private readonly Stack<SafeHandle?> SafeHandleStack = [];
private readonly Queue<SafeHandle?> SafeHandleQueue = [];
private readonly List<IDisposable?> DisposableList = [];
private readonly Stack<IDisposable?> DisposableStack = [];
private readonly Queue<IDisposable?> DisposableQueue = [];
private readonly List<IAsyncDisposable?> AsyncDisposableList = [];
private readonly Stack<IAsyncDisposable?> AsyncDisposableStack = [];
private readonly Queue<IAsyncDisposable?> AsyncDisposableQueue = [];
protected Disposable () { }
~Disposable () { this.Dispose(disposing: false); }
public bool IsDisposed => this.Disposed;
protected T AddDisposable<T> (T disposable) where T : IDisposable
{ this.ThrowDisposedException(); this.DisposableList.Add(disposable); return disposable; }
protected T PushDisposable<T> (T disposable) where T : IDisposable
{ this.ThrowDisposedException(); this.DisposableStack.Push(disposable); return disposable; }
protected T EnqueueDisposable<T> (T disposable) where T : IDisposable
{ this.ThrowDisposedException(); this.DisposableQueue.Enqueue(disposable); return disposable; }
protected T AddAsyncDisposable<T> (T asyncDisposable) where T : IAsyncDisposable
{ this.ThrowDisposedException(); this.AsyncDisposableList.Add(asyncDisposable); if (asyncDisposable is IDisposable disposable) { this.DisposableList.Add(disposable); } return asyncDisposable; }
protected T PushAsyncDisposable<T> (T asyncDisposable) where T : IAsyncDisposable
{ this.ThrowDisposedException(); this.AsyncDisposableStack.Push(asyncDisposable); if (asyncDisposable is IDisposable disposable) { this.DisposableStack.Push(disposable); } return asyncDisposable; }
protected T EnqueueAsyncDisposable<T> (T asyncDisposable) where T : IAsyncDisposable
{ this.ThrowDisposedException(); this.AsyncDisposableQueue.Enqueue(asyncDisposable); if (asyncDisposable is IDisposable disposable) { this.DisposableQueue.Enqueue(disposable); } return asyncDisposable; }
protected T AddSafeHandle<T> (T safeHandle) where T : SafeHandle
{ this.ThrowDisposedException(); this.SafeHandleList.Add(safeHandle); return safeHandle; }
protected T PushSafeHandle<T> (T disposable) where T : SafeHandle
{ this.ThrowDisposedException(); this.SafeHandleStack.Push(disposable); return disposable; }
protected T EnqueueSafeHandle<T> (T disposable) where T : SafeHandle
{ this.ThrowDisposedException(); this.SafeHandleQueue.Enqueue(disposable); return disposable; }
public void ThrowDisposedException ()
{
if (this.Disposed)
{
var type = this.GetType();
throw new ObjectDisposedException(type.FullName, $@"Attempt to access a disposed object: [{type.FullName}].");
}
}
public void Dispose ()
{
this.Dispose(disposing: true);
GC.SuppressFinalize(obj: this);
}
protected virtual void Dispose (bool disposing)
{
if (!this.Disposed)
{
if (disposing)
{
// Dispose objects implementing [IDisposable] and [SafeHandle].
while (this.DisposableList.Count > 0) { try { this.DisposableList [0]?.Dispose(); } catch { } this.DisposableList.RemoveAt(0); }
while (this.DisposableStack.Count > 0) { try { this.DisposableStack.Pop()?.Dispose(); } catch { } }
while (this.DisposableQueue.Count > 0) { try { this.DisposableQueue.Dequeue()?.Dispose(); } catch { } }
while (this.SafeHandleList.Count > 0) { try { this.SafeHandleList [0]?.Dispose(); } catch { } this.SafeHandleList.RemoveAt(0); }
while (this.SafeHandleStack.Count > 0) { try { this.SafeHandleStack.Pop()?.Dispose(); } catch { } }
while (this.SafeHandleQueue.Count > 0) { try { this.SafeHandleQueue.Dequeue()?.Dispose(); } catch { } }
// This approach is meant to help both async and non-async consumption scenarios.
// Any objects implementing [IAsyncDisposable] as well as [IDisposable] have already been dealt with at this point.
// https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/implementing-disposeasync#implement-both-dispose-and-async-dispose-patterns.
// It is up to the comsuming code to ensure that [DisposeAsync] is called, if needed. Example: [using (var ad = new AsyncDisposable())] vs. [await using (var ad = = new AsyncDisposable())].
}
// Dispose unmanaged resources (excluding [SafeHandle] objects).
// Free unmanaged resources (unmanaged objects), override finalizer, and set large fields to null.
this.Disposed = true;
}
}
public async ValueTask DisposeAsync ()
{
// Perform asynchronous cleanup.
await this.DisposeAsyncCore()
.ConfigureAwait(false);
// Dispose of unmanaged resources.
this.Dispose(false);
// Suppress finalization.
GC.SuppressFinalize(this);
}
protected virtual async ValueTask DisposeAsyncCore ()
{
if (!this.Disposed)
{
while (this.AsyncDisposableList.Count > 0)
{
var asyncDisposable = this.AsyncDisposableList [0];
if (asyncDisposable is not null) { try { await asyncDisposable.DisposeAsync().ConfigureAwait(false); } catch { } }
this.AsyncDisposableList.RemoveAt(0);
}
while (this.AsyncDisposableStack.Count > 0)
{
var asyncDisposable = this.AsyncDisposableStack.Pop();
if (asyncDisposable is not null) { try { await asyncDisposable.DisposeAsync().ConfigureAwait(false); } catch { } }
}
while (this.AsyncDisposableQueue.Count > 0)
{
var asyncDisposable = this.AsyncDisposableQueue.Dequeue();
if (asyncDisposable is not null) { try { await asyncDisposable.DisposeAsync().ConfigureAwait(false); } catch { } }
}
// TODO: We want to ensure that objects implementing only [IDisposable] are handled as well.
// Although there are not circular references between the dispose mthods, calling [this.Dispose()] here directly smells funny.
this.Dispose();
this.Disposed = true;
}
}
}
虽然我尝试遵守最佳实践,但我不确定以下几点:
Dispose
内部呼叫DisposeAsyncCore
。我有些不舒服。或许我还没有想清楚这一点。我应该调用虚拟实现吗Dispose(disposing: true|false???)
?using (var ad = new AsyncDisposable())
] 与 [await using (var ad = new AsyncDisposable())
],是否有任何问题?SafeHandle
] 物品应该扔到哪里?等等您的目标是允许继承的类能够根据需要添加一次性对象,而不必覆盖/加重它们自己的
Dispose(bool)/DisposeAsyncCore
覆盖。
如有任何建议,我们将不胜感激。
这里有一个用法示例以供澄清:
public sealed class SampleTask:
Disposable
{
private readonly MemoryStream Stream1;
private readonly Excel.Application Application;
public SampleTask ()
{
// Add child objects in any desired order.
this.Application = this.AddDisposable(new Excel.Application());
this.Stream1 = this.PushAsyncDisposable(new MemoryStream());
}
// This class needs to have control over the lifetime of some
// exposed objects irrespective of their external reference count.
public Image GetImage () => this.AddDisposable(new Bitmap(10, 10));
public Excel.Worksheet GetExcelWorksheet () => this.AddDisposable(this.Application.Workbooks [0].Sheets [0]);
protected override void Dispose (bool disposing) => base.Dispose(disposing);
protected override ValueTask DisposeAsyncCore () => base.DisposeAsyncCore();
}
您的代码,就像许多处理终结的代码(实际上是 C# 的“析构函数”的设计)一样,似乎因对终结实际工作原理的一些误解而受到影响。
与流行的看法相反,当对象
被垃圾收集时,对象的
Finalize
方法不会运行,而是当 本来会被垃圾收集时运行,但由于存在注册的终结器某处。 已注册终结器的存在将阻止一个对象或它持有强引用的任何对象“实际上”被垃圾收集,直到终结器被取消注册(这将由于触发它而发生),并且只要对对象的引用存在于宇宙中的任何地方,就不可能确定代码不会再次尝试使用对象。
如果希望拥有在对象“实际上”被垃圾收集时执行的代码,则应该有一个面向公众的“shell”对象,该对象构造并保存对两个或三个私有对象的引用。 其中之一应该封装主要对象功能并处理任何订阅的事件或通知,其中之一应该是“金丝雀”对象,当其所有者被垃圾收集时,该对象将发出声音。 第三个对象将保存足够的信息来执行清理,但仅当第一个对象保存对外部对象的引用时才需要。 Canary 对象应该对其所有者(shell 对象)持有一个“长弱引用”(一个 WeakReference
,可选的布尔构造函数参数设置为 true),以及对负责清理的对象及其 Finalize 方法的普通引用应该检查 WeakReference
是否仍然存在并重新注册自身以进行终结(如果 WeakReference
仍然存在)或调用主功能对象的清理方法。 请注意,当调用 cleanup 方法时,主对象可能仍然是事件订阅之类的目标,但代码可以确定宇宙中的任何地方都不会再存在对外部对象的引用。
自从我在 .NET 中完成大量编码以来,许多类型确实不需要对终结进行任何操作,但如果需要终结,我建议使用多对象模式并小心确保没有引用循环会将 Canary 对象或清理对象连接回 shell 对象,并且 shell 对象之外不存在对 Canary 对象的强引用。 当终结确实触发时,应该设法最小化无法被垃圾收集的对象的范围。