【问题标题】:Code not executing in IAsyncEnumerable iterator, after lock in finally最终锁定后,代码未在 IAsyncEnumerable 迭代器中执行
【发布时间】:2020-05-03 15:53:13
【问题描述】:

我在枚举IAsyncEnumerable 时遇到了一个奇怪的问题,并附有System.Linq.Async 运算符Take。在我的迭代器中,我有一个 try-finally 块,其中在 try 块内产生了一些值,在 finally 块内产生了一些清理代码。清理代码位于 lock 块内。问题是lock 块后面的任何代码都不会执行。没有抛出异常,只是忽略了代码,就像它不存在一样。 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、控制台应用程序

【问题讨论】:

  • 非常有趣的行为。锁内的代码被执行; Take(2) 也会发生同样的事情;对于Take(3),发出"Finally after lock"
  • 你应该在github.com/dotnet/reactive报告它
  • 小问题。为什么需要锁在那里?
  • @GuruStron 因为我在枚举开始时在集合中添加了一个对象,并在枚举完成时将其删除。而使用lock 是确保线程安全的最简单方法。
  • @TheodorZoulias 我有点困惑,因为锁定对象是 repro =) 方法的本地对象。也有待讨论,但我个人认为使用concurrent collection 是最简单的)

标签: c# async-await iasyncenumerable linq-async


【解决方案1】:

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

首先将 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/'&lt;GetStream&gt;d__1'::'&lt;&gt;w__disposeMode',仅当生成的枚举器不在 disposeMode 中时才会在 lock 语句之后运行代码。

System.Linq.Async 有两个运算符,它们在内部使用 AsyncEnumerablePartition - SkipTake。不同的是,当Take 完成时,它不会运行底层枚举器完成,而Skip 会(我在这里详细阐述了一点,原因没有查看底层实现),所以当为@触发处置代码时987654333@ 情况下,disposeMode 设置为 true 并且该部分代码未运行。

这是重现问题的课程(基于 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&lt;int&gt;(GetStream()))

我会说这是一些边缘案例编译器问题,因为它似乎在 finally 块之后奇怪地处理所有代码,例如,如果您将 Console.WriteLine("After global finally"); 添加到 GetStream 的末尾,它也不会被打印以防万一如果迭代器尚未“完成”。您的解决方法有效,因为 WriteLine 在 finally 块中。

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

【讨论】:

  • 感谢 Stron 的回答。非常有趣的分析和观察!也感谢submiting GitHub 的问题(我懒得自己做!)。
  • NP,这对我来说既有趣又有教育意义 =)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-12-03
  • 1970-01-01
  • 2011-04-09
  • 2012-09-02
  • 2014-05-15
  • 1970-01-01
相关资源
最近更新 更多