[英]Code not executing in IAsyncEnumerable iterator, after lock in finally
我在枚举IAsyncEnumerable
时遇到了一个奇怪的问题,附加了System.Linq.Async运算符Take
在我的迭代器中,我有一个 try-finally 块,其中在try
块中产生了一些值,在finally
块中产生了一些清理代码。 清理代码位于lock
块内。 问题是lock
块后面的任何代码都不会执行。 没有抛出异常,只是忽略了代码,就像它不存在一样。 这是一个重现此行为的程序:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
public class Program
{
static async Task Main()
{
await foreach (var item in GetStream().Take(1))
{
Console.WriteLine($"Received: {item}");
}
Console.WriteLine($"Done");
}
static async IAsyncEnumerable<int> GetStream()
{
var locker = new object();
await Task.Delay(100);
try
{
yield return 1;
yield return 2;
}
finally
{
Console.WriteLine($"Finally before lock");
lock (locker) { /* Clean up */ }
Console.WriteLine($"Finally after lock");
}
}
}
Output:
Received: 1
Finally before lock
Done
控制台中未打印文本“Finally after lock” !
这只发生在附加了Take
操作符的情况下。 如果没有操作符,文本将按预期打印。
这是System.Linq.Async
库中的错误,C# 编译器中的错误,还是其他?
作为一种解决方法,我目前在 finally 内使用嵌套的 try- finally
块,它可以工作但很尴尬:
finally
{
try
{
lock (locker) { /* Clean up */ }
}
finally
{
Console.WriteLine($"Finally after lock");
}
}
.NET Core 3.1.3, .NET Framework 4.8.4150.0, C# 8, System.Linq.Async 4.1.1, Visual Studio 16.5.4, Console Application
不会声称我完全理解这个问题以及如何解决它(以及是谁的错),但这就是我发现的:
首先,finally 块被转换为下一个 IL:
IL_017c: ldarg.0 // this
IL_017d: ldfld bool TestAsyncEnum.Program/'<GetStream>d__1'::'<>w__disposeMode'
IL_0182: brfalse.s IL_0186
IL_0184: br.s IL_0199
IL_0186: ldarg.0 // this
IL_0187: ldnull
IL_0188: stfld object TestAsyncEnum.Program/'<GetStream>d__1'::'<>s__2'
// [37 17 - 37 58]
IL_018d: ldstr "Finally after lock"
IL_0192: call void [System.Console]System.Console::WriteLine(string)
IL_0197: nop
// [38 13 - 38 14]
IL_0198: nop
IL_0199: endfinally
} // end of finally
如您所见,编译器生成的代码具有下一个分支IL_017d: ldfld bool TestAsyncEnum.Program/'<GetStream>d__1'::'<>w__disposeMode'
仅当生成的枚举器不在 disposeMode 中时才会在lock
语句后运行代码.
System.Linq.Async
有两个内部使用AsyncEnumerablePartition
的运算符 - Skip
和Take
。 不同之处在于,当Take
完成时,它不会运行底层枚举器完成,而Skip
会运行(我在这里详细说明了一点,原因没有查看底层实现),所以当为Take
case 触发处置代码时, disposeMode
是设置为 true 并且该部分代码不会运行。
这是重现问题的 class (基于 nuget 中发生的事情):
public class MyAsyncIterator<T> : IAsyncEnumerable<T>, IAsyncEnumerator<T>
{
private readonly IAsyncEnumerable<T> _source;
private IAsyncEnumerator<T>? _enumerator;
T _current = default!;
public T Current => _current;
public MyAsyncIterator(IAsyncEnumerable<T> source)
{
_source = source;
}
public IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = new CancellationToken()) => this;
public async ValueTask DisposeAsync()
{
if (_enumerator != null)
{
await _enumerator.DisposeAsync().ConfigureAwait(false);
_enumerator = null;
}
}
private int _taken;
public async ValueTask<bool> MoveNextAsync()
{
_enumerator ??= _source.GetAsyncEnumerator();
if (_taken < 1 && await _enumerator!.MoveNextAsync().ConfigureAwait(false))
{
_taken++; // COMMENTING IT OUT MAKES IT WORK
_current = _enumerator.Current;
return true;
}
return false;
}
}
并在代码中使用await foreach (var item in new MyAsyncIterator<int>(GetStream()))
我会说这是一些边缘案例编译器问题,因为它似乎在 finally 块之后奇怪地处理所有代码,例如,如果你添加Console.WriteLine("After global finally");
到GetStream
的末尾,如果迭代器没有“完成”,它也不会被打印出来。 您的解决方法有效,因为WriteLine
在 finally 块中。
在github上提交问题,看看 dotnet 团队会怎么说。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.