最终锁定后,代码未在 IAsyncEnumerable 迭代器中执行

Code not executing in IAsyncEnumerable iterator, after lock in finally

我在枚举重现此行为的 IAsyncEnumerable, with the System.Linq.Async operator Take attached to it. In my iterator I have a try-finally block with some values yielded inside the try block, and some clean-up code inside the finally block. The clean-up code is inside a lock block. The issue is that whatever code follows the lock block is not executed. No exception is thrown, the code is just ignored like it's not there. Here is a program 时遇到了一个奇怪的问题:

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");
        }
    }
}

输出:

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、控制台应用程序

不会声称我完全理解这个问题以及如何解决它(这是谁的错),但这就是我发现的:

首先,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 的运算符 - SkipTake。不同之处在于,当 Take 完成时,它不会 运行 底层枚举器完成,而 Skip 会完成(我在这里详细说明了一点,因为没有查看底层实现),所以当针对 Take 情况触发处置代码时,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 的末尾,它将如果迭代器没有 "completed",也不会被打印出来。您的解决方法有效,因为 WriteLine 位于 finally 块中。

已在 github 上提交问题,看看 dotnet 团队会怎么说。