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 subsequentMoveNextAsync
call; after all, the implementation may await a task and continue execution somewhere else. However, that doesn’t meanMoveNextAsync
is “thread-safe”—far from it. On a given async enumerator,MoveNextAsync
must never be invoked concurrently, meaningMoveNextAsync
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 eitherMoveNextAsync
orDisposeAsync
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.