簡體   English   中英

最終鎖定后,代碼未在 IAsyncEnumerable 迭代器中執行

[英]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的運算符 - SkipTake 不同之處在於,當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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM