问题描述
我的 IAsyncEnumerable<T>
函数必须运行其清理代码,无论它返回的枚举数是否被正确处理。在下面的示例中,我使用了将字节数组返回到数组池的任务作为强制清理代码的示例。每个测试在完全完成之前停止使用枚举器。除了最后一个测试之外的所有测试都通过 foreach
实用程序正确处理了枚举器,但最后一个测试故意不处理枚举器。相反,它允许枚举器超出范围,然后触发垃圾收集以尝试查看系统本身是否可以触发最后的清理代码。
Starting 'Cancellation token'.
Finalizing 'Cancellation token'.
Starting 'Exception'.
Finalizing 'Exception'.
Starting 'Break'.
Finalizing 'Break'.
Starting 'Forget to dispose'.
Finalizing 'Forget to dispose'.
测试代码如下:
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
// -- Cancellation token test --
var cts = new CancellationTokenSource();
await foreach (var (index,bytes) in GetDataPackets("Cancellation token",cts.Token)) {
if (index == 2)
cts.Cancel();
}
// -- Thrown exception test --
try {
await foreach (var (index,bytes) in GetDataPackets("Exception")) {
if (index == 2)
throw new Exception("Boom");
}
} catch { }
// -- With Break test --
await foreach (var (index,bytes) in GetDataPackets("Break")) {
if (index == 2)
break;
}
// -- Forget to dispose test --
// Create variables and forget them in another "no inlining" method
// to make sure they have gone out of scope and are available for garbage collection.
await ForgetTodispose();
GC.Collect();
GC.WaitForPendingFinalizers();
[MethodImpl(MethodImplOptions.NoInlining)]
async Task ForgetTodispose() {
var enumerable = GetDataPackets("Forget to dispose");
var enumerator = enumerable.GetAsyncEnumerator();
await enumerator.MoveNextAsync();
while (enumerator.Current.Index != 2)
await enumerator.MoveNextAsync();
}
static async IAsyncEnumerable<(int Index,Memory<byte> Bytes)> GetDataPackets(string jobName,[EnumeratorCancellation] CancellationToken cancellationToken = default) {
Console.WriteLine($"Starting '{jobName}'.");
var rand = new Random();
var buffer = ArrayPool<byte>.Shared.Rent(512);
try {
for (var i = 0; i < 10; i++) {
try {
await Task.Delay(10,cancellationToken);
} catch (OperationCanceledException) {
yield break;
}
rand.NextBytes(buffer);
yield return (i,new Memory<byte>(buffer));
}
} finally {
Console.WriteLine($"Finalizing '{jobName}'.");
ArrayPool<byte>.Shared.Return(buffer);
}
}
坚持不懈,然后我尝试了一些可能会有所帮助的想法。他们都没有:
想法 1:添加 using 语句和执行清理工作的一次性结构:失败。
static async IAsyncEnumerable<(int Index,[EnumeratorCancellation] CancellationToken cancellationToken = default) {
Console.WriteLine($"Starting '{jobName}'.");
var rand = new Random();
var buffer = ArrayPool<byte>.Shared.Rent(512);
using var disposer = new disposer(() => {
Console.WriteLine($"Finalizing '{jobName}'.");
ArrayPool<byte>.Shared.Return(buffer);
});
for (var i = 0; i < 10; i++) {
try {
await Task.Delay(10,cancellationToken);
} catch (OperationCanceledException) {
yield break;
}
rand.NextBytes(buffer);
yield return (i,new Memory<byte>(buffer));
}
}
readonly struct disposer : Idisposable {
readonly Action _disposeAction;
public disposer(Action disposeAction)
=> _disposeAction = disposeAction;
public void dispose() {
_disposeAction();
}
}
想法 2:将 disposer
结构转换为具有终结器方法的类,希望其终结器可能被触发:也失败。
class disposer : Idisposable {
readonly Action _disposeAction;
public disposer(Action disposeAction)
=> _disposeAction = disposeAction;
public void dispose() {
_disposeAction();
GC.SuppressFinalize(this);
}
~disposer() {
_disposeAction();
}
}
除了从头开始编写我自己的枚举器类之外,我如何才能使这个编译器生成的枚举器始终运行其清理代码,即使在未正确处理的终结器线程中也是如此?
解决方法
我的 IAsyncEnumerable 函数必须运行其清理代码,无论它返回的枚举数是否被正确处理。
句号。你不能让 any 类型运行托管清理代码,不管它是否被正确处理。这在 .NET 中根本不可能。
您可以编写终结器,但终结器不能具有任意代码。通常,它们仅限于访问值类型成员并执行一些 p/Invoke 样式的调用。例如,它们不能将缓冲区返回到数组池。更普遍的是,除了少数例外,它们根本无法调用任何托管代码。它们实际上只是为了清理非托管资源。
所以这与异步枚举器没有任何关系。如果对象没有被释放,你不能保证清理代码会运行,任何类型的对象都是这种情况。
最好的解决方案是在清理代码被释放时运行它(例如,在异步枚举器函数的 finally
块中)。任何不处理的代码负责创建资源泄漏。这是所有其他 .NET 代码的工作方式,也是异步枚举器的工作方式。