《Concurrency in C# Cookbook》--- 读书随记(3)
CHAPTER 3 Asynchronous Streams
《Concurrency in C# Cookbook》
Asynchronous, Parallel, and Multithreaded ProgrammingAuthor: Stephen Cleary
如果需要电子书的小伙伴,可以留下邮箱,看到了会发送的
异步流是异步接收多个数据项的一种方法。它们是基于异步enumerables构建的(IAsyncEnumable < T >)。异步enumerable是enumerable的异步版本; 也就是说,它可以根据需要为使用者生成项,并且每个项都可以异步生成。我发现将异步流与其他可能更为熟悉的类型进行对比并考虑其差异是有用的。这有助于我记住何时使用异步流以及何时使用其他类型更合适
Asynchronous Streams and Task< T >
使用 Task < T > 的标准异步方法只能异步处理单个数据值。一旦一个给定的 Task < T > 完成,就完成了; 一个 Task < T > 不能为其使用者提供多个 T 值。即使 T 是一个集合,该值也只能提供一次
当将 Task < T > 与异步流进行比较时,异步流更类似于 enumerables。具体来说,IAsyncEnumerator < T > 可以提供任意数量的 T 值,一次一个。与 IEnumerator < T > 一样,IAsyncEnumerator < T > 的长度可能是无限的
Asynchronous Streams and IEnumerable< T >
当您的代码迭代 IEnumable < T > 时,它会在从枚举中检索每个元素时阻塞。如果 IEnumable < T > 表示某些 I/O 绑定操作,比如数据库查询或 API 调用,那么使用代码最终会阻塞 I/O,这并不理想。IAsyncEnumable < T > 的工作方式与 IEnumable < T > 类似,只不过它异步检索每个下一个元素
Asynchronous Streams and Task< IEnumerable< T >>
完全可以异步返回包含多个项的集合; 一个常见的例子是 Task < List < T>> 。尽管如此,返回 List < T > 的异步方法只获得一条 return 语句; 集合在返回之前必须完全填充。即使返回 Task < iEnumable < T>> 的方法也可能异步返回一个 enumerable,但是这个 enumerable 是同步计算的。假设 LINQ-to-Entity 有一个 ToListAsync LINQ 方法,它返回 Task < List < T>>。当 LINQ 提供程序执行此操作时,它必须与数据库进行通信,并在完成填充列表并返回之前获得所有匹配的响应
Task < IEnumable < T > > 的局限性在于,它不能在获取项目时返回它们; 如果返回一个集合,它必须将所有项目加载到内存中,填充该集合,然后一次返回整个集合。即使它返回一个 LINQ 查询,它也可以异步构建该查询,但是一旦返回该查询,就会同步从该查询中检索每个条目。IAsyncEnumable < T > 也异步返回多个项,但区别在于,IAsyncEnumable < T > 可以对返回的每个项异步操作。这是一个真正的异步流
3.1 Creating Asynchronous Streams
Problem
您需要返回多个值,每个值可能需要一些异步工作
Solution
从一个方法返回多个值可以通过 yield return 完成,而异步方法使用 async 和 await。对于异步流,您可以组合这两个; 只需使用 IAsyncEnumable < T > 的返回类型
async IAsyncEnumerable<int> GetValuesAsync()
{
await Task.Delay(1000); // some asynchronous work
yield return 10;
await Task.Delay(1000); // more asynchronous work
yield return 13;
}
一个更实际的示例是异步枚举使用参数进行分页的 API 的所有结果
async IAsyncEnumerable<string> GetValuesAsync(HttpClient client)
{
int offset = 0;
const int limit = 10;
while (true)
{
// Get the current page of results and parse them.
string result = await client.GetStringAsync(
$"https://example.com/api/values?offset={offset}&limit={limit}");
string[] valuesOnThisPage = result.Split('\n');
// Produce the results for this page.
foreach (string value in valuesOnThisPage)
yield return value;
// If this is the last page, we're done.
if (valuesOnThisPage.Length != limit)
break;
// Otherwise, proceed to the next page.
offset += limit;
}
}
当 GetValuesAsync 启动时,它对第一页数据执行异步请求,然后生成第一个元素。然后请求第二个元素时,GetValuesAsync 会立即生成它,因为它也在数据的第一页中。下一个元素也在那个页面中,依此类推,最多10个元素。然后,当请求第11个元素时,valuesOnThisPage 中的所有值都已生成,因此第一页上没有更多的元素。GetValuesAsync 将继续执行 while 循环,进入下一页,对第二页数据执行异步请求,接收一批新的值,然后生成第11个元素
Discussion
在这个更加实际的示例中,您可能会注意到,只有一些结果需要任何异步工作。在这个例子中,页面长度为10,每10个元素中只有1个需要异步工作。如果页面大小为20,那么每20个元素中只有1个需要异步工作
这是异步流的正常模式。对于许多流,大多数异步迭代实际上是同步的; 异步流只允许异步检索任何下一个项。异步流的设计考虑到了异步代码和同步代码; 这就是异步流基于 ValueTask < T > 构建的原因。通过在底层使用 ValueTask < T > ,异步流可以最大限度地提高效率,无论是同步检索还是异步检索项目
3.2 Consuming Asynchronous Streams
Problem
您需要处理异步流(也称为异步 enumerable)的结果
Solution
消费异步操作一般是通过 await,消费一个 enumerable 一般是通过 foreach;那么消费一个异步的enumerable,就直接将它们结合在一起就可以了
public async Task ProcessValueAsync(HttpClient client)
{
await foreach (string value in GetValuesAsync(client))
{
Console.WriteLine(value);
}
}
3.3 Using LINQ with Asynchronous Streams
Problem
您希望使用定义良好且经过良好测试的运算符来处理异步流
Solution
IAsyncEnumerable< T > 也支持LINQ,通过 System.Linq.Async 社区nuget包提供这个服务
例如,关于 LINQ 的一个常见问题是,如果 Where 的谓词是异步的,那么如何使用 Where 操作符。换句话说,您希望基于某种异步条件筛选序列
例如,您需要查找数据库或 API 中的每个元素,以确定它是否应该包含在结果序列中。Where 不能处理异步条件,因为 Where 运算符要求其委托返回一个即时的同步应答
异步流有一个支持库,它定义了许多有用的运算符
IAsyncEnumerable<int> values = SlowRange().WhereAwait(
async value =>
{
// Do some asynchronous work to determine
// if this element should be included.
await Task.Delay(10);
return value % 2 == 0;
});
await foreach (int result in values)
{
Console.WriteLine(result);
}
// Produce sequence that slows down as it progresses.
async IAsyncEnumerable<int> SlowRange()
{
for (int i = 0; i != 10; ++i)
{
await Task.Delay(i * 100);
yield return i;
}
}
3.4 Asynchronous Streams and Cancellation
Problem
您需要一种取消异步流的方法
Solution
并非所有异步流都需要取消。当达到某个条件时,可以简单地停止枚举。如果这是唯一需要的“取消”类型,那么就不需要真正的取消,如下面的示例所示
await foreach (int result in SlowRange())
{
Console.WriteLine(result);
if (result >= 8)
break;
}
// Produce sequence that slows down as it progresses.
async IAsyncEnumerable<int> SlowRange()
{
for (int i = 0; i != 10; ++i)
{
await Task.Delay(i * 100);
yield return i;
}
}
也就是说,取消异步流通常很有用,因为一些操作员会将取消令牌传递给他们的源流。在这种情况下,您可能希望使用一个取消令牌来停止来自外部代码的 await foreach
返回 IAsyncEnumable < T > 的 async 方法可以通过定义一个标记为 EnumeratorCancellation 属性的参数来获取取消令牌。然后它可以自然地使用令牌,这通常是通过将其传递给其他接受取消令牌的 API 来完成的,如下所示:
using var cts = new CancellationTokenSource(500);
CancellationToken token = cts.Token;
await foreach (int result in SlowRange(token))
{
Console.WriteLine(result);
}
// Produce sequence that slows down as it progresses.
async IAsyncEnumerable<int> SlowRange(
[EnumeratorCancellation] CancellationToken token = default)
{
for (int i = 0; i != 10; ++i)
{
await Task.Delay(i * 100, token);
yield return i;
}
}
CHAPTER 4 Parallel Basics
暂时跳过
CHAPTER 5 Dataflow Basics
暂时跳过
CHAPTER 6 System.Reactive Basics
暂时跳过
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库