简体   繁体   English

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

[英]Code not executing in IAsyncEnumerable iterator, after lock in finally

I am experiencing a strange issue while enumerating an IAsyncEnumerable , with the System.Linq.Async operator Take attached to it.我在枚举IAsyncEnumerable时遇到了一个奇怪的问题,附加了System.Linq.Async运算符Take 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.在我的迭代器中,我有一个 try-finally 块,其中在try块中产生了一些值,在finally块中产生了一些清理代码。 The clean-up code is inside a lock block.清理代码位于lock块内。 The issue is that whatever code follows the lock block is not executed.问题是lock块后面的任何代码都不会执行。 No exception is thrown, the code is just ignored like it's not there.没有抛出异常,只是忽略了代码,就像它不存在一样。 Here is a program that reproduces this behavior:这是一个重现此行为的程序

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: Output:

Received: 1
Finally before lock
Done

The text "Finally after lock" is not printed in the console!控制台中未打印文本“Finally after lock”

This only happens with the Take operator attached.这只发生在附加了Take操作符的情况下。 Without the operator the text is printed as expected.如果没有操作符,文本将按预期打印。

Is this a bug in the System.Linq.Async library, a bug in the C# compiler, or something else?这是System.Linq.Async库中的错误,C# 编译器中的错误,还是其他?

As a workaround I am currently using a nested try-finally block inside the finally , which works but it's awkward:作为一种解决方法,我目前在 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 .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

Would not claim that I fully understand the issue and how to fix it(and whose fault is it) but that's what I found:不会声称我完全理解这个问题以及如何解决它(以及是谁的错),但这就是我发现的:

First of all the finally block is translated to next IL:首先,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

As you can see the compiler generated code has next branching IL_017d: ldfld bool TestAsyncEnum.Program/'<GetStream>d__1'::'<>w__disposeMode' which will run the code after lock statement only if the generated enumerator is not in the disposeMode.如您所见,编译器生成的代码具有下一个分支IL_017d: ldfld bool TestAsyncEnum.Program/'<GetStream>d__1'::'<>w__disposeMode'仅当生成的枚举器不在 disposeMode 中时才会在lock语句后运行代码.

System.Linq.Async has two operators which internally use AsyncEnumerablePartition - Skip and Take . System.Linq.Async有两个内部使用AsyncEnumerablePartition的运算符 - SkipTake Difference is that when Take finishes it does not run underlying enumerator to completion, and Skip does (I have elaborated here a little bit, cause have not looked in the underlying implementation), so when the disposal code is triggered for Take case the disposeMode is set to true and that part of code is not run.不同之处在于,当Take完成时,它不会运行底层枚举器完成,而Skip会运行(我在这里详细说明了一点,原因没有查看底层实现),所以当为Take case 触发处置代码时, disposeMode是设置为 true 并且该部分代码不会运行。

Here is class (based on what's happening in the nuget) to repro issue:这是重现问题的 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;
    }
}

And usage in your code await foreach (var item in new MyAsyncIterator<int>(GetStream()))并在代码中使用await foreach (var item in new MyAsyncIterator<int>(GetStream()))

I would say that this is some edge case compiler issue, cause it seems to handle strangely all code after finally blocks, for example if your add Console.WriteLine("After global finally");我会说这是一些边缘案例编译器问题,因为它似乎在 finally 块之后奇怪地处理所有代码,例如,如果你添加Console.WriteLine("After global finally"); to the end of GetStream it will not be printed also in case if the iterator has not "completed".GetStream的末尾,如果迭代器没有“完成”,它也不会被打印出来。 Your workaround works cause the WriteLine is in finally block.您的解决方法有效,因为WriteLine在 finally 块中。

Submitted issue on github , will see what dotnet team will say.github上提交问题,看看 dotnet 团队会怎么说。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM