一文说通异步 LINQ

用不好异步 LINQ,基本上就等于用不好 LINQ 了。

LINQ 这个东西,出来很早了,写过几年代码的兄弟们,或多或少都用过一些。

早期的 LINQ,主要是同步的,直到 C# 8.0 加入 IAsyncEnumerable,LINQ 才真正转向异步。这本来是个非常好的改变,配合 System.Linq.Async 库提供的扩展,可以在诸如 Where、Select、GroupBy 等各种地方用到异步。

但事实上,在我 Review 代码时,见了很多人的代码,并没有按异步的规则去使用,出现了很多的坑。

举个简单的例子:

static async Task<List<T>> Where<T>(this IAsyncEnumerable<T> source, Func<T, bool> predicate)
{var filteredItems = new List<T>();await foreach (var item in source){if (predicate(item)){filteredItems.Add(item);}}return filteredItems;
}

这样的写法,看着是用到了 async / await 对,但实际上并没有实现异步,程序依然是按照同步在运行。换句话说,这只是一个样子上的异步,实际没有任何延迟执行的效果。

1. 延迟执行

其实,这儿正确的写法也挺简单,用到的就是个异步的迭代器(关于异步迭代器,如果需要了解,可以看我的另一篇推文):

static async IAsyncEnumerable<T> Where<T>(this IAsyncEnumerable<T> source, Func<T, bool> predicate)
{await foreach (var item in source){if (predicate(item)){yield return item;}}
}

这种写法下,编译器会将方法转了状态机,并在实际调用时,才通过枚举器返回异步枚举项。

看看调用过程:

IAsyncEnumerable<User> users = ...
IAsyncEnumerable<User> filteredUsers = users.Where(User => User.Name == "WangPlus");await foreach (User user in filteredUsers)
{Console.WriteLine(user.Age);
}

在这个调用的例子中,在 Where 时,实际方法并不会马上开始。只有在下面 foreach 时,才真正开始执行 Where 方法。

延迟执行,这是异步 LINQ 的第一个优势。

2. 流执行

流执行,依托的也是异步迭代器。

所谓流执行,其实就是根据调用的要求,一次返回一个对象。通过使用异步迭代器,可以不用一次返回所有的对象,而是一个一个地返回单个的对象,直到枚举完所有的对象。

流执行需要做个技巧性的代码,需要用到一个 C# 8.0 的新特性:局部方法。

看代码:

static IAsyncEnumerable<T> Where<T>(this IAsyncEnumerable<T> source, Func<T, bool> predicate)
{return Core();async IAsyncEnumerable<T> Core(){await foreach (var item in source){if (predicate(item)){yield return item;}}}
}

3. 取消异步 LINQ

前面两个小节,写的是异步 LINQ 的执行。

通常使用异步 LINQ 的原因,就是因为执行时间长,一般需要一段时间来完成。因此,取消异步 LINQ 就很重要。想象一下,一个长的 DB 查询已经超时了的情况,该怎么处理?

为了支持取消,IAsyncEnumerable.GetEnumerator 本身接受一个 CancellationToken 参数来中止任务,并用一个扩展方法挂接到 foreach 调用:

CancellationToken cancellationToken = ...
IAsyncEnumerable<User> users = ...
IAsyncEnumerable<User> filteredUsers = users.Where(User => User.Name == "WangPlus");await foreach (var User in filteredUsers.WithCancellation(cancellationToken))
{Console.WriteLine(User.Age);
}

同时,在上面的 Where 定义中,也要响应 CancellationToken 参数:

static IAsyncEnumerable<T> Where<T>(this IAsyncEnumerable<T> source, Func<T, bool> predicate)
{return Core();async IAsyncEnumerable<T> Core([EnumeratorCancellation] CancellationToken cancellationToken = default){await foreach (var item in source.WithCancellation(cancellationToken)){if (predicate(item)){yield return item;}}}
}

多解释一下:在 Where 方法中,CancellationToken 只能加到局部函数 Core 中,一个简单的原因是 Where 本身并不是异步方法,而且,我们也不希望从 Where 往里传递。想象一下:

Users.Where(xxx, cancellationToken).Select(xxx, cancellationToken).OrderBy(xxx, cancellationToken);

这样的代码会让人晕死。

所以,我们会采用上面的方式,允许消费者在枚举数据时传递 CancellationToken 来达到取消异步操作的目的。

4. 处理ConfigureAwait(false)

这是另一个异步必须要注意的部分,其实就是上下文。

通常大多数的方法,我们不需要关注上下文,但总有一些需要,在等待的异步操作恢复后,需要返回到某个上下文的情况。这种情况在 UI 线程编码时通常都需要考虑。很多人提到的异步死锁,就是这个原因。

处理也很简单:

static IAsyncEnumerable<T> Where<T>(this IAsyncEnumerable<T> source, Func<T, bool> predicate)
{return Core();async IAsyncEnumerable<T> Core([EnumeratorCancellation] CancellationToken cancellationToken = default){await foreach (var item in source.WithCancellation(cancellationToken).ConfigureAwait(false)){if (predicate(item)){yield return item;}}}
}

这儿也多说两句:按微软的说法,await foreach 本身是基于模式的,WithCancellation 和 ConfigureAwait 返回同样的结构体 ConfiguredCancelableAsyncEnumerable。这个结构体没有实现 IAsyncEnumerable 接口,而是做了一个 GetAsyncEnumerator 方法,返回一个具有 MoveNextAsync、Current、DisposeAsync 的枚举器,因此可以 await foreach 。

5. 方法扩展

上面 4 个小节,我们完成了一个 Where 异步 LINQ 的全部内容。

不过,这个方法有一些限制和不足。熟悉异步的兄弟们应该已经看出来了,里面用了一个委托 predicate 来做数据过滤,而这个委托,是个同步的方法。

事实上,根据微软对异步 LINQ 的约定,每个操作符应该是三种重载:

  • 同步委托的实现,就是上面的 Where 方法;

  • 异步委托的实现,这个是指具有异步返回类型的实现,通常这种方法名称会用一个 Await 做后缀,例如:WhereAwait;

  • 可以接受取消的异步委托的实现,通常这种方法会用 AwaitWithCancellation 做后缀,例如:WhereAwaitWithCancellation。

参考微软的异步方法,基本上都是以这种结构来命名方法名称的。

下面,我们也按这个方式,来做一个 Where 方法的几个重载。

WhereAwait 方法

上面说了,这会是一个异步实现。所以,条件部分就不能用 Func<T, bool> 这样的同步委托了,而需要改为 Func<T, ValueTask<bool>>。这里的 ValueTask 倒不是必须,用 Task 也可以,只不过我更习惯用 ValueTask。两个的区别:Task 是类,有上下文,而 ValueTask 是结构。

代码是这样:

static IAsyncEnumerable<T> WhereAwait<T>(this IAsyncEnumerable<T> source, Func<T, ValueTask<bool>> predicate)
{return Core();async IAsyncEnumerable<T> Core([EnumeratorCancellation] CancellationToken cancellationToken = default){await foreach (var item in source.WithCancellation(cancellationToken).ConfigureAwait(false)){if (await predicate(item).ConfigureAwait(false)){yield return item;}}}
}

调用时是这样:

IAsyncEnumerable<User> filteredUsers = users.WhereAwait(async user => await someIfFunction());

WhereAwaitWithCancellation方法

在上面的基础上,又加了一个取消操作。

看代码:

static IAsyncEnumerable<T> WhereAwaitWithCancellation<T>(this IAsyncEnumerable<T> source, Func<T, CancellationToken, ValueTask<bool>> predicate)
{return Core();async IAsyncEnumerable<T> Core([EnumeratorCancellation] CancellationToken cancellationToken = default){await foreach (var item in source.WithCancellation(cancellationToken).ConfigureAwait(false)){if (await predicate(item, cancellationToken).ConfigureAwait(false)){yield return item;}}}
}

调用时是这样:

IAsyncEnumerable<User> filteredUsers = users.WhereAwaitWithCancellation(async (user, token) => await someIfFunction(user, token));

6. 总结

异步 LINQ,多数是在 LINQ 的扩展方法中使用,而不是我们通常习惯的 LINQ 直写。

事实上,异步 LINQ 的扩展,对 LINQ 本身是有比较大的强化作用的,不管从性能,还是可读性上,用多了,只会更爽。

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

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

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

相关文章

HDU_1541 Stars(树状数组)

poj上1A&#xff0c; HDU上6A&#xff0c;我晕啊&#xff01;注意几点&#xff1a; 1、多组数据&#xff1b; 2、memset(c, 0, siezeof(c)); 3、memset(ans, 0, sizeof(ans)); my code: View Code #include <stdio.h>#include <string.h>#define N 32010int c[N],…

点击ride界面edit空白_『技术锦囊』如何在SOLIDWORKS界面调用宏程序?

SOLIDWORKS宏程序为广大设计开发者提供了非常便捷的开发环境&#xff0c;合理的使用宏程序除了可以节约时间还可以减少很多不必要的操作&#xff0c;例如一键替换图纸等。此次便与大家讲讲&#xff0c;如何在SOLIDWORKS界面调用宏程序。操作流程1、任意开启一张工程图图纸&…

摇滚java游戏_java 集合类

一、Array &#xff0c; ArraysJava所有“存储及随机访问一连串对象”的做法&#xff0c;array是最有效率的一种。1、效率高&#xff0c;但容量固定且无法动态改变。array还有一个缺点是&#xff0c;无法判断其中实际存有多少元素&#xff0c;length只是告诉我们array的容量。2…

女人在想什么

1 行吧&#xff0c;这样至少回家不用给洗jiojio了。2 世纪难题——《女人在想什么》3 方法总比困难多4 跟瓜摊大哥学切西瓜5 你是怎样上去的&#xff1f;6 摄影师&#xff1a;我是因为没有对手才做摄影的&#xff01;7 有了女儿后&#xff0c;儿子的处境好像不太妙8 想知道白色…

linux打包压缩命令汇总

tar命令 [rootlinux ~]# tar [-cxtzjvfpPN] 文件与目录 ....参数&#xff1a;-c &#xff1a;建立一个压缩文件的参数指令(create 的意思)&#xff1b;-x &#xff1a;解开一个压缩文件的参数指令&#xff01;-t &#xff1a;查看 tarfile 里面的文件&#xff01;特别注意&…

zoj2271 Chance to Encounter a Girl(DP)

/* 概率计算&#xff1a;按时间为阶段&#xff0c;每个点由上一阶段周围的四个点来维护。 注意事项&#xff1a;1.时间O&#xff08;N^3*T&#xff09;&#xff0c;在问题的边缘时间&#xff0c;所以打表计算。     2.关于概率的求解&#xff0c;如果遇到就结束了&#…

12 个问题搞懂 Redis

都说学习需要带着问题&#xff0c;带着思考进行学习&#xff0c;下面就以问题的形式来学习下 Redis 。1、什么是 Redis &#xff1f;Redis 是一个高性能的 key-value 数据库&#xff1b;作者来自意大利西西里岛的 Salvatore Sanfilippo &#xff1b;Redis 使用 ANSI C 语言编写…

sql企业管理器_Valentina Studio for mac(开源数据库管理器)

Valentina Studio for mac是您使用MySQL&#xff0c;MariaDB&#xff0c;SQL Server&#xff0c;PostgreSQL&#xff0c;SQLite和Valentina DB数据库的通用数据库管理工具。valentina studio mac下载可让您连接所有主要数据库&#xff0c;运行查询并生成图表&#xff0c;以更好…

java程序编六角星_跨平台移动端解决方案—Weex

跨端方案背景一1起因由于客户端Webview内嵌H5的各种受限&#xff0c;例如性能差、JS执行效率低以及伴随着大量的机型兼容问题&#xff0c;于是有了各种混合跨端开发解决方案&#xff1a;Hybrid、React-Native、Weex、Flutter、小程序、快应用等。2优势我们知道各大应用市场对于…

[转]常用Delphi开发资料网址

发现一个很好的个人网站&#xff0c;该网站收集了大量有关Delphi开发资料的网址链接&#xff0c;值得收藏。 相关Delphi链接如下&#xff1a; Books, Tutorials Tomes of Delphi: Algorithms and data structures by Julian M. Bucknall , Source http://www.boyet.com/Code/To…

使用微软WPF技术开发产品优势究竟在那里

2008年在世界经济历史上是一个不平凡的一年&#xff0c;在这一年中&#xff0c;美国的五大投资银行有两家破产。金融危机席卷全球&#xff0c;美国的失业率在过去的一年从4%飙升到了10%&#xff0c;有些城市的失业率甚至高达20%&#xff1b;加拿大的就业数字也是惨不忍睹&#…

父类可以调用子类的方法吗_python类的继承、多继承及查找方法顺序

讲解类的继承&#xff0c;先用无__init__()方法的类讲解&#xff0c;会更容易理解分三部分讲解1、单继承(无__init__()方法)继承方式&#xff1a;子类(父类):方法重写&#xff1a;防止执行父类的方法。不想继承父类的某个方法&#xff0c;就自己重写一个一样名称的方法执行父类…

解决ASP.NET Core部署到IIS,更新项目另一个程序正在使用此文件,进程无法访问...

问题部署到IIS上的ASP.NET Core项目&#xff0c;在更新的时候会进程占用的错误解决思路初步解决方案&#xff1a;1&#xff0c;关闭应用程序池2&#xff0c;关闭网站3&#xff0c;更新项目缺点&#xff1a;网站没法访问&#xff0c;部署项目停的时间过长答案查询官方文档后&…

下面选项能正确表示JAVA_模拟试题2

Java语言程序设计模拟试题二一、选择题&#xff1a;共20小题&#xff0c;每小题1分&#xff0c;满分20分&#xff1a;请将答案填入题后括号中。1&#xff0e;以下的选项中能正确表示Java语言中的一个整型常量的是 ( )A) 12. B) -20 C) 1,000 D) 4 5 62。下列的变量定义中&#…

hdu 1671

Trie树&#xff0c;题目不难&#xff0c;但还是WA了一次&#xff0c;没有考虑后输入的字符串是前输入的字符串的前缀的情况&#xff0c;真是太粗心了。不过做了几道Trie树的题目以后&#xff0c;代码写得还是比较通用了&#xff0c;慢慢再改进吧 /* * hdu1671/win.c * Created …

使用Redis Stream来做消息队列和在Asp.Net Core中的实现

Redis - Wikipedia写在前面我一直以来使用redis的时候&#xff0c;很多低烈度需求(并发要求不是很高)需要用到消息队列的时候&#xff0c;在项目本身已经使用了Redis的情况下都想直接用Redis来做消息队列&#xff0c;而不想引入新的服务&#xff0c;kafka和RabbitMQ等&#xff…

java基础代码下载_Java基础(一)(示例代码)

1. 概述1.1 什么是Java语言Java语言&#xff1a;面向对象的程序设计语言与机器无关的二进制格式的类文件Java虚拟机(用来执行类文件)完整的软件程序包(跨平台的API和库)1.1.1 Java语言特点语法简单&#xff0c;功能强大分布式与安全性与平台无关解释、编译两种运行方式多线程动…

windows挂载ext4_使用 UEFI 双启动 Windows 和 Linux | Linux 中国

这是一份在同一台机器上设置 Linux 和 Windows 双重启动的速成解释&#xff0c;使用统一可扩展固件接口&#xff08;UEFI&#xff09;。来源&#xff1a;https://linux.cn/article-12891-1.html作者&#xff1a;Alan Formy-duval译者&#xff1a;郑&#xff08;本文字数&#x…

Master DNS服务的搭建

很多企业都通过Linux来搭建自己的DNS服务器来提高网路效率&#xff0c;在此我们来学习如何搭建Master DNS服务器。首先&#xff0c;确定是否搭建yum仓库&#xff0c;并有软件包可用&#xff0c;如DVD光盘是否挂载。一、安装DNS服务所需要的软件包DNS所需要的软件包有&#xff1…

oh,我的老伙计,你看看这近五十个dapr视频

oh&#xff0c;我的老伙计&#xff0c;你看看这近五十个 dapr 视频。这不就是你想要的视频资料吗&#xff1f;快来捡走吧&#xff01;开始了&#xff0c;但是没完全开始 Dapr 是一个可移植的、事件驱动的运行时&#xff0c;它使任何开发人员能够轻松构建出弹性的、无状态和有状…