百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

C#8.0中的异步流 c#异步编程总结

bigegpt 2024-10-12 05:18 9 浏览

2020年的最后一天,闲着没事上网看了看IAsyncEnumerable。记录一下一些心得。

异步流(IAsyncEnumerable)是作为.net core 3.0/C#8.0的一部分于2019年下半年发布的。当我第一次看到这个名字的时候,我脑海里的第一反应是大神Jeffery Ritcher在08年写的AsyncEnumerable,也就是async/await实现模型的前身。虽然这两个东西名字相近,但它们是在不同年代解决不同的问题。异步流主要解决如何在C#中更方便高效的进行异步流处理(迭代)的问题。

编外话:有兴趣的朋友可以去Channel9搜一下Jeffery Ritcher和AsyncEnumerable的视频,对理解C#的异步模型有很大的帮助。

异步流主要使用了三个新的interface:IAsyncEnumerable, IAsyncEnumerator, IAsyncDisposable。.Net团队在C#中为异步流一如既往的加入了足够的语法糖,这使得异步流使用起来非常方便。从下面的例子可以看出,除了和异步相关的关键词(async和await foreach)之外语法和同步的foreach迭代方式基本一致:

// 定义(下面的例子将TextReader.ReadLineAsync包装成IAsyncEnumerable)
public async IAsyncEnumerable<string?> ReadLineAsync()
{
    string? line = string.Empty;
    while(line != null)
    {
        line = await this.reader.ReadLineAsync();
        if (line != null)
        {
            yield return line;
        }
    }
}

// 用await foreach调用
await foreach(var line in reader.ReadLineAsync()) Use(line)

语法中还可以方便的支持CancellationToken。这些我们不在这里赘述。对于上面的ReadLineAsync,C#会自动生成相应的异步迭代器IAsyncIterator,然后await foreach会在一个while循环中持续调用迭代器的MoveNextAsync直到迭代结束。

我们下面会详细讨论异步流迭代的具体实现细节,效率,以及适合哪些场景下的应用。

C#编译器是如何编译上述语法的

说到这里有两个相关背景要提一下:1. 对于同步的迭代(使用IEnumerable返回值和yield return的函数),C#编译器会自动根据函数生成一个基于状态机的迭代器用来管理每一步结果的yield return。2. 对于async/await,C#编译器也会自动生成一个状态机管理异步操作完成后的后续代码的回调。相关的知识点网上可以找到很多,我们同样不在这里赘述。

在处理IAsyncEnumerable时,C#编译器将上面两个状态机有机的结合在了一起用来生成相应的IAsyncEnumerator。这个状态机可能的返回情况有三种:有同步当前值可以直接返回时,无当前值开始异步调用需要等待时,迭代结束时(正常或者错误)。

如果拿上面的示例程序做例子,相应的调用流程大致如下:

  1. caller开始调用await foreach(...)
  2. 自动生成并返回本次的迭代器(IAsyncEnumerator)示例
  3. 调用迭代器的GetNextAsync
  4. GetNextAsync重置ManualResetValueTaskSource
  5. GetNextAsync调用迭代器状态机(通过调用自动生成的IAsyncStateMachine.MoveNext)
    1. 如果有值可以同步返回, 保存值到Current,置ManualResetValueTaskSource为true,立即返回。
    2. 如果没有当前值可用,状态机调用用户的异步代码。如果代码返回同步值,回到4.a;否则,通过AwaitUnsafeOnComplete设置ManualResetValueTaskSource到状态机的回调然后返回
  6. 如果5有同步值返回,GetNextAsync返回值为true的ValueTask;否则返回一个基于ManualResetValueTaskSource的ValueTask。此ValueTaskSource和5中的是同一实例。
  7. caller处理GetNextAsync返回值:
    1. caller如果拿到ValueTask值为true,访问迭代器的Current拿到当前迭代值,处理并从3重复处理
    2. 如果拿到的ValueTask有ValueTaskSource需要等待,caller设置自己的异步状态机现场并向上层返回。
  8. 5.b中的异步操作结束,迭代器状态机恢复,拿到当前值,做5.a同样的处理
  9. caller在7中的异步状态机恢复,重复7.a的处理。

可以看到,对上面几条简单的几条语句编译器做了非常多的处理。所有这些处理对我们都是透明的,大大简化了程序员的工作。一般情况下我们可以不用担心这些具体细节。

很感谢.Net团队在这方面的辛苦工作。然而,从另外一个角度上,这些编译器的自动代码生成又会导致我们编译后的代码膨胀。这和C#异步编程的实现方式有直接的关系。希望有一天.Net可以直接使用async/await相关的代码信息进行goroutine一样的直接调度而不是使用编译器添加的隐式异步callback。

IAsyncEnumerable相关的效率优化

非常值得一提的是.Net团队在实现IAsyncEnumerable时考虑了非常多的效率优化问题,尤其是对于堆分配优化到了极致。值得一提的是以下两点:

  • object复用:

上述操作中用到了很多不同的接口。所有这些接口都被同一个自动生成的class实现,避免了运行时需要分配多个object。甚至在调用GetAsyncEnumerator时,如果没有其它调用,当前的object会被用来直接作为迭代器。当然这是在Solid设计原则和效率之间的有意识妥协。上述的ReadLineAsync对应的类实现如下图所示:

[CompilerGenerated]
private sealed class <ReadLineAsync>d__3 : 
	IAsyncEnumerable<string>,
  IAsyncEnumerator<string>,
  IAsyncDisposable, 
  IValueTaskSource<bool>,
  IValueTaskSource,
  IAsyncStateMachine
{...}
  • 使用ValueTask和IValueTaskSource避免无必要的Task对象的分配:

迭代器IAsyncEnumerator的MoveNextAsync原型如下:

ValueTask<bool> MoveNextAsync();

可以看到返回值是一个ValueTask<bool>而不是Task<bool>。如果MoveNextAsync可以有值可以同步返回,我们只需要在栈上分配一个ValueTask<bool>(true)然后返回。如果需要异步等待,一般情况下我们需要对每次异步迭代分配一个Task<bool>然后封装到ValueTask<bool>里面。C#团队没有这么做,他们直接使用了IValueTaskSource,然后通过每次迭代重置它使得代码可以对每次迭代甚至DisposeAsync都重复使用同一个IValueTaskSource。

这些优化加起来的结果就是,不管异步流需要迭代一千次还是一万次,绝大多数情况下自动生成的代码只会在堆上分配一个对象

异步流的可能应用场景

通过以上可以看出,C#异步流非常简单易用,而且本身在内存方面的效率优化做的很好。这就意味着它很适用于高频次的流式处理,例如:

  • 网络协议如TCP/HTTP2数据包/流处理
  • 应用层流协议处理,如SignalR,gRPC等
  • 数据库io,EFCore据说在考虑用异步流重新部分功能
  • 流格式转换,使用异步流可以很轻松高效的将一个流格式转换为新的流格式并且提供非常友好的调用方式

那么WebApi和RESTful怎么样呢?在Webapi里使用异步流可以降低同步处理线程block的风险。RESTful api可以直接返回IAsyncEnumerable。然而,目前的json序列化还需要cache所有数据,所以这个对Webapi性能的影响并不大。


我已经迫不及待想在下一个工作里实验一下异步流了。你呢?

Happy coding, Peace.

相关推荐

最全的MySQL总结,助你向阿里“开炮”(面试题+笔记+思维图)

前言作为一名编程人员,对MySQL一定不会陌生,尤其是互联网行业,对MySQL的使用是比较多的。对于求职者来说,MySQL又是面试中一定会问到的重点,很多人拥有大厂梦,却因为MySQL败下阵来。实际上...

Redis数据库从入门到精通(redis数据库设计)

目录一、常见的非关系型数据库NOSQL分类二、了解Redis三、Redis的单节点安装教程四、Redis的常用命令1、Help帮助命令2、SET命令3、过期命令4、查找键命令5、操作键命令6、GET命...

netcore 急速接入第三方登录,不看后悔

新年新气象,趁着新年的喜庆,肝了十来天,终于发了第一版,希望大家喜欢。如果有不喜欢看文字的童鞋,可以直接看下面的地址体验一下:https://oauthlogin.net/前言此次带来得这个小项目是...

精选 30 个 C++ 面试题(含解析)(c++面试题和答案汇总)

大家好,我是柠檬哥,专注编程知识分享。欢迎关注@程序员柠檬橙,编程路上不迷路,私信发送以下关键字获取编程资源:发送1024打包下载10个G编程资源学习资料发送001获取阿里大神LeetCode...

Oracle 12c系列(一)|多租户容器数据库

作者杨禹航出品沃趣技术Oracle12.1发布至今已有多年,但国内Oracle12C的用户并不多,随着12.2在去年的发布,选择安装Oracle12c的客户量明显增加,在接下来的几年中,Or...

flutter系列之:UI layout简介(flutter-ui-nice)

简介对于一个前端框架来说,除了各个组件之外,最重要的就是将这些组件进行连接的布局了。布局的英文名叫做layout,就是用来描述如何将组件进行摆放的一个约束。在flutter中,基本上所有的对象都是wi...

Flutter 分页功能表格控件(flutter 列表)

老孟导读:前2天有读者问到是否有带分页功能的表格控件,今天分页功能的表格控件详细解析来来。PaginatedDataTablePaginatedDataTable是一个带分页功能的DataTable,...

Flutter | 使用BottomNavigationBar快速构建底部导航

平时我们在使用app时经常会看到底部导航栏,而在flutter中它的实现也较为简单.需要用到的组件:BottomNavigationBar导航栏的主体BottomNavigationBarI...

Android中的数据库和本地存储在Flutter中是怎样实现的

如何使用SharedPreferences?在Android中,你可以使用SharedPreferencesAPI来存储少量的键值对。在Flutter中,使用Shared_Pref...

Flet,一个Flutter应用的实用Python库!

▼Flet:用Python轻松构建跨平台应用!在纷繁复杂的Python框架中,Flet宛如一缕清风,为开发者带来极致的跨平台应用开发体验。它用最简单的Python代码,帮你实现移动端、桌面端...

flutter系列之:做一个图像滤镜(flutter photo)

简介很多时候,我们需要一些特效功能,比如给图片做个滤镜什么的,如果是h5页面,那么我们可以很容易的通过css滤镜来实现这个功能。那么如果在flutter中,如果要实现这样的滤镜功能应该怎么处理呢?一起...

flutter软件开发笔记20-flutter web开发

flutterweb开发优势比较多,采用统一的语言,就能开发不同类型的软件,在web开发中,特别是后台式软件中,相比传统的html5开发,更高效,有点像c++编程的方式,把web设计出来了。一...

Flutter实战-请求封装(五)之设置抓包Proxy

用了两年的flutter,有了一些心得,不虚头巴脑,只求实战有用,以供学习或使用flutter的小伙伴参考,学习尚浅,如有不正确的地方还望各路大神指正,以免误人子弟,在此拜谢~(原创不易,转发请标注来...

为什么不在 Flutter 中使用全局变量来管理状态

我相信没有人用全局变量来管理Flutter应用程序的状态。毫无疑问,我们的Flutter应用程序需要状态管理包或Flutter的基本小部件(例如InheritedWidget或St...

Flutter 攻略(Dart基本数据类型,变量 整理 2)

代码运行从main方法开始voidmain(){print("hellodart");}变量与常量var声明变量未初始化变量为nullvarc;//未初始化print(c)...