第四章:C#异步流
第四章:C#异步流
随着异步编程在 C# 中的发展,async
和 await
为处理异步操作提供了极大的便利。然而,在处理数据流时,传统的异步方法如 Task<T>
并不够灵活。为了解决这一问题,C# 8.0 引入了 异步流(IAsyncEnumerable<T>
),异步枚举是枚举(IEnumerable<T>
)的异步版本,它允许我们以异步方式逐个处理数据项,而不是同步获取集合中的每个元素。这种模式非常适合处理大量数据、网络流、文件读取等场景。
异步流通过 await foreach
语法,使我们能够逐步从异步数据源中拉取数据,同时保持代码简洁且具备良好的性能。
3.1 异步流简介
异步流和 Task<T>
Task<T>
的标准异步方法只足以应对异步处理单个数据值。一旦某个Task<T>
完成,任务就结束了。单个Task<T>
不能为其使用者提供多个T值。即使T是一个集合,该值也只能提供一次。关于如何结合Task<T>
与async
。与Task<T>
相比,异步流更类似于枚举。具体来说,IAsyncEnumerator<T>
可以提供任意数量的T值,而且一次一个。和IEnumerator<T>
一样,IAsyncEnumerator<T>
可以是无限长的。
-
示例:
Task<T>
处理单一结果public async Task<int> GetSingleValueAsync() { await Task.Delay(1000); // 模拟异步操作 return 42; } int result = await GetSingleValueAsync(); Console.WriteLine(result);
异步流和 IEnumerable<T>
顾名思义,IAsyncEnumerable<T>
与IEnumerable<T>
相似。这或许并不奇怪,它们都能让使用者以一次一个的方式获取元素。至于二者的差别,从名字上便能洞悉:一个是异步的,而另一个不是。当代码遍历IEnumerable<T>
并从枚举获取每一个元素时,它会顺带阻塞。如果IEnumerable<T>
代表某些I/O密集型操作,比如数据库查询或API调用,那么消费者代码最终会阻塞I/O,这并不理想。IAsyncEnumerable<T>
的工作方式与IEnumerable<T>
类似,但是它会异步获取下一个元素。
-
示例:
IEnumerable<T>
处理同步数据public IEnumerable<int> GetValues() { for (int i = 0; i < 5; i++) { yield return i; } } foreach (var value in GetValues()) { Console.WriteLine(value); }
异步流和 Task<IEnumerable<T>>
在异步方法中返回包含多个数据项的集合是常见的做法,例如 Task<List<T>>
。这种方法通常只有一条 return
语句,并且在返回 List<T>
之前,集合必须完全填充。同样,返回 Task<IEnumerable<T>>
也可以用于异步返回集合,但实际的枚举操作会同步进行。
需要注意的是,LINQ to Entities
提供的 ToListAsync
方法返回的是 Task<List<T>>
,在调用时必须与数据库通信,等待所有匹配的结果加载完成后,才能返回整个列表。Task<IEnumerable<T>>
的局限性在于它无法逐个返回数据项。它必须先将所有项加载到内存中,填充集合后再一次性返回,即使返回的是 LINQ 查询,枚举时也是同步的。
相比之下,IAsyncEnumerable<T>
支持异步流,它可以逐项异步返回数据,并在处理每个数据项时执行异步操作,真正实现了按需异步获取数据的特性。
-
示例:
Task<IEnumerable<T>>
处理完整的异步集合public async Task<IEnumerable<int>> GetValuesAsync() { var values = new List<int>(); for (int i = 0; i < 5; i++) { await Task.Delay(500); // 模拟异步操作 values.Add(i); } return values; } var result = await GetValuesAsync(); foreach (var value in result) { Console.WriteLine(value); }
异步流和 IObservable<T>
IObservable<T>
和 异步流(IAsyncEnumerable<T>
)都可以用于处理异步数据流,但它们有本质上的不同:
-
IObservable<T>
:是一种 推送 模式(push-based),数据源主动将数据推送给订阅者。订阅者通过实现IObserver<T>
接口来接收数据和错误通知。典型的应用场景是事件、消息通知等。 -
IAsyncEnumerable<T>
:是一种 拉取 模式(pull-based),消费者通过await foreach
逐一获取数据。数据源不会主动推送数据,而是等待消费者请求数据时才产生下一个数据。 -
示例:
IObservable<T>
的推送模式public class NumberPublisher : IObservable<int> { private List<IObserver<int>> observers = new List<IObserver<int>>(); public IDisposable Subscribe(IObserver<int> observer) { observers.Add(observer); return new Unsubscriber(observers, observer); } public async Task PublishNumbersAsync() { for (int i = 0; i < 5; i++) { await Task.Delay(500); // 模拟异步操作 foreach (var observer in observers) { observer.OnNext(i); // 推送数据给所有订阅者 } } foreach (var observer in observers) { observer.OnCompleted(); // 通知完成 } } } public class NumberObserver : IObserver<int> { public void OnNext(int value) => Console.WriteLine($"接收到: {value}"); public void OnError(Exception error) => Console.WriteLine($"错误: {error.Message}"); public void OnCompleted() => Console.WriteLine("完成"); } -
示例:
IAsyncEnumerable<T>
的拉取模式public async IAsyncEnumerable<int> GetAsyncNumbers() { for (int i = 0; i < 5; i++) { await Task.Delay(500); // 模拟异步操作 yield return i; } } await foreach (var value in GetAsyncNumbers()) { Console.WriteLine($"拉取到: {value}"); }
区别:
IObservable<T>
推送数据:数据源主动推送数据(类似于事件驱动),订阅者被动接收。IAsyncEnumerable<T>
拉取数据:消费者通过await foreach
主动拉取数据。
异步流可以看作是异步版本的 IEnumerable<T>
,而 IObservable<T>
则是基于事件的异步推送机制。两者在使用场景上存在一定的重叠,但在处理数据流的方式上有根本区别。
小结
为了让不同返回类型的使用场景更加直观,以下是对应的代码示例,展示如何从分页 API 中获取数据。
示例 API 假设
我们假设有一个分页 API,它根据 offset
和 limit
返回数据。API 方法签名如下:
Task<List<int>> FetchPageAsync(int offset, int limit);
该 API 返回一页包含整数的列表。接下来,我们会展示使用不同返回类型来实现分页数据获取的方法。
1. 返回 Task<T>
// 假设只需要获取第一页的数据。 public async Task<List<int>> GetFirstPageAsync() { return await FetchPageAsync(0, 10); } // 调用 var result = await GetFirstPageAsync(); Console.WriteLine($"数据: {string.Join(", ", result)}");
优点:
- 简单、易于使用。
- 适用于只需调用一次 API 的场景。
缺点:
- 无法处理分页场景,不能多次调用 API 获取完整数据。
2. 返回 IEnumerable<T>
// 同步分页实现。 public IEnumerable<int> GetAllData() { int offset = 0; const int limit = 10; while (true) { var page = FetchPageAsync(offset, limit).Result; // 同步调用 if (page.Count == 0) yield break; foreach (var item in page) { yield return item; } offset += limit; } } // 调用 foreach (var item in GetAllData()) { Console.WriteLine(item); }
优点:
- 支持分页,且使用
yield return
实现延迟加载。
缺点:
- 同步阻塞 API 调用,无法利用异步编程的优势。
3. 返回 Task<List<T>>
// 异步分页实现,累积所有结果并一次性返回。 public async Task<List<int>> GetAllDataAsync() { var allData = new List<int>(); int offset = 0; const int limit = 10; while (true) { var page = await FetchPageAsync(offset, limit); if (page.Count == 0) break; allData.AddRange(page); offset += limit; } return allData; } // 调用 var result = await GetAllDataAsync(); Console.WriteLine($"所有数据: {string.Join(", ", result)}");
优点:
- 异步调用 API,避免同步阻塞。
缺点:
- 必须等待所有数据获取完成后才能返回,不能边获取边处理。
4. 返回 IObservable<T>
using System; using System.Reactive.Linq; // 使用 Reactive Extensions 实现推式模型。 public IObservable<int> GetDataAsObservable() { return Observable.Create<int>(async (observer, cancellationToken) => { int offset = 0; const int limit = 10; while (!cancellationToken.IsCancellationRequested) { var page = await FetchPageAsync(offset, limit); if (page.Count == 0) break; foreach (var item in page) { observer.OnNext(item); } offset += limit; } observer.OnCompleted(); }); } // 调用 GetDataAsObservable().Subscribe( onNext: item => Console.WriteLine($"收到数据: {item}"), onCompleted: () => Console.WriteLine("数据接收完成"));
优点:
- 基于推式模型,适合实时数据流处理。
- 可以在数据项到达时立即处理。
缺点:
- 适合推式数据流,但分页 API 通常更适合拉式模型。
5. 返回 IAsyncEnumerable<T>
// 使用 C# 8.0 引入的 IAsyncEnumerable<T> 实现异步分页。 public async IAsyncEnumerable<int> GetAllDataAsyncEnumerable() { int offset = 0; const int limit = 10; while (true) { var page = await FetchPageAsync(offset, limit); if (page.Count == 0) yield break; foreach (var item in page) { yield return item; } offset += limit; } } // 调用 await foreach (var item in GetAllDataAsyncEnumerable()) { Console.WriteLine($"异步接收到数据: {item}"); }
优点:
- 支持异步和延迟加载,适合分页 API 场景。
- 调用者可以边获取边处理,无需等待所有数据获取完成。
缺点:
- 需要调用者支持
await foreach
,这在较新的 C# 版本中才可用(C# 8.0 及以上)。
几种方式对比总结
返回类型 | 单值/多值 | 同步/异步 | 推式/拉式 | 优点 | 缺点 |
---|---|---|---|---|---|
T |
单值 | 同步 | 都不是 | 简单易用 | 无法处理分页、异步场景 |
IEnumerable<T> |
多值 | 同步 | 拉式 | 延迟加载,适合简单场景 | 不支持异步调用 |
Task<T> |
单值 | 异步 | 拉式 | 简单易用 | 无法处理分页场景 |
Task<List<T>> |
单个集合值(单值) | 异步 | 拉式 | 异步调用,避免阻塞 | 无法边获取边处理 |
IObservable<T> |
单值或多值 | 异步 | 推式 | 适合实时消息推送 | 对于分页 API 场景可能不合适 |
IAsyncEnumerable<T> |
多值 | 异步 | 拉式 | 支持异步和延迟加载 | 需要使用 await foreach |
在大多数分页 API 场景中,IAsyncEnumerable<T>
是最佳选择,能够兼顾异步和延迟加载特性。如果处理实时推送数据,IObservable<T>
会是更好的选择,但是IObservable<T>
学习门槛要高一些。
3.5 创建异步流
在一些场景中,我们需要返回多个值,并对每个值执行一些异步操作。异步流提供了一种简洁的方式来实现这一需求。下面通过两种不同的方法探讨如何实现这种异步处理:
- 通过
IEnumerable<T>
返回多个值,然后对其进行异步处理。这是传统的同步集合处理方式,但处理每个值时需要手动将操作转为异步。 - 通过
Task<T>
异步返回单个值,然后再添加其他的返回值。这种方法适用于需要异步获取单个值的场景,但在需要返回多个值时,代码变得复杂且不直观。
解决方案
C# 提供了一个更简洁的方法来实现这一需求,即使用 async
和 yield return
结合。通过 async
标记方法为异步,并使用 yield return
返回多个值,我们可以创建一个异步流(IAsyncEnumerable<T>
)。这种方式将异步操作与流式数据生成结合在一起,只返回一个异步流,简化了代码结构。
例如:
async IAsyncEnumerable<int> GetValuesAsync() { await Task.Delay(1000); // 异步处理 yield return 10; await Task.Delay(1000); // 更多异步处理 yield return 13; }
这个简单的例子展示了如何通过 await
和 yield return
来创建异步流。在这个方法中,异步操作执行后,使用 yield return
逐个返回异步生成的值。
更实际的示例:API 分页处理
在实际应用中,异步流可以非常有效地处理分页数据。例如,通过 HttpClient
从 API 异步获取分页数据,并逐页返回每个值:
async IAsyncEnumerable<string> GetValuesAsync(HttpClient client) { int offset = 0; const int limit = 10; while (true) { // 异步获取当前页的数据 string result = await client.GetStringAsync( $"https://exampleurl/api/values?offset={offset}&limit={limit}"); // 解析当前页的数据 string[] valuesOnThisPage = result.Split('\n'); // 异步返回该页的每个值 foreach (string value in valuesOnThisPage) { yield return value; } // 如果当前页的元素数量小于限制值,说明达到最后一页,退出循环 if (valuesOnThisPage.Length != limit) { break; } // 处理下一页 offset += limit; } }
示例说明
- 分页请求:
GetValuesAsync
方法通过HttpClient
异步请求 API 的分页数据,每次请求一页(假设每页返回 10 个元素)。每个分页请求都是异步的,通过await
获取结果。 - 逐步生成数据:当一页的数据到达后,使用
yield return
逐个返回该页中的每个值,而不需要等待所有分页的数据到达。这意味着消费者可以在第一页数据到达时就开始处理,而不是等待整个数据集加载完成。 - 分页控制:当某一页返回的元素数量小于
limit
时,表示已经是最后一页,退出循环。
这个异步流的实现方式使得数据处理更加高效,尤其是在处理大量数据时,消费者可以边获取边处理,而无需等待整个数据集的加载完成。
异步流的讨论
自从 C# 引入 async
和 await
以来,开发者们一直希望能够将它们与 yield return
一起使用,这个想法在很长一段时间内未能实现。但随着异步流的引入,.NET 和 C# 现代版本终于支持了这种组合。异步流使得我们能够以异步方式返回多个值,并且简化了代码的编写。
在这个实际示例中,异步流展示了部分数据异步处理的模式。每页的数据(假设每页有 10 个元素)中,只有 1 次异步操作是与网络请求相关的,其他的元素生成是同步的。这种异步流的设计非常常见:大多数迭代是同步的,但允许异步操作在必要时发生。
性能和设计考量
异步流的设计兼顾了异步操作和同步操作的性能优化。这也是异步流基于 ValueTask<T>
的原因之一。无论异步流的元素是同步获取还是异步获取,ValueTask<T>
的使用确保了性能的最大化。
在设计异步流时,还可以考虑支持取消操作。某些场景下,取消操作可能不是必须的,消费者可以选择不再获取下一个元素。而在其他情况下,使用 CancellationToken
可以更好地支持流操作的中途取消。
小结
异步流为处理异步数据流提供了极大的灵活性,尤其在需要逐步获取和处理数据的场景中,它简化了代码结构并提升了性能。在处理分页数据或大数据集时,异步流允许我们边获取边处理,而无需等待所有数据准备就绪。通过 async
和 yield return
的结合,我们可以在一个简单且优雅的模型中处理复杂的异步数据流。
3.6 消费异步流
在处理异步流时,我们需要逐个获取异步流产生的结果,这个过程称为 异步枚举。与传统的同步枚举不同,异步枚举允许我们在等待异步数据到达的同时,逐步处理每个元素。
问题
假设我们有一个异步流(IAsyncEnumerable<T>
),需要逐个处理它的结果。该过程不仅包括异步获取流中的每个元素,还可能需要对每个元素执行异步操作。
解决方案
消耗异步流可以通过 await
和 foreach
的结合来实现,具体是使用 await foreach
语法。该语法允许我们异步地枚举每个元素,并在每个元素上执行操作。
示例:异步枚举 API 响应分页
假设我们有一个方法 GetValuesAsync
,它返回一个分页的 API 结果流,我们可以通过 await foreach
来逐个异步处理这些结果,并将它们输出到控制台:
IAsyncEnumerable<string> GetValuesAsync(HttpClient client); public async Task ProcessValuesAsync(HttpClient client) { await foreach (string value in GetValuesAsync(client)) { Console.WriteLine(value); } }
工作原理
从概念上看,当执行 GetValuesAsync
时,它会返回一个 IAsyncEnumerable<T>
类型的异步流。随后,foreach
语句会为该异步流创建一个 异步枚举器。与同步枚举器类似,异步枚举器用于获取下一个元素,但该获取操作可能是异步的。
await foreach
的行为:await foreach
在等待下一个元素时,会暂停当前的代码执行,直到异步流准备好下一个元素。- 如果有新的元素到达,
await foreach
会进入循环代码体,执行操作。 - 当异步流没有更多的元素时,循环结束。
对每个元素执行异步处理
在异步流的每个元素上执行异步操作是一种常见的需求。await foreach
支持在循环体中执行异步操作,例如在处理每个元素时添加一个延迟:
IAsyncEnumerable<string> GetValuesAsync(HttpClient client); public async Task ProcessValuesAsync(HttpClient client) { await foreach (string value in GetValuesAsync(client)) { await Task.Delay(100); // 异步处理 Console.WriteLine(value); } }
在这个例子中,await foreach
会异步获取流中的第一个元素,然后执行循环体中的异步操作(如延迟 100 毫秒)。接着,它会异步获取下一个元素并再次执行循环体中的操作,直到所有元素处理完毕。
await foreach
的隐含 await
在 await foreach
内部,获取下一个元素的操作实际上是一个异步操作,类似于使用 await
等待异步任务的完成。当下一个元素准备好时,代码会继续执行。如果异步流完成了所有元素的生成,循环会自动退出。
使用 ConfigureAwait(false)
通常,在异步代码中我们可以使用 ConfigureAwait(false)
来避免捕获同步上下文。对于异步流,同样可以使用 ConfigureAwait(false)
来优化性能。它会传递给 await foreach
内部的隐含 await
调用,以防止不必要的上下文切换。
示例:使用 ConfigureAwait(false)
IAsyncEnumerable<string> GetValuesAsync(HttpClient client); public async Task ProcessValuesAsync(HttpClient client) { await foreach (string value in GetValuesAsync(client).ConfigureAwait(false)) { await Task.Delay(100).ConfigureAwait(false); // 异步处理 Console.WriteLine(value); } }
在这个例子中,ConfigureAwait(false)
被应用于 await foreach
和每个 Task.Delay
,这可以避免不必要的上下文切换,从而提高性能。
讨论
-
await foreach
是消耗异步流的自然方式:
C# 提供了await foreach
来简化异步流的消费。它与传统的foreach
逻辑类似,但允许异步获取元素并异步处理每个元素。 -
上下文捕获与
ConfigureAwait(false)
:
ConfigureAwait(false)
是一种常见的优化手段,用于避免不必要的上下文捕获。在异步流中,它同样适用,可以传递给await foreach
,以提高异步操作的效率。 -
支持取消操作:
传递取消令牌是异步流中的一种高级操作,可以用于中断流的枚举。尽管await foreach
能够自然地消费异步流,但在某些复杂场景中,使用取消令牌可以更好地控制流的生命周期。 -
异步 LINQ 运算符:
虽然await foreach
是处理异步流的直观方式,但有时我们可以使用更高级的异步 LINQ 运算符库来处理异步流。 -
同步和异步代码的混合:
await foreach
的代码体既可以是同步的,也可以是异步的。在处理异步操作(如网络请求)时,await foreach
允许我们以异步的方式获取并处理数据。 -
与其他流抽象的对比:
与IObservable<T>
等其他流抽象不同,await foreach
允许异步的处理方式,而IObservable<T>
是基于同步订阅模型的。这种区别使得await foreach
更加灵活,尤其在涉及异步处理时。
小结
await foreach
是消耗异步流的最自然方式,它允许我们逐个异步地获取流中的元素,并进行同步或异步处理。通过结合 ConfigureAwait(false)
,可以进一步优化上下文切换,提高性能。在处理复杂的异步流时,还可以通过取消令牌来中断流的执行,或使用异步 LINQ 运算符来简化流的处理。
3.7 对异步流使用 LINQ
问题
我们希望用熟悉的 LINQ 操作来处理异步流(IAsyncEnumerable<T>
),并且支持异步条件处理。如何优雅地在异步流上使用 LINQ?
解决方案
在同步流(IEnumerable<T>
)上,我们可以使用 LINQ 操作符(如 Where
、Select
等)。同样,对于异步流(IAsyncEnumerable<T>
),可以通过 System.Linq.Async 库提供的扩展方法,使用类似的 LINQ 操作符来处理数据。
示例:异步条件的 Where
传统的 Where
不能处理异步条件,因为它要求同步返回结果。为了解决这个问题,System.Linq.Async
提供了 WhereAwait
,它可以处理异步条件:
IAsyncEnumerable<int> values = SlowRange().WhereAwait(async value => { await Task.Delay(10); // 异步操作 return value % 2 == 0; // 只保留偶数 }); await foreach (int result in values) { Console.WriteLine(result); } async IAsyncEnumerable<int> SlowRange() { for (int i = 0; i < 10; ++i) { await Task.Delay(i * 100); // 模拟延迟 yield return i; } }
在这里,WhereAwait
可以处理异步操作,筛选出符合条件的元素。
同步 LINQ 操作符的异步流版本
你仍然可以使用同步的 Where
或 Select
来处理异步流:
IAsyncEnumerable<int> values = SlowRange().Where(value => value % 2 == 0); await foreach (int result in values) { Console.WriteLine(result); }
即使这些操作是同步的,它们仍然返回异步流,适合与 await foreach
一起使用。
常见的异步 LINQ 操作符
Where
/WhereAwait
:筛选元素,WhereAwait
支持异步条件。Select
/SelectAwait
:转换元素,SelectAwait
支持异步转换。CountAsync
/CountAwaitAsync
:计算元素数量,支持同步和异步条件。
示例:异步终止运算符 CountAwaitAsync
如果你想根据异步条件计算元素数量,可以使用 CountAwaitAsync
:
int count = await SlowRange().CountAwaitAsync(async value => { await Task.Delay(10); // 异步操作 return value % 2 == 0; });
CountAwaitAsync
允许你使用异步条件来计算符合条件的元素数量。
运算符命名规则
Await
后缀:用于接受异步委托的运算符(如WhereAwait
)。Async
后缀:用于返回单个异步结果而非异步流的运算符(如CountAsync
)。
总结
System.Linq.Async
提供了常见的 LINQ 操作符,支持异步流处理。- 使用
Await
结尾的运算符来处理异步条件,使用Async
结尾的运算符来返回异步结果。 - 这些运算符让处理异步流变得更加简单和直观,使异步编程与 LINQ 结合得更紧密。
3.8 异步流及其取消操作
问题
如何取消异步流的执行?
解决方案
并非所有异步流都需要显式取消。在某些情况下,简单地停止枚举就可以达到取消的效果。例如,使用 break
停止 await foreach
循环:
await foreach (int result in SlowRange()) { Console.WriteLine(result); if (result >= 8) break; // 手动停止 } // 生成一个逐渐减缓的序列 async IAsyncEnumerable<int> SlowRange() { for (int i = 0; i != 10; ++i) { await Task.Delay(i * 100); // 模拟异步操作 yield return i; } }
这种方式适用于简单场景,但在需要更灵活的取消机制时,可以使用 CancellationToken
。
使用 CancellationToken
取消异步流
某些运算符支持传递取消令牌,用于从外部代码取消 await foreach
。你可以通过为异步流方法添加 CancellationToken
参数来实现取消操作:
using var cts = new CancellationTokenSource(500); // 500ms后取消 CancellationToken token = cts.Token; await foreach (int result in SlowRange(token)) { Console.WriteLine(result); } // 生成一个逐渐减缓的序列,支持取消 async IAsyncEnumerable<int> SlowRange([EnumeratorCancellation] CancellationToken token = default) { for (int i = 0; i != 10; ++i) { await Task.Delay(i * 100, token); // 支持取消 yield return i; } }
在这个例子中,CancellationToken
在 Task.Delay
中被使用,当取消令牌触发时,流中的异步操作会停止。
讨论
在上面的例子中,取消令牌直接传递给生成异步枚举的方法。这是处理异步流取消的常见做法。还有另一种场景是:你可能会收到一个异步流,并需要在消费时应用取消令牌。这时,可以使用 WithCancellation
扩展方法。
示例:WithCancellation
用法
WithCancellation
扩展方法允许你在消费异步流时附加 CancellationToken
,无需修改流的生成逻辑:
async Task ConsumeSequence(IAsyncEnumerable<int> items) { using var cts = new CancellationTokenSource(500); // 500ms后取消 CancellationToken token = cts.Token; await foreach (int result in items.WithCancellation(token)) { Console.WriteLine(result); } } // 生成一个逐渐减缓的序列 async IAsyncEnumerable<int> SlowRange([EnumeratorCancellation] CancellationToken token = default) { for (int i = 0; i != 10; ++i) { await Task.Delay(i * 100, token); // 支持取消 yield return i; } } await ConsumeSequence(SlowRange());
WithCancellation
方法会将 CancellationToken
附加到异步流的枚举器上,允许在消费流时进行取消操作。
WithCancellation
与 ConfigureAwait
WithCancellation
可以与 ConfigureAwait(false)
配合使用,防止上下文捕获,提高异步流的性能:
async Task ConsumeSequence(IAsyncEnumerable<int> items) { using var cts = new CancellationTokenSource(500); // 500ms后取消 CancellationToken token = cts.Token; await foreach (int result in items.WithCancellation(token).ConfigureAwait(false)) { Console.WriteLine(result); } }
这种组合可以同时实现取消操作和性能优化。
小结
- 简单取消:可以通过
break
手动停止await foreach
循环。 CancellationToken
:用于更灵活的取消异步流,通过传递给异步流生成器或使用WithCancellation
实现。WithCancellation
:扩展方法允许你在消费异步流时附加取消令牌,适用于不修改流生成逻辑的场景。ConfigureAwait(false)
:可以与WithCancellation
一起使用,避免不必要的上下文切换,提高性能。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· DeepSeek “源神”启动!「GitHub 热点速览」
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· NetPad:一个.NET开源、跨平台的C#编辑器