Home > Enterprise >  Should IAsyncEnumerator<T>.DisposeAsync() throw if the last MoveNextAsync() task is not awaite
Should IAsyncEnumerator<T>.DisposeAsync() throw if the last MoveNextAsync() task is not awaite

Time:11-05

Why does the UseEventsFail throw in the code below? Could it be that I dispose the async enumerator without awaiting the last MoveNextAsync() task? This example is simplified repro of my real program, so I need to dispose the async enumerator to release its resources. And Task.CompletedTask is usually a Task.Delay() used as a timeout for UseEvents(). If the enumerator task completes before the timeout task, then no exception is thrown.

Stack trace of exception:

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

Code:

// 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;
    }
}

CodePudding user response:

From the MSDN article Iterating with Async Enumerables in C# 8 by Stephen Toub:

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.

So, no, IAsyncEnumerator<T>s do not support concurrency. The same is true for IEnumerator<T>s. You can't call Dispose from one thread while a MoveNext is running on another thread. Enumerating in general (synchronously or asynchronously) is not a thread-safe procedure, and must be synchronized.

  • Related