一文说通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属性。而这个属性代表了前进到的元素。

上面这个例子,我们通过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迭代器,理解工作过程就比较容易了:

  1. 首先,我们给出一个对象IEnumerable。注意,IEnumerableIEnumerator是不同的。

  2. 当我们调用Sources时,就创建了GeneratedEnumerable。它存储状态参数x,并公开了需要的IEnumerable方法。

  3. 后面,在需要foreach迭代数据时,会调用GetEnumerator(),而它又调用GeneratedEnumerator以充当数据上的游标。

  4. 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被取消时,任务会被取消。

总结

同步迭代其实在各个代码中用的都比较多,但异步迭代用得很好。一方面,这是个相对新的东西,另一方面,是会有点绕,所以很多人都不敢碰。

今天这个,也是个人的一些经验总结,希望对大家理解迭代能有所帮助。

喜欢就来个三连,让更多人因你而受益

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/306258.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

GraphQL:Descriptor Attributes

GraphQL 既是一种用于 API 的查询语言也是一个满足你数据查询的运行时。GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述&#xff0c;使得客户端能够准确地获得它需要的数据&#xff0c;而且没有任何冗余&#xff0c;也让 API 更容易地随着时间推移而演进&#xff0c…

idea怎么把代码放到git_在IDEA中如何初始化Git,把项目推送到Git上

在IDEA中如何初始化Git&#xff0c;把项目推送到Git上登录Gitee(码云)账号&#xff0c;新建仓库先按如下步骤简单新建一个仓库&#xff1a;创建成功后&#xff0c;会出现下图中所示的原始文件&#xff1a;在IDEA上的Terminal中进行操作注意&#xff1a;可能有些朋友刚打开Termi…

大会线上同步直播, 来不到现场也可以线上看直播,以及参会秘籍

紧急提醒&#xff1a;还有1天&#xff01;2020.NET开发者大会就要开幕啦&#xff01;你都做好参会准备没有&#xff1f;特殊时期&#xff0c;为方便小伙伴们顺利参会&#xff0c;小编特意整理了这篇大会参会攻略&#xff0c;大到各种日程安排、小到签到、出行、防疫等&#xff…

大曾幽默打油诗_这才是真正的幽默打油诗,逗人一笑,又引人深思!

阅读本文前&#xff0c;请您先点击上面的蓝色字体“点点星光”&#xff0c;再点击“关注”&#xff0c;这样您就可以继续免费收到文章了。每天都有分享。完全是免费订阅&#xff0c;请放心关注来源&#xff1a;诗词天地大曾 &#xff0c;曾初良&#xff0c;也乐斋主&#xff0c…

如何使用第三方日志记录提供程序替代.NET Core中的内置程序

背景.NET Core 支持适用于各种内置和第三方日志记录提供程序的日志记录 API。 先来看下如何将日志记录 API 与内置提供程序一起使用。调用 CreateDefaultBuilder&#xff0c;这将添加以下日志记录提供程序&#xff1a;控制台调试EventSourceEventLog&#xff1a;仅限 Windowspu…

mysql新加不了数据库_MySQL数据库之mysql增加新用户无法登陆解决方法

本文主要向大家介绍了MySQL数据库之mysql增加新用户无法登陆解决方法 &#xff0c;通过具体的内容向大家展现&#xff0c;希望对大家学习MySQL数据库有所帮助。今天安装openstack folsom版本&#xff0c;安装完mysql&#xff0c;为各个服务增加对应的数据库和用户后&#xff0c…

编程去除背景绿幕抠图,基于.NET+OpenCVSharp

摘要&#xff1a;本文介绍了一种使用OpenCVSharp对摄像头中的绿幕视频进行实时“抠人像、替换背景”的方式&#xff0c;对于项目中的算法进行了分析。本文中给出了简化OpenCVSharp中Mat、MatExpr等托管资源释放的方法。本文还介绍了“高效摄像头播放控件”以及和OpenCVSharp的性…

.NET 云原生架构师训练营(模块二 基础巩固 依赖注入)--学习笔记

2.2.1 核心模块--依赖注入什么是依赖注入.NET Core DI 生命周期服务设计服务范围检查ASP.NET Core 依赖注入&#xff1a;https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/dependency-injection?viewaspnetcore-5.0什么是依赖注入Dependency injection 依赖注入Inv…

vue ref 绑定的事件需要移除吗_Vue易遗忘的基础复习(二)

数据请求Vue-resource请求在Vue2.0之后已经被舍弃2. fetch请求因为传统 Ajax &#xff08;指 XMLHttpRequest&#xff09;存在一些令人头疼的问题&#xff1a;配置和调用方式非常混乱&#xff0c;而且基于事件的异步模型写起来也没有现代的 Promise&#xff0c;generator/yield…

如何在 ASP.NET Core 中使用 API 分析器

ASP.NET Core 2.2 引入了 API 分析器&#xff0c;它有利于提高 API 的文档化&#xff0c;API分析器 可以应用在任何带有 ApiController 特性的 Controller 上&#xff0c;本篇就和大家一起讨论下。安装 API 分析器 如果你使用的是 ASP.NET Core 2.2 的话&#xff0c;用 Visual …

.net mysql字符串截取_【MySQL】字符串截取之SUBSTRING_INDEX和【MySQL】字符串四则运算...

substring_index(str,delim,count)str:要处理的字符串delim:分隔符count:计数例子&#xff1a;strwww.google.com1.count是正数&#xff0c;那么就是从左往右数&#xff0c;第N个分隔符的左边的全部内容SELECT SUBSTRING_INDEX(www.google.com,.,1);结果是&#xff1a;wwwSELEC…

用C#+Selenium+ChromeDriver 爬取网页,完美模拟真实的用户浏览行为

背景Selenium是一个用于Web应用程序测试的工具。Selenium测试直接运行在浏览器中&#xff0c;就像真正的用户在操作一样。而对于爬虫来说&#xff0c;使用Selenium操控浏览器来爬取网上的数据那么肯定是爬虫中的杀手武器。这里&#xff0c;我将介绍selenium 谷歌浏览器的一般使…

mysql 设置宽松模式_mysql5.6 sql_mode设置为宽松模式

最近遇到一个很奇怪的事情由于数据人员的需求&#xff0c;现在需要修改mysql的sql_modesql_mode默认是sql_modeNO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES这时候我在/etc/my.cnf配置如下sql_modeNO_ENGINE_SUBSTITUTION重启后还是mysql> SELECT GLOBAL.sql_mode;---------…

ASP.NET Core ActionFilter引发的一个EF异常

最近在使用ASP.NET Core的时候出现了一个奇怪的问题。在一个Controller上使用了一个ActionFilter之后经常出现EF报错。InvalidOperationException: A second operation started on this context before a previous operation completed. Any instance members are not guarante…

mysql os.pid_MySQL在OS El Capitan的配置[PID file error]

Installation使用Homebrew安装&#xff1a;brew install mysqlbash中显示如下信息&#xff0c;安装成功。Weve installed your MySQL database without a root password. To secure it run:mysql_secure_installationTo connect run:mysql -urootTo have launchd start mysql n…

BCVP开发者说第5期:QuartzCore.Blazor

沉静岁月&#xff0c;淡忘流年1项目简介QuartzCore.BlazorQuartzCore.Blazor 是一个基于 .Net5 开发的轻量级 Quartz 作业配置中心&#xff0c;实践应用 Ant Design Blazor 和 FreeSql 两个技术&#xff0c; 对这两个技术感兴趣的小伙伴可以加我一起学习讨论哦&#xff0c;对有…

mysql update返回_MySQL中,当update修改数据与原数据相同时会再次执行吗?

本文同步Java知音社区&#xff0c;专注于Java作者&#xff1a;powdbahttps://yq.aliyun.com/articles/694162一、背景本文主要测试MySQL执行update语句时&#xff0c;针对与原数据&#xff08;即未修改&#xff09;相同的update语句会在MySQL内部重新执行吗&#xff1f;二、测试…

.NET 云原生架构师训练营(模块二 基础巩固 日志)--学习笔记

2.2.2 核心模块--日志ILogger 的使用日志的 ID日志的分类日志的级别LoggerProvider日志的最佳实践.NET Core 和 ASP.NET Core 中的日志记录&#xff1a;https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/logging/?viewaspnetcore-5.0ILogger 的使用在 Get 方法中添…

mysql数据库设计三大范式_了解数据库设计三大范式

数据库设计范式什么是范式&#xff1a;简言之就是&#xff0c;数据库设计对数据的存储性能&#xff0c;还有开发人员对数据的操作都有莫大的关系。所以建立科学的&#xff0c;规范的的数据库是需要满足一些规范的来优化数据数据存储方式。在关系型数据库中这些规范就可以称为范…

使用BeetleX网关部署第三方Web服务

BeetleX的http/ws网关在早期版本可以启动和管理第三方Web服务进程&#xff0c;在最新的1.5版本中引入了文件管理功能&#xff0c;通过这一功能可以对第三方Web服务进行发布管理。加入文件管理后BeetleX的新版本网关服务可以理解为一个简单化的IIS&#xff0c;但它的不同之处是可…