必须用 async IAsyncEnumerable<T> 作返回类型,否则失去流式异步价值;常见空流因误写为 Task<IAsyncEnumerable<T>> 或缺少 yield return;手动封装需用 MemoryPool 租借独立缓冲区并及时归还;CancellationToken 需加 [EnumeratorCancellation] 且在 yield 前主动检查。

必须用 async IAsyncEnumerable<T> 做返回类型,不能用 Task<IAsyncEnumerable<T>> 或 IEnumerable<T> 包一层——否则就不是流,而是“等全量加载完才吐出集合”,失去异步流价值。
为什么 await foreach 一用就空或立刻结束
常见现象:方法写了 yield return,但调用方 await foreach 拿不到任何元素,甚至不进循环体。
- 根本原因:返回类型写成了
public async Task<IAsyncEnumerable<string>> GetData()—— 编译器允许,但实际返回的是未启动的状态机,await foreach调用GetAsyncEnumerator()时得到一个“已完成但无数据”的枚举器 - 正确签名只有一种:
public async IAsyncEnumerable<string> GetData(),且方法体内必须有至少一次yield return - 如果逻辑里用了
await但没yield return(比如只做初始化或校验),也会导致空流;编译器不报错,但行为不符合预期
手动封装 Stream 时内存错乱的根源
想对 FileStream 或网络流做块级异步迭代?直接 yield return buffer.AsMemory(0, read) 是危险的。
-
buffer是复用的字节数组,下一次ReadAsync()会覆盖内容,消费者拿到的Memory<byte>可能指向已被改写的内存 - 必须用
MemoryPool<byte>.Shared.Rent(int)每次分配独立内存块,再yield return memory - 必须在
finally或using中调用rent.Return(),否则租借数持续增长,最终触发OutOfMemoryException - 缓冲区大小建议设为
4096或8192:太小增加调度开销,太大浪费池空间
CancellationToken 不生效的三个关键点
传了 CancellationToken 却无法取消?问题通常不在参数本身,而在使用方式。
-
[EnumeratorCancellation]必须加在CancellationToken参数上,否则调用方用.WithCancellation(ct)时信号不会透传进迭代器体 - 底层 API(如
HttpClient.GetAsync、StreamReader.ReadLineAsync)必须真正接受并响应该 token;只传进去但没用在await表达式里,等于没传 -
ct.ThrowIfCancellationRequested()要主动检查位置:放在yield return前最安全,避免已生成的数据被中断丢弃
最容易被忽略的是:异步流的资源清理不是自动的。await foreach 会隐式调用 DisposeAsync(),但前提是你的迭代器方法里没有漏掉 await using 或 try/finally —— 比如忘了在 finally 里释放 rent,或者没把 StreamReader 包在 await using 里,就会导致连接/句柄泄漏。这不是“写完就能跑通”的功能,而是每一步都得对齐生命周期。


















