惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

L
LINUX DO - 最新话题
小众软件
小众软件
C
Check Point Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
V
Visual Studio Blog
Last Week in AI
Last Week in AI
P
Proofpoint News Feed
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
The GitHub Blog
The GitHub Blog
T
Tailwind CSS Blog
Recorded Future
Recorded Future
雷峰网
雷峰网
WordPress大学
WordPress大学
A
Arctic Wolf
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Project Zero
Project Zero
T
Tor Project blog
T
Threat Research - Cisco Blogs
Scott Helme
Scott Helme
Spread Privacy
Spread Privacy
G
Google Developers Blog
Security Latest
Security Latest
MongoDB | Blog
MongoDB | Blog
T
Threatpost
I
InfoQ
T
Tenable Blog
T
The Exploit Database - CXSecurity.com
S
Security Affairs
H
Hackread – Cybersecurity News, Data Breaches, AI and More
人人都是产品经理
人人都是产品经理
C
Cisco Blogs
博客园 - 三生石上(FineUI控件)
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
aimingoo的专栏
aimingoo的专栏
C
CXSECURITY Database RSS Feed - CXSecurity.com
美团技术团队
Google DeepMind News
Google DeepMind News
The Hacker News
The Hacker News
D
Docker
博客园 - 【当耐特】
大猫的无限游戏
大猫的无限游戏
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Vercel News
Vercel News
PCI Perspectives
PCI Perspectives
Microsoft Azure Blog
Microsoft Azure Blog
C
CERT Recently Published Vulnerability Notes
月光博客
月光博客
Cloudbric
Cloudbric
A
About on SuperTechFans
F
Fortinet All Blogs

元视角

.NET 生态下的 Agent 框架选型:从 ReAct 到原生推理 - 元视角 从「能用」到「好用」:LLM 流式响应实现方式的探索之路 - 元视角 当我用 2000 条聊天记录,让 AI 为我画一幅自画像 - 元视角 基于 Supabase 的 AI 应用开发探索 - 元视角 微博 × MCP:社交媒体新玩法解锁 - 元视角 四点钟海棠花未眠 - 元视角 Semantic Kernel × MCP:智能体的上下文增强探索 - 元视角 基于 K-Means 聚类分析实现人脸照片的快速分类 - 元视角 容器技术驱动下的代码沙箱实践与思考 - 元视角 温故而知新:后端通用查询方案的再思考 - 元视角 浅议 CancellationToken 在前后端协同取消场景中的应用 - 元视角 Semantic Kernel 视角下的 Text2SQL 实践与思考 - 元视角 关于 ChatGPT 的流式传输,你需要知道的一切 - 元视角 RAG 的是与非、Rewrite 和 Rerank - 元视角 使用 EFCore 和 PostgreSQL 实现向量存储及检索 - 元视角 基于 LLaMA 和 LangChain 实践本地 AI 知识库 - 元视角 使用 llama.cpp 在本地部署 AI 大模型的一次尝试 - 元视角 如何为 Git 配置多个 SSH Key - 元视角 C# 使用 LibUsbDotNet 实现 USB 设备检测 - 元视角 基于 C# 实现样式与数据分离的打印方案 - 元视角 基于 SVG 的图形交互方案实践 - 元视角 前端视频播放技术概览 - 元视角 温故而知新,再话 Python 动态导入 - 元视角 后 GPT 时代,NLP 不存在了? - 元视角 视频是不能 P 的系列:使用 Milvus 实现海量人脸快速检索 - 元视角 GDI+下字体大小自适应方案初探 - 元视角 小爱音箱集成 ChatGPT 的不完全教程 - 元视角 程序员视角下的三体世界随想 - 元视角 关于 Docker 容器配置信息的渐进式思考 - 元视角 在 Docker 容器内集成 Crontab 定时任务 - 元视角 为你的服务器集成 LDAP 认证 - 元视角 似花还似非花 - 元视角 视频是不能 P 的系列:使用 Dlib 实现人脸识别 - 元视角 浅议分布式链路追踪与日志的整合 - 元视角 关于 Git 大文件上传这件小事 - 元视角 .NET 进程内队列 Channel 的入门与应用 - 元视角 使用 Fody 实现 .NET 的静态编织 - 元视角 .NET Core + ELK 搭建可视化日志分析平台(下) - 元视角 聊一聊前端图片懒加载背后的故事 - 元视角 支持外部链接跳转的 Vue Router 扩展实现 - 元视角 视频是不能 P 的系列:OpenCV 和 Dlib 实现表情包 - 元视角 不得不说的 ASP.NET Core 集成测试 - 元视角 再议 DDD 视角下的 EFCore 与 领域事件 - 元视角 Vue.js 前端项目容器化部署实践极简教程 - 元视角 再见,人间四月天 - 元视角 Python 图像风格化迁移助力画家梦想 - 元视角 利用 ASP.NET Core 中的标头传播实现分布式链路追踪 - 元视角 利用 gRPC 实现文件的上传与下载 - 元视角 七种武器:延迟队列的原理和实现总结 - 元视角 gRPC 流式传输极简入门指南 - 元视角 Envoy 集成 Jaeger 实现分布式链路追踪 - 元视角 浅议非典型 Web 应用场景下的身份认证 - 元视角 gRPC 借助 Any 类型实现接口的泛化调用 - 元视角 分布式丛林探险系列之 Redis 集群模式 - 元视角 分布式丛林探险系列之 Redis 主从复制模式 - 元视角 通过 Python 预测 2021 年双十一交易额 - 元视角 gRPC 搭配 Swagger 实现微服务文档化 - 元视角 SSL/TLS 加密传输与数字证书的前世今生 - 元视角 使用 Python 自动识别防疫健康码 - 元视角 你不可不知的容器编排进阶技巧 - 元视角 ASP.NET Core 搭载 Envoy 实现 gRPC 服务代理 - 元视角 再话 AOP,从简化缓存操作说起 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务身份认证(JWT) - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的监控预警 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的反向代理 - 元视角 ASP.NET Core gRPC 打通前端世界的尝试 - 元视角 EFCore 实体命名约定库:EFCore.NamingConventions - 元视角 ASP.NET Core gRPC 集成 Polly 实现优雅重试 - 元视角 ASP.NET Core gRPC 健康检查的探索与实现 - 元视角 ASP.NET Core gRPC 拦截器的使用技巧分享 - 元视角 SnowNLP 使用自定义语料进行模型训练 - 元视角 使用 HttpMessageHandler 实现 HttpClient 请求管道自定义 - 元视角 ABP vNext 的实体与服务扩展技巧分享 - 元视角 ABP vNext 对接 Ant Design Vue 实现分页查询 - 元视角 源代码探案系列之 .NET Core 跨域中间件 CORS - 元视角 源代码探案系列之 .NET Core 限流中间件 AspNetCoreRateLimit - 元视角 源代码探案系列之 .NET Core 并发限制中间件 ConcurrencyLimiter - 元视角 通过 EmbededFileProvider 实现 Blazor 的静态文件访问 - 元视角 低代码,想说爱你不容易 - 元视角 记一次失败的 ThoughtWorks 面试经历 - 元视角 从 C# 1.0 到 C# 9.0,历代 C# 语言特性一览 - 元视角 通过 Python 分析 2020 年全年微博热搜数据 - 元视角 基于 Python 和 Selenium 实现 CSDN 一键三连自动化 - 元视角 使用多线程为你的 Python 爬虫提速的 N 种姿势,你会几种? - 元视角 实现网页长截图的常见思路总结 - 元视角 温故而知新,由 ADO.NET 与 Dapper 所联想到的 - 元视角 视频是不能 P 的系列:OpenCV 人脸检测 - 元视角 作为技术宅的我,是这样追鬼滅の刃的 - 元视角 使用 Python 抽取《半泽直树》原著小说人物关系 - 元视角 厉害了!打工人用 Python 分析西安市职位信息 - 元视角 使用 dotTrace 对 .NET 应用进行性能分析与优化 - 元视角 一道 HashSet 面试题引发的蝴蝶效应 - 元视角 基于选项模式实现.NET Core 的配置热更新 - 元视角 Dapper.Contrib 在 Oracle 环境下引发 ORA-00928 异常问题的解决 - 元视角 .NET Core 中对象池(Object Pool)的使用 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(下):EventBus 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(中):RabbitMQ 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(上):基础篇 - 元视角 记一次从已损坏的 Git 仓库中找回代码的经历 - 元视角 .NET Core 原生 DI 扩展之属性注入实现 - 元视角
异步 Lambda 表达式问题的探索 - 元视角
飞鸿踏雪 · 2017-04-16 · via 元视角

各位朋友,大家好,欢迎大家关注我的博客,我是 Payne,我的博客地址是:http://qinyuanpei.com。今天博主想和大家探讨的是,.NET 中异步 Lambda 表达式的问题。为什么要讨论这个问题呢,这或许要从公司首席架构推广内部框架这件事情说起。我其实很久以前就有这种在团队内部做技术演进的想法,即通过公共类库、团队 Wiki 和技术交流等形式逐步地推进和完善团队整体架构的统一,因为一个团队在业务方向和技术选型上基本是一致的,因此团队内的技术演进对提高开发效率和交付质量意义重大,所以我能理解首席架构在内部推广公共类库这件事情,因为除了 KPI 这种功利性的目标以外,从长远来看这些东西对一个团队来说是积极而有利的,可是我们都知道工程师是这个世界上最傲慢的人,如果一个东西设计得不好,他们一定会尝试去改进甚至重新设计,所以架构并非是一种虚无缥缈的、凭空想象出来的东西,它的存在必须是为了解决某种问题。

  所以我始终认为,架构设计必须由一线开发人员来提炼和抽象,因为只有真正经历过"坑"的人,才会清楚地知道团队里最需要解决的问题是什么,一个良好的架构绝对不是由某些所谓"专家"闭门造车的结果,你只有真正了解了一个问题,懂得如何去定义一个问题,你才会知道目前这个团队中最迫切需要去解决的问题是什么,虽然说团队里技术层次存在差异,一个技术选型必然会和普通社会学问题一样存在众口难调的情形,可是一个东西设计得不好它就是不好,你不能强迫团队成员必须去使用它,因为这实在有悖于"自由"和"分享"的黑客文化。我相信软件开发没有银弹可言,这意味着它没有一种一劳永逸的解决方案,即使它的抽象层次再高、代码鲁棒性再好,所以团队内部技术演进应该采取"自下而上"的方式,对待工程师最好的方式就是给他们充分的自由,“自上而下"的行政命令不适合工程师文化,自计算机文明诞生以来,那种来自内心深处的"极客思维"决定了我们的基因,所以啊,“请原谅我一生不羁放纵爱自由”。

  好了,现在回到这个问题本身,问题产生的根源来自 ICommand 接口,而我们都知道该接口主要承担命令绑定作用。通过 ICommand 接口的定义我们可以知道,ICommand 接口的 Execute 方法是一个同步方法,因此常规的做法如 RelayCommand 或者 DelegateCommand,基本上都是传入一个 Action 来指向一个具体方法,最终 ICommand 接口中的 Execute 方法执行的实际上是这个具体方法。截止到目前为止,这个策略在主流的场景下都实施得非常好,可是我们在引入 Task、async/await 这些新的概念以后,我们突然发现 ICommand 接口存在一个亟待解决的问题,即它缺乏一个支持异步机制的 Execute 方法,显然这是一个历史遗留问题。      我开始关注这个问题是当我在同事 John 和 Charles 的项目中看到类似下面的代码,事实上他们都是非常优秀的高级工程师,在对这个问题理解和探讨的过程中,我要特别感谢他们愿意分享他们的想法。我们一起来看看下面的代码:

public RelayCommand RunCommand
{
  get
  {
    return new RelayCommand(async ()=>{
      /* await awaitable */
    });
  }
}

请相信你的眼睛,因为你没有看错,让我倍感纠结的的正是这样一段简单的代码。这段代码让我迷惑的地方有两处,第一,RelayCommand 实现了 ICommand 接口,而 ICommand 接口的 Execute 方法是一个同步的方法,为什么我们可以在这个里传入一个异步方法,并通过 Action 这种委托类型来对其进行包装;第二,Action 是一个 void 类型,即无返回值的委托类型,我们这里显然使用 async 关键字修饰了一个无返回值的方法,因为我们在这个匿名方法内部使用了 await 语法。可是我们知道微软官方的建议是,使用 async 关键字来修饰一个返回值类型为 Task 或者 Task的方法。在我了解到 async 关键字还可以这样使用以后,对第二处疑惑我稍稍有些许释怀,因为事实上 Charles 就是正式通过这种思路来启发我,可我始终无法理解,为什么我们可以在一个同步的方法里执行一段异步代码,并试图去安慰自己说这段代码是异步的,在执行一个非常耗时的任务时界面不会阻塞。

我们的项目需要在整个任务执行过程中输出操作日志,这意味着消息会实时地输出到界面上并且不会阻塞界面。我们在为此设计了一个基于观察者模式的消息队列,所有需要发送实时消息的模块被抽象为一个消息主题,而界面模块、日志模块等被抽象为消息观察者,所有订阅过的消息主题都会将消息推送到消息队列中,这一切目前在设计上是符合业务需求的。可是很快我们就会发现一个问题,使用 await 或者 Wait()方法时,消息并不是实时地发送到界面上去的,因为我们知道 await 或者 Wait()方法会一直等待一个异步任务执行完成,所以消息会在任务结束的一瞬间被全部发送到界面上,这显示是不符合我们的期望的,所以 Execute()方法里执行的必然是一个同步方法,它不会因为我们传入了一个异步方法而改变,况且同步和异步是相对而言的,如果我们将 await 语法修改为 Task.Run(),我们就会发现在异步任务执行完成前同步方法就开始执行了,而这正是我们想要的结果。

在这里我更感兴趣的一个问题是,.NET 框架中的委托、匿名方法、Lambda 表达式和 Task 是不同时期.NET 的产物,那么我们在这里使用一个 async 关键字来修饰一个匿名方法,编译器在处理它的时候到底会怎么做呢?因为我们知道委托会被编译成一个包装类,那么现在在这篇文章中的提到的这个问题背景下,它会有什么不同呢?我们一起来看下面的代码:

static void Main(string[] args)
{
  Action action1 = async () => await DoWorkAsync();
  Action action2 = () => DoWork();
}

我们注意到这里声明了两个 Action,即两个没有返回值的委托类型,它们的不同点在于前者使用了 async/await 这两个关键字,而后者则是一个普通的同步方法,那么这两者生成的 IL 代码是否有区别呢?我们可以通过 IL DASM 或者是 IL Spy 这两个工具来查看 IL 代码:

查看IL代码 查看IL代码

我们可以注意到两点,第一,两个委托类型生成的中间代码完全一致,都是CachedAnonymousMethodDelegate,这在某种程度上说明不管 Action 里包装的是一个同步方法还是一个异步方法,最终生成的 IL 代码应该都是相同的。第二,同匿名方法和扩展方法一样,async/await 并未引入新的 IL 指令,async/await 内部应该是在维护一个状态机,这一点和 yield 关键字应该是相似的,并且对于异步的匿名方法(指 voild 类型),通过 IL 代码可知它是由AsyncVoidMethodBuilder类来生成的,而对于异步的方法(指 Task 和 Task类型),则是由AsyncTaskMethodBuilder类来生成,需要说明的是这两者在功能上相差无几,唯一的区别就在于异常处理。

关于异步编程中异常的处理,老赵在其博客关于 C#中 async/await 中的异常处理(上)关于 C#中 async/await 中的异常处理(下)这两篇博客中做了非常详细的解释,建议大家有时间的话去阅读这两篇文章,我们在这里关注结论就好。    具体来讲,async Task 或者 async Task方法引发异常时,会捕获异常并将其放置在 Task 对象里,并且只有 Task 对象被 await 时会引发异常。特别地,在调用 Task.WhenAll()方法时,一个 Task 对象中可能会含有多个异常,此时 await 仅仅会重新抛出第一个异常,但是在 Task 上使用 Task.Wait 或 Task.Result 同步阻塞时,所有异常都会用 AggregateException 包装后引发。对于嵌套的 Task,即含有子任务的 Task,应该采用 AggregateException 来获取和处理所有的异常。Task/Task中未捕获的异常可以通过 TaskScheduler.UnobservedTaskException 来处理,这些异常不会继续向上抛导致程序异常退出。    async void 方法引发异常时,因为它没有 Task 对象来放置异常,因此它的异常 SynchronizationContext 上引发,而且因为AsyncVoidMethodBuilder内部并没有使用 TaskScheduler,因此对于 async void 方法来说,线程池中未捕获的异常将会一直向上抛并最终导致程序异常终止,虽然我们可以在 AppDomain.UnhandledException 这个事件中捕捉到这些"未处理的异常”,但这并不能阻止程序异常终止,通过我们可以通过注册这个事件来记录异常日志,以帮助我们快速定位问题。

好了,现在我们回到这篇文章开始的问题,我们现在知道 async Task 和 async Task引发的异常,都不会是程序立即终止,除非我们显式地去 await 一个 Task 对象会引发异常,可是对 async void 来讲,一旦它引发异常,常规的 try-catch 时无法捕捉到异常的,这种"未处理的异常"会一直向上抛并最终导致程序异常终止。我为什么要说这个问题呢,因为我们在文章开始的时候写了一个异步的 lambda 表达式,最终它会被编译为 async void,我们现在应该会了解到,async void 非常容易引发未处理的异常并导致程序异常退出,所以这是微软官方最佳实践中不推荐使用 async void 的原因,因为使用 async void 就意味着我们要去捕获所有的异常。可是对标记为 async 的 lambda 表达式来讲,这个问题是非常隐蔽而且蛋疼的,或许不使用 async void 就是最为正确的选择了吧!

最后,其实坦白讲,我自己是不清楚在这篇文章里我到底说什么的,因为这样一个在项目开发中遇到的问题,其实并不是一个特别重要的内容,因为它实在是太容易被我们给忽略啦。我最初关注这个问题完全是因为好奇,因为我从来没有见到过这种 lambda 表达式的写法,虽然纠结这样一个语法上的问题,和孔乙己讨论茴香豆的"茴"字由几种写法一样,都是一个相当迂腐不堪的表现,可我庆幸这份好奇让我了解到了更多的东西。其实总结下这篇文章中关注的点,主要有:

  • 由同步方法和异步方法包装的委托类型在 IL 层面上是无差别的,委托关注的是参数列表和返回类型,和是否有 async 关键字修饰没有关系。
  • 匿名方法或者 lambda 最终依然会被编译为一个方法,在有 async 关键字修饰的情况下,建议使用 Func 而不是 Action,因为前者可以生成 async Task 或者 async Task,而后者仅仅可以生成 async void。
  • async Task/async Task和 async void 在异常处理机制上存在差异,前者未处理的异常不会继续向上抛导致程序异常退出,而后者未处理的异常会继续向上抛并导致程序异常退出,因此如果坚持要使用 async void,就一定处理各种异常。

参考文章: Microsoft - async/await - 异步编程中的最佳做法 TianFang - C# 5.0 async 函的提示和技巧