Не стал бы утверждать, что я полностью понимаю проблему и как ее исправить (и чья это вина), но вот что я обнаружил:
Прежде всего, блок 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'
, которое будет запускать код после оператора lock
, только если сгенерированный перечислитель не находится в disposeMode.
System.Linq.Async
имеет два оператора, которые внутренне используют AsyncEnumerablePartition
- Skip
и Take
. Разница в том, что когда Take
завершает работу, он не запускает базовый перечислитель до завершения, а Skip
выполняет (я немного пояснил здесь, причина не смотрел в базовую реализацию), поэтому, когда код удаления запускается для Take
case 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<int>(GetStream()))
Я бы сказал, что это какая-то проблема с компилятором в крайнем случае, потому что кажется, что он странным образом обрабатывает весь код после блоков finally, например, если вы добавляете Console.WriteLine("After global finally");
в конец GetStream
не будет напечатано и в том случае, если итератор не «завершен». Ваш обходной путь работает, потому что WriteLine
находится в блоке finally.
Отправленный вопрос на github , посмотрим, что скажет команда tnet.