如果没有等待最后一个 MoveNextAsync() 任务,IAsyncEnumerator<T>.DisposeAsync() 是否应该抛出?

Should IAsyncEnumerator<T>.DisposeAsync() throw if the last MoveNextAsync() task is not awaited?

为什么 UseEventsFail 会抛出下面的代码?难道我没有等待最后一个 MoveNextAsync() 任务就处理了异步枚举器?这个例子是我真实程序的简化重现,所以我需要处理异步枚举器来释放它的资源。 Task.CompletedTask 通常是 Task.Delay() 用作 UseEvents() 的超时。如果枚举器任务在超时任务之前完成,则不会抛出异常。

异常堆栈跟踪:

at Program.<<<Main>$>g__GenerateEvents|0_3>d.System.IAsyncDisposable.DisposeAsync()

代码:

// All these are ok
await GenerateEvents().GetAsyncEnumerator().DisposeAsync();
await using var enu = GenerateEvents().GetAsyncEnumerator();
await UseEvents();
await UseEvents2();

// This fail
await UseEventsFail();

async Task UseEvents()
{
    await using var enu = GenerateEvents().GetAsyncEnumerator();
    await Task.WhenAny(enu.MoveNextAsync().AsTask());
}

async Task UseEvents2()
{
    var enu = GenerateEvents().GetAsyncEnumerator();
    await Task.WhenAny(enu.MoveNextAsync().AsTask(), Task.CompletedTask);
}

async Task UseEventsFail()
{
    await using var enu = GenerateEvents().GetAsyncEnumerator();
    await Task.WhenAny(enu.MoveNextAsync().AsTask(), Task.CompletedTask);
}

async IAsyncEnumerable<bool> GenerateEvents()
{
    while (true) {
        await Task.Delay(1000);
        yield return true;
    }
}

摘自 Stephen Toub 的 MSDN 文章 Iterating with Async Enumerables in C# 8

It should be evident that it’s fine for one MoveNextAsync call to occur on a different thread from a previous or subsequent MoveNextAsync call; after all, the implementation may await a task and continue execution somewhere else. However, that doesn’t mean MoveNextAsync is “thread-safe”—far from it. On a given async enumerator, MoveNextAsync must never be invoked concurrently, meaning MoveNextAsync shouldn’t be called again on a given enumerator until the previous call to it has completed. Similarly, DisposeAsync on an iterator shouldn’t be invoked while either MoveNextAsync or DisposeAsync on that same enumerator is still in flight.

所以,不,IAsyncEnumerator<T>不支持并发。 IEnumerator<T>s也是如此。您不能从一个线程调用 Dispose 而另一个线程上的 MoveNext 是 运行。一般枚举(同步或异步)不是线程安全过程,必须同步。