异步流是C# 8.0引入的重要特性,它解决了“异步产生数据”与“逐个消费数据”无法很好结合的问题。在它出现之前,如果一个方法既要执行异步操作,又要像迭代器那样逐个产出元素,几乎无法优雅实现。本文将系统讲解异步流的原理、语法与实战用法。

一、什么是异步流,它解决了什么问题
我们先从一个典型场景说起:假设需要从数据库或网络接口读取一批数据,数据量可能很大,如果一次性加载到内存中的一个List里,既浪费内存,又让用户长时间等待首条数据。理想的方式是:每读到一条就处理一条,同时读取动作本身又是异步的,不会阻塞线程。
同步迭代器通过IEnumerable和yield return实现了惰性求值,但它内部无法使用await;返回Task<List<T>的方法虽然支持异步,却必须等全部数据就绪后一次性返回。异步流正是两者的结合体:方法声明为async IAsyncEnumerable<T>,方法体内既可以写await,也可以写yield return,消费端则使用await foreach逐个接收元素。
这样带来的好处是明显的:内存占用从O(n)降到O(1)级别,首条数据的到达时间大幅提前,整体吞吐和响应速度都得到改善。需要注意的是,编译器会把这类方法编译成一个状态机,与IAsyncEnumerator配合,通过MoveNextAsync方法异步推进迭代。
二、基本语法:生产端与消费端的写法
生产端的写法非常直观,只需将返回类型改为IAsyncEnumerable<T>,并标记为async:
public async IAsyncEnumerable<int> GenerateNumbersAsync(int count)
{
for (int i = 1; i <= count; i++)
{
// 模拟异步操作,比如网络请求或数据库查询
await Task.Delay(300);
yield return i;
}
}消费端使用await foreach来遍历,它每次会异步等待下一个元素的到来:
public async Task ConsumeAsync()
{
await foreach (var number in GenerateNumbersAsync(5))
{
Console.WriteLine($"收到数据:{number}");
}
}这段代码每隔300毫秒输出一条数据,整个过程没有占用额外的线程去阻塞等待。如果换用Task<List<int>>的方案,用户必须等满1.5秒才能看到任何输出,而异步流在300毫秒后就能看到第一条,这就是体验上的本质区别。
还要注意一点:异步流方法本身不会立即开始执行,只有在消费端调用MoveNextAsync(即await foreach开始迭代)时,方法体才会逐步执行,这与同步迭代器的惰性执行行为一致。
三、取消支持与异常处理
长时间运行的异步流应该支持取消。标准做法是使用[EnumeratorCancellation]特性修饰CancellationToken参数,这样await foreach传入的令牌就能正确传递到生产端:
public async IAsyncEnumerable<int> ReadDataAsync(
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
for (int i = 0; i < 1000; i++)
{
cancellationToken.ThrowIfCancellationRequested();
await Task.Delay(200, cancellationToken);
yield return i;
}
}
// 消费端写法
await foreach (var item in ReadDataAsync().WithCancellation(ct))
{
Console.WriteLine(item);
}如果不加[EnumeratorCancellation]特性,直接传递给WithCancellation的令牌在编译层面不会与方法参数关联,取消将无法生效。这是初学者最常踩的坑之一。
异常处理方面,生产端抛出的异常会在await foreach的等待点上抛出,可以用普通的try-catch包裹整个循环。另外建议使用await using来配合实现了IAsyncDisposable的资源,确保迭代中途中断时资源也能正确释放。
四、典型应用场景与注意事项
异步流最适合以下几类场景:一是数据库逐行读取,例如Entity Framework Core的AsAsyncEnumerable()方法就返回异步流,配合await foreach可以边查边处理;二是大文件分块解析,比如逐行读取日志文件时不阻塞线程;三是调用分页接口时不断拉取下一页,对外呈现为一个连续的序列。
// EF Core 中逐条处理数据,避免一次性加载整表
await foreach (var order in db.Orders
.Where(o => o.CreatedAt >= startDate)
.AsAsyncEnumerable())
{
await ProcessOrderAsync(order);
}使用时有几个注意点需要牢记。第一,一个IAsyncEnumerable实例通常只能被遍历一次,重复遍历需要重新创建或者用工厂方法;第二,在ASP.NET Core中返回异步流给接口响应时,会以流式方式写出JSON数据,客户端可以渐进式接收,这对大结果集非常友好;第三,不要在异步流方法里做过于耗时的同步操作,否则会阻塞迭代节奏,影响消费端体验。
总结来说,异步流把惰性迭代和异步等待这两个能力优雅地组合在一起,只要牢记async IAsyncEnumerable加yield return的生产端写法、await foreach的消费端写法,以及取消令牌的正确传递方式,就能在数据处理场景中显著提升程序的性能与响应能力。
C#异步流IAsyncEnumerableawait foreach修改时间:2026-08-31 23:19:00