第四章:C#异步流

第四章:C#异步流


随着异步编程在 C# 中的发展,asyncawait 为处理异步操作提供了极大的便利。然而,在处理数据流时,传统的异步方法如 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,它根据 offsetlimit 返回数据。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 创建异步流

在一些场景中,我们需要返回多个值,并对每个值执行一些异步操作。异步流提供了一种简洁的方式来实现这一需求。下面通过两种不同的方法探讨如何实现这种异步处理:

  1. 通过 IEnumerable<T> 返回多个值,然后对其进行异步处理。这是传统的同步集合处理方式,但处理每个值时需要手动将操作转为异步。
  2. 通过 Task<T> 异步返回单个值,然后再添加其他的返回值。这种方法适用于需要异步获取单个值的场景,但在需要返回多个值时,代码变得复杂且不直观。

解决方案

C# 提供了一个更简洁的方法来实现这一需求,即使用 asyncyield return 结合。通过 async 标记方法为异步,并使用 yield return 返回多个值,我们可以创建一个异步流(IAsyncEnumerable<T>)。这种方式将异步操作与流式数据生成结合在一起,只返回一个异步流,简化了代码结构。

例如:

async IAsyncEnumerable<int> GetValuesAsync()
{
await Task.Delay(1000); // 异步处理
yield return 10;
await Task.Delay(1000); // 更多异步处理
yield return 13;
}

这个简单的例子展示了如何通过 awaityield 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# 引入 asyncawait 以来,开发者们一直希望能够将它们与 yield return 一起使用,这个想法在很长一段时间内未能实现。但随着异步流的引入,.NET 和 C# 现代版本终于支持了这种组合。异步流使得我们能够以异步方式返回多个值,并且简化了代码的编写。

在这个实际示例中,异步流展示了部分数据异步处理的模式。每页的数据(假设每页有 10 个元素)中,只有 1 次异步操作是与网络请求相关的,其他的元素生成是同步的。这种异步流的设计非常常见:大多数迭代是同步的,但允许异步操作在必要时发生。

性能和设计考量

异步流的设计兼顾了异步操作和同步操作的性能优化。这也是异步流基于 ValueTask<T> 的原因之一。无论异步流的元素是同步获取还是异步获取,ValueTask<T> 的使用确保了性能的最大化。

在设计异步流时,还可以考虑支持取消操作。某些场景下,取消操作可能不是必须的,消费者可以选择不再获取下一个元素。而在其他情况下,使用 CancellationToken 可以更好地支持流操作的中途取消。

小结

异步流为处理异步数据流提供了极大的灵活性,尤其在需要逐步获取和处理数据的场景中,它简化了代码结构并提升了性能。在处理分页数据或大数据集时,异步流允许我们边获取边处理,而无需等待所有数据准备就绪。通过 asyncyield return 的结合,我们可以在一个简单且优雅的模型中处理复杂的异步数据流。

3.6 消费异步流

在处理异步流时,我们需要逐个获取异步流产生的结果,这个过程称为 异步枚举。与传统的同步枚举不同,异步枚举允许我们在等待异步数据到达的同时,逐步处理每个元素。

问题

假设我们有一个异步流(IAsyncEnumerable<T>),需要逐个处理它的结果。该过程不仅包括异步获取流中的每个元素,还可能需要对每个元素执行异步操作。

解决方案

消耗异步流可以通过 awaitforeach 的结合来实现,具体是使用 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 的行为
    1. await foreach 在等待下一个元素时,会暂停当前的代码执行,直到异步流准备好下一个元素。
    2. 如果有新的元素到达,await foreach 会进入循环代码体,执行操作。
    3. 当异步流没有更多的元素时,循环结束。

对每个元素执行异步处理

在异步流的每个元素上执行异步操作是一种常见的需求。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 操作符(如 WhereSelect 等)。同样,对于异步流(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 操作符的异步流版本

你仍然可以使用同步的 WhereSelect 来处理异步流:

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;
}
}

在这个例子中,CancellationTokenTask.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 附加到异步流的枚举器上,允许在消费流时进行取消操作。

WithCancellationConfigureAwait

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 一起使用,避免不必要的上下文切换,提高性能。
posted @   平元兄  阅读(80)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· DeepSeek “源神”启动!「GitHub 热点速览」
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· NetPad:一个.NET开源、跨平台的C#编辑器
点击右上角即可分享
微信分享提示