迭代时如何跳出 IAsyncEnumerable?

Bru*_*ell 1 c# c#-8.0 iasyncenumerable

C# 8 添加了对异步迭代器块的支持,因此您可以等待事物并返回一个IAsyncEnumarator而不是一个IEnumerable

public async IAsyncEnumerable<int> EnumerateAsync() {
    for (int i = 0; i < 10; i++) {
        yield return i;
        await Task.Delay(1000);
    }
}
Run Code Online (Sandbox Code Playgroud)

使用如下所示的非阻塞消费代码:

await foreach (var item in EnumerateAsync()) {
    Console.WriteLine(item);
}
Run Code Online (Sandbox Code Playgroud)

这将导致我的代码运行大约 10 秒。但是,有时我想await foreach在所有元素都被消耗掉之前突破。随着break然而,我们需要等到期待已久目前Task.Delay已经完成。我们如何在不等待任何悬而未决的异步任务的情况下立即跳出该循环?

Bru*_*ell 5

使用 aCancellationToken是解决方案,因为这是唯一可以取消Task.Delay代码中的东西。我们在你的内部获取它的方法IAsyncEnumerable是在创建它时将它作为参数传递,所以让我们这样做:

public async IAsyncEnumerable<int> EnumerateAsync(CancellationToken cancellationToken = default) {
    for (int i = 0; i < 10; i++) {
        yield return i;
        await Task.Delay(1000, cancellationToken);
    }
}
Run Code Online (Sandbox Code Playgroud)

消费方面:

// In this example the cancellation token will be caneled after 2.5 seconds
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2.5));
await foreach (var item in EnumerateAsync(cts.Token)) {
    Console.WriteLine(item);
}
Run Code Online (Sandbox Code Playgroud)

当然,这将在返回 3 个元素后取消枚举,但将以TaskCanceledException抛出Task.Delay. 为了优雅地退出await foreach我们必须抓住它并在生产端打破:

public async IAsyncEnumerable<int> EnumerateAsync(CancellationToken cancellationToken = default) {
    for (int i = 0; i < 10; i++) {
        yield return i;
        try {
            await Task.Delay(1000, cancellationToken);
        } catch (TaskCanceledException) {
            yield break;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

笔记

截至目前,这仍处于预览阶段,可能会发生变化。如果你有兴趣这个主题,你可以观看C#语言团队的讨论大约CancellationTokenIAsyncEnumeration

  • 我只想补充一点,VS2019 和 Core 3 的 preview2 将对`CancellationToken` 有更好的支持。我们将提供一个 `.WithCancellationToken()` 扩展方法。见 LDM 会议记录:https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-11-28.md#cancellation-of-async-streams (3认同)