一文说通C#中的异步迭代器
今天来写写C#中的异步迭代器 - 机制、概念和一些好用的特性
迭代器的概念
迭代器的概念在C#中出现的比较早,很多人可能已经比较熟悉了。
通常迭代器会用在一些特定的场景中。
举个例子:有一个foreach
循环:
foreach (var item in Sources)
{
Console.WriteLine(item);
}
这个循环实现了一个简单的功能:把Sources
中的每一项在控制台中打印出来。
有时候,Sources
可能会是一组完全缓存的数据,例如:List<string>
:
IEnumerable<string> Sources(int x)
{
var list = new List<string>();
for (int i = 0; i < 5; i++)
list.Add($"result from Sources, x={x}, result {i}");
return list;
}
这里会有一个小问题:在我们打印Sources
的第一个的数据之前,要先运行完整运行Sources()
方法来准备数据,在实际应用中,这可能会花费大量时间和内存。更有甚者,Sources
可能是一个无边界的列表,或者不定长的开放式列表,比方一次只处理一个数据项目的队列,或者本身没有逻辑结束的队列。
这种情况,C#给出了一个很好的迭代器解决:
IEnumerable<string> Sources(int x)
{
for (int i = 0; i < 5; i++)
yield return $"result from Sources, x={x}, result {i}";
}
这个方式的工作原理与上一段代码很像,但有一些根本的区别 - 我们没有用缓存,而只是每次让一个元素可用。
为了帮助理解,来看看foreach
在编译器中的解释:
using (var iter = Sources.GetEnumerator())
{
while (iter.MoveNext())
{
var item = iter.Current;
Console.WriteLine(item);
}
}
当然,这个是省略掉很多东西后的概念解释,我们不纠结这个细节。但大体的意思是这样的:编译器对传递给foreach
的表达式调用GetEnumerator()
,然后用一个循环去检查是否有下一个数据(MoveNext()
),在得到肯定答案后,前进并访问Current
属性。而这个属性代表了前进到的元素。
为防止非授权转发,这儿给出本文的原文链接:https://www.cnblogs.com/tiger-wang/p/14136934.html
上面这个例子,我们通过MoveNext()
/Current
方式访问了一个没有大小限制的向前的列表。我们还用到了yield
迭代器这个很复杂的东西 - 至少我是这么认为的。
我们把上面的例子中的yield
去掉,改写一下看看:
IEnumerable<string> Sources(int x) => new GeneratedEnumerable(x);
class GeneratedEnumerable : IEnumerable<string>
{
private int x;
public GeneratedEnumerable(int x) => this.x = x;
public IEnumerator<string> GetEnumerator() => new GeneratedEnumerator(x);
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
class GeneratedEnumerator : IEnumerator<string>
{
private int x, i;
public GeneratedEnumerator(int x) => this.x = x;
public string Current { get; private set; }
object IEnumerator.Current => Current;
public void Dispose() { }
public bool MoveNext()
{
if (i < 5)
{
Current = $"result from Sources, x={x}, result {i}";
i++;
return true;
}
else
{
return false;
}
}
void IEnumerator.Reset() => throw new NotSupportedException();
}
这样写完,对照上面的yield
迭代器,理解工作过程就比较容易了:
- 首先,我们给出一个对象
IEnumerable
。注意,IEnumerable
和IEnumerator
是不同的。 - 当我们调用
Sources
时,就创建了GeneratedEnumerable
。它存储状态参数x
,并公开了需要的IEnumerable
方法。 - 后面,在需要
foreach
迭代数据时,会调用GetEnumerator()
,而它又调用GeneratedEnumerator
以充当数据上的游标。 MoveNext()
方法逻辑上实现了for循环,只不过,每次调用MoveNext()
只执行一步。更多的数据会通过Current
回传过来。另外补充一点:MoveNext()
方法中的return false
对应于yield break
关键字,用于终止迭代。
是不是好理解了?
下面说说异步中的迭代器。
异步中的迭代器
上面的迭代,是同步的过程。而现在Dotnet开发工作更倾向于异步,使用async/await
来做,特别是在提高服务器的可伸缩性方面应用特别多。
上面的代码最大的问题,在于MoveNext()
。很明显,这是个同步的方法。如果它运行需要一段时间,那线程就会被阻塞。这会让代码执行过程变得不可接受。
我们能做得最接近的方法是异步获取数据:
async Task<List<string>> Sources(int x) {...}
但是,异步获取数据并不能解决数据缓存延迟的问题。
好在,C#为此特意增加了对异步迭代器的支持:
public interface IAsyncEnumerable<out T>
{
IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default);
}
public interface IAsyncEnumerator<out T> : IAsyncDisposable
{
T Current { get; }
ValueTask<bool> MoveNextAsync();
}
public interface IAsyncDisposable
{
ValueTask DisposeAsync();
}
注意,从.NET Standard 2.1
和.NET Core 3.0
开始,异步迭代器已经包含在框架中了。而在早期版本中,需要手动引入:
# dotnet add package Microsoft.Bcl.AsyncInterfaces
目前这个包的版本号是5.0.0。
还是上面例子的逻辑:
IAsyncEnumerable<string> Source(int x) => throw new NotImplementedException();
看看foreach
可以await
后的样子:
await foreach (var item in Sources)
{
Console.WriteLine(item);
}
编译器会将它解释为:
await using (var iter = Sources.GetAsyncEnumerator())
{
while (await iter.MoveNextAsync())
{
var item = iter.Current;
Console.WriteLine(item);
}
}
这儿有个新东西:await using
。与using
用法想法,但释放时会调用DisposeAsync
,而不是Dispose
,包括回收清理也是异步的。
这段代码其实跟前边的同步版本非常相似,只是增加了await
。但是,编译器会分解并重写异步状态机,它就变成异步的了。原理不细说了,不是本文关注的内容。
那么,带有yield
的迭代器如何异步呢?看代码:
async IAsyncEnumerable<string> Sources(int x)
{
for (int i = 0; i < 5; i++)
{
await Task.Delay(100); // 这儿模拟异步延迟
yield return $"result from Sources, x={x}, result {i}";
}
}
嗯,看着就舒服。
这就完了?图样图森破。异步有一个很重要的特性:取消。
那么,怎么取消异步迭代?
异步迭代的取消
异步方法通过CancellationToken
来支持取消。异步迭代也不例外。看看上面IAsyncEnumerator<T>
的定义,取消标志也被传递到了GetAsyncEnumerator()
方法中。
那么,如果是手工循环呢?我们可以这样写:
await foreach (var item in Sources.WithCancellation(cancellationToken).ConfigureAwait(false))
{
Console.WriteLine(item);
}
这个写法等同于:
var iter = Sources.GetAsyncEnumerator(cancellationToken);
await using (iter.ConfigureAwait(false))
{
while (await iter.MoveNextAsync().ConfigureAwait(false))
{
var item = iter.Current;
Console.WriteLine(item);
}
}
没错,ConfigureAwait
也适用于DisposeAsync()
。所以最后就变成了:
await iter.DisposeAsync().ConfigureAwait(false);
异步迭代的取消捕获做完了,接下来怎么用呢?
看代码:
IAsyncEnumerable<string> Sources(int x) => new SourcesEnumerable(x);
class SourcesEnumerable : IAsyncEnumerable<string>
{
private int x;
public SourcesEnumerable(int x) => this.x = x;
public async IAsyncEnumerator<string> GetAsyncEnumerator(CancellationToken cancellationToken = default)
{
for (int i = 0; i < 5; i++)
{
await Task.Delay(100, cancellationToken); // 模拟异步延迟
yield return $"result from Sources, x={x}, result {i}";
}
}
}
如果有CancellationToken
通过WithCancellation
传过来,迭代器会在正确的时间被取消 - 包括异步获取数据期间(例子中的Task.Delay
期间)。当然我们还可以在迭代器中任何一个位置检查IsCancellationRequested
或调用ThrowIfCancellationRequested()
。
此外,编译器也会通过[EnumeratorCancellation]
来做这完成这个任务,所以我们还可以这样写:
async IAsyncEnumerable<string> Sources(int x, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
for (int i = 0; i < 5; i++)
{
await Task.Delay(100, cancellationToken); // 模拟异步延迟
yield return $"result from Sources, x={x}, result {i}";
}
}
这个写法与上面的代码其实是一样的,区别在于加了一个参数。
实际应用中,我们有下面几种写法上的选择:
// 不取消
await foreach (var item in Sources)
// 通过WithCancellation取消
await foreach (var item in Sources.WithCancellation(cancellationToken))
// 通过SourcesAsync取消
await foreach (var item in SourcesAsync(cancellationToken))
// 通过SourcesAsync和WithCancellation取消
await foreach (var item in SourcesAsync(cancellationToken).WithCancellation(cancellationToken))
// 通过不同的Token取消
await foreach (var item in SourcesAsync(tokenA).WithCancellation(tokenB))
几种方式区别于应用场景,实质上没有区别。对两个Token
的方式,任何一个Token
被取消时,任务会被取消。
总结
同步迭代其实在各个代码中用的都比较多,但异步迭代用得很好。一方面,这是个相对新的东西,另一方面,是会有点绕,所以很多人都不敢碰。
今天这个,也是个人的一些经验总结,希望对大家理解迭代能有所帮助。
![]() |
微信公众号:老王Plus 扫描二维码,关注个人公众号,可以第一时间得到最新的个人文章和内容推送 本文版权归作者所有,转载请保留此声明和原文链接 |
一文说通C#中的异步迭代器的更多相关文章
- 一文说通C#中的异步编程补遗
前文写了关于C#中的异步编程.后台有无数人在讨论,很多人把异步和多线程混了. 文章在这儿:一文说通C#中的异步编程 所以,本文从体系的角度,再写一下这个异步编程. 一.C#中的异步编程演变 1. ...
- 一文说通C#中的异步编程
天天写,不一定就明白. 又及,前两天看了一个关于同步方法中调用异步方法的文章,里面有些概念不太正确,所以整理了这个文章. 一.同步和异步. 先说同步. 同步概念大家都很熟悉.在异步概念出来之前,我 ...
- C# 8 中的异步迭代器 IAsyncEnumerable<T> 解析
异步编程已经流行很多年了,.NET 引入的 async 和 await 关键词让异步编程更具有可读性,但有一个遗憾,在 C# 8 之前都不能使用异步的方式处理数据流,直到 C# 8 引入的 IAsyn ...
- [译]Python中的异步IO:一个完整的演练
原文:Async IO in Python: A Complete Walkthrough 原文作者: Brad Solomon 原文发布时间:2019年1月16日 翻译:Tacey Wong 翻译时 ...
- 【JS】336- 拆解 JavaScript 中的异步模式
点击上方"前端自习课"关注,学习起来~ JavaScript 中有很多种异步编程的方式.callback.promise.generator.async await 甚至 RxJS ...
- 【JS】285- 拆解 JavaScript 中的异步模式
JavaScript 中有很多种异步编程的方式.callback.promise.generator.async await 甚至 RxJS.我最初接触不同的异步模式时,曾想当然的觉得 promise ...
- 新手教程:不写JS,在MIP页中实现异步加载数据
从需求谈起:在 MIP 页中异步加载数据 MIP(移动网页加速器) 的 加速原理 除了靠谱的 MIP-Cache CDN 加速外,最值得一提的就是组件系统.所有 JS 交互都需要使用 MIP 组件实现 ...
- 深入Asyncio(八)异步迭代器
Async Iterators: async for 除了async def和await语法外,还有一些其它的语法,本章学习异步版的for循环与迭代器,不难理解,普通迭代器是通过__iter__和__ ...
- [技术翻译]在现代JavaScript中编写异步任务
本周再来翻译一些技术文章,本次预计翻译三篇文章如下: 04.[译]使用Nuxt生成静态网站(Generate Static Websites with Nuxt) 05.[译]Web网页内容是如何影响 ...
随机推荐
- .Net 开源项目 FreeRedis 实现思路之 - Redis 6.0 客户端缓存技术
写在开头 FreeRedis 是一款继 CSRedisCore 之后重写的 .NET redis 客户端开源组件,以 MIT 协议开源托管于 github,目前支持 .NET 5..NETCore 2 ...
- NO.A.0010——Windows常用快捷键使用教程
小娜操作: Win + C: 打开Cortana微软小娜,并开始聆听...... Win + Q: 打开Cortana: Win + S: 打开Cortana:sdfghjkrtgyh XBOX操作: ...
- 左右声道音频怎么制作,用Vegas就对啦
一款优秀的视频剪辑软件,不仅有高水平的视频制作功能,它的音频编辑功能也是必不可少的.Vegas就是这么一款软件,同时具备视频制作特效制作的同时,还能帮助制作轨道音频效果. 下面,就让小编带大家去学习, ...
- 实现 Application_Start 和 Application_End
理解 ASP.NET Core: 实现 Application_Start 和 Application_End 在 ASP.NET 中两个常用的处理节点是 Application_Start() 和 ...
- 免费撸12个月AWS服务器
前言 AWS联合博客园免费发送福利了,活动时间11月1号-11月31号,注册AWS免费体验12个月的服务器哦. 参考教程 官网教程: https://www.cnblogs.com/cmt/p/139 ...
- 【P1588】丢失的牛——区间dp/bfs
(题面来自Luogu) 题目描述 FJ丢失了他的一头牛,他决定追回他的牛.已知FJ和牛在一条直线上,初始位置分别为x和y,假定牛在原地不动.FJ的行走方式很特别:他每一次可以前进一步.后退一步或者直接 ...
- vs2019 Com组件初探-通过IDispatch接口调用Com
vs2019 Com组件初探-简单的COM编写以及实现跨语言调用 上一篇实现了如何编写基于IDipatch接口的COM以及vbs如何调用编写的COM 本次主要是实现VBS的CreateObject函数 ...
- java12(eclipse断点调试)
选择结构switch 1.格式: switch(整型数据){ case 值A:System.out.println("");break; case 值B:System.out.pr ...
- python核心高级学习总结3-------python实现进程的三种方式及其区别
python实现进程的三种方式及其区别 在python中有三种方式用于实现进程 多进程中, 每个进程中所有数据( 包括全局变量) 都各有拥有⼀份, 互不影响 1.fork()方法 ret = os.f ...
- PyQt(Python+Qt)学习随笔:Qt Designer中部件的焦点策略focusPolicy设置
在Qt Designer中可以设置部件的焦点策略,部件的焦点策略属性取值范围由枚举类型Qt.FocusPolicy来定义,该枚举类型及其含义如下表所示: 注意:经老猿测试鼠标轮滚动获取焦点,只有在鼠标 ...