c# 异步流 IAsyncEnumerable 的实现原理

来源:这里教程网 时间:2026-02-21 17:40:15 作者:

为什么 IAsyncEnumerable 不能用普通 foreach 遍历

因为

IAsyncEnumerable<t></t>
是异步拉取数据的序列,它的每个元素可能需要 await 才能拿到,而传统
foreach
是同步迭代器协议(依赖
IEnumerator<t>.MoveNext()</t>
Current
),无法挂起等待。直接写
foreach (var x in asyncStream)
会编译失败——C# 编译器只允许在
await foreach
语句中使用它。

IAsyncEnumerable 的核心是 IAsyncEnumerator

它本质上是一个“可 await 的枚举器”,关键成员是:

Current
:只读属性,返回当前元素(不触发计算)
MoveNextAsync()
:返回
ValueTask<bool></bool>
,真正触发下一项获取(可能 IO、延迟、网络请求等)

每次

await foreach
迭代时,编译器会自动展开为循环调用
MoveNextAsync()
,并 await 它;成功后才读取
Current
。这意味着每一步都可以被调度器中断、切换上下文,且支持取消(通过
CancellationToken
参数重载)。

手动实现 IAsyncEnumerable 的两种常见方式

最常用的是用

yield return
+
async
方法(C# 8+):

public static async IAsyncEnumerable<int> CountDown(int from)
{
    for (int i = from; i >= 0; i--)
    {
        await Task.Delay(100); // 模拟异步工作
        yield return i;
    }
}

编译器会将这个方法转换为一个状态机类,实现

IAsyncEnumerable<t></t>
和内部
IAsyncEnumerator<t></t>
,自动处理暂停/恢复逻辑。

另一种是手动 new 一个实现类(适合需精细控制生命周期或复用枚举器的场景),但必须注意:

GetAsyncEnumerator()
每次调用应返回**新实例**(否则并发
await foreach
会冲突);且要正确传播
CancellationToken
,否则取消信号会被忽略。

容易被忽略的陷阱:DisposeAsync 和取消传播

IAsyncEnumerable<t></t>
的枚举器实现了
IAsyncDisposable
,意味着你可以在迭代中途用
await using
确保资源释放:

await using var e = numbers.GetAsyncEnumerator();
while (await e.MoveNextAsync())
{
    Console.WriteLine(e.Current);
} // DisposeAsync() 自动调用

但如果你自己实现

IAsyncEnumerator<t></t>
,必须显式支持取消:

MoveNextAsync(CancellationToken)
必须传入 token 并参与 await(如
await task.WithCancellation(token)
DisposeAsync()
应取消未完成的异步操作,并等待清理完成
不要在
Current
中做异步计算——它不该有副作用,也不该 await

否则,用户调用

await foreach
时传入
cancellationToken
,实际不会中断正在执行的
MoveNextAsync
,导致取消失效。

相关推荐