2

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;
    }
}
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
maloo
  • 685
  • 1
  • 6
  • 16

1 Answers1

2

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.

Robert Harvey
  • 178,213
  • 47
  • 333
  • 501
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • 1
    ahh, nice find :) – TheGeneral Nov 05 '21 at 00:59
  • 1
    So I assume cancellation is the only option to properly dispose am IAsyncEnumerable. That is, supply a cancellation token to GetEnumerator(), have the async iterator be cancellable ([EnumeratorCancellation]). Then on timout cancel the iterator, await Task.WhenAny(lastMoveNextAsyncTask), then call await DisposeAsync() – maloo Nov 05 '21 at 01:36
  • @maloo yeap, pretty much that's it. – Theodor Zoulias Nov 05 '21 at 01:51
  • 1
    **Bottom line** *"Enumerating in general (synchronously or asynchronously) is not a thread-safe procedure, and must be synchronized."* – Kamran Sep 14 '22 at 20:36