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
MoveNextAsynccall to occur on a different thread from a previous or subsequentMoveNextAsynccall; after all, the implementation may await a task and continue execution somewhere else. However, that doesn’t meanMoveNextAsyncis “thread-safe”—far from it. On a given async enumerator,MoveNextAsyncmust never be invoked concurrently, meaningMoveNextAsyncshouldn’t be called again on a given enumerator until the previous call to it has completed. Similarly,DisposeAsyncon an iterator shouldn’t be invoked while eitherMoveNextAsyncorDisposeAsyncon 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.
