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

推荐订阅源

N
News and Events Feed by Topic
S
SegmentFault 最新的问题
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Last Week in AI
Last Week in AI
Jina AI
Jina AI
H
Help Net Security
C
Check Point Blog
aimingoo的专栏
aimingoo的专栏
MyScale Blog
MyScale Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Vercel News
Vercel News
L
LangChain Blog
Recorded Future
Recorded Future
F
Full Disclosure
Google DeepMind News
Google DeepMind News
Microsoft Security Blog
Microsoft Security Blog
I
InfoQ
GbyAI
GbyAI
B
Blog RSS Feed
T
The Blog of Author Tim Ferriss
Engineering at Meta
Engineering at Meta
A
About on SuperTechFans
M
MIT News - Artificial intelligence
爱范儿
爱范儿
V
V2EX
Microsoft Azure Blog
Microsoft Azure Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Y
Y Combinator Blog
B
Blog
WordPress大学
WordPress大学
Blog — PlanetScale
Blog — PlanetScale
W
WeLiveSecurity
MongoDB | Blog
MongoDB | Blog
Cloudbric
Cloudbric
N
News and Events Feed by Topic
The Cloudflare Blog
月光博客
月光博客
博客园 - 三生石上(FineUI控件)
有赞技术团队
有赞技术团队
D
DataBreaches.Net
博客园 - 【当耐特】
T
Troy Hunt's Blog
V
Visual Studio Blog
V2EX - 技术
V2EX - 技术
Apple Machine Learning Research
Apple Machine Learning Research
博客园 - 司徒正美
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Google Online Security Blog
Google Online Security Blog
The GitHub Blog
The GitHub Blog

寒九

Service Worker缓存图片 – 寒九 ASP.NET Core中使用EF Core实现部分更新实体记录 – 寒九 微软商店开发者账户申请 – 寒九 数据库结构管理利器——数据库迁移(数据库模式迁移) – 寒九 JPEG/JPG/JFIF/TIFF/EXIF格式解析 – 寒九 音乐播放器核心设计——播放列表设计 – 寒九 LeetCode-30 串联所有单词的子串题解 – 寒九 LeetCode-10 正则表达式匹配题解 – 寒九 WinUI 3与UWP中GridView和ListView的性能优化 – 寒九
Async方法导致的死锁与无响应 – 寒九
HHao 文章: 41 · 2024-03-20 · via 寒九

最近在写WinUI 3时,经常遇到在各种事件中调用异步方法的场景,在事件处理方法中等待异步调用完成需要使用await关键字并将事件处理方法标志为async。大部分事件处理方法都是没有返回值的,即返回值为void。此前有在一些地方看到过async void修饰的方法有一些问题,所以想着不用await/async关键词,而改用Task.Result或者Task.Wait()来阻塞当前代码,以实现await的效果,没想到直接卡死了整个程序,深入探究了一番发现了卡死的原因。本文内容主要来源于以下两篇文章:

以一个按钮事件为例:

C#

// My "library" method.
public static async Task<JObject> GetJsonAsync(Uri uri)
{
  // (real-world code shouldn't use HttpClient in a using block; this is just example code)
  using (var client = new HttpClient())
  {
    var jsonString = await client.GetStringAsync(uri);
    return JObject.Parse(jsonString);
  }
}

// My "top-level" method.
public void Button1_Click(...)
{
  var jsonTask = GetJsonAsync(...);
  textBox1.Text = jsonTask.Result;
}

在点击此按钮后,整个程序卡死无响应,其卡死的原因是因为上面的代码会造成死锁。

为什么会产生死锁呢?代码中并没有资源争抢的操作,死锁是产生在哪里呢?

在C#中,由async修饰的方法,即异步方法,会被编译器编译为状态机。当遇到调用异步方法时,CLR会捕获调用的上下文,保存在状态机中,以便异步方法完成后可以继续在相同的上下文中运行。简单来说就是把调用异步方法前的环境保存下来,然后去做别的事情,等异步方法调用完成后,再把保存的环境恢复,就可以继续运行后续的代码了。

在这个例子中,当遇到await client.GetStringAsync(uri)时,CLR保存了当前的上下文(UI上下文,因为事件处理程序都是在UI处理线程中完成的)到GetJsonAsync异步方法的状态机中去,然后将GetStringAsync方法分配给其它线程运行,UI线程就去处理后续的程序了(因为在调用GetJsonAsync时没有等待,而在调用GetStringAsync时等待了),接着遇到了jsonTask.Result,就在这里以同步方式阻塞了UI上下文来等待GetJsonAsync完成。这个时候,UI上下文被UI线程占用了并一直在等GetJsonAsync完成,而在GetJsonAsync中,GetStringAsync完成后,需要恢复上下文继续运行后续代码,它需要的上下文正是UI上下文,于是两个线程形成了互相等待,死锁就产生了。详细的过程如下:

  1. 事件处理方法Button1_Click调用 GetJsonAsync(在 UI上下文中)
  2. GetJsonAsync 通过调用 HttpClient.GetStringAsync(仍在上下文中)启动 REST 请求
  3. GetStringAsync 返回未完成的task,指示 REST 请求未完成
  4. GetJsonAsync 等待 GetStringAsync 返回的task。上下文将被捕获,稍后将用于继续运行 GetJsonAsync 方法。 GetJsonAsync 返回未完成的task,表明 GetJsonAsync 方法未完成
  5. 顶级方法等待 GetJsonAsync 返回的任务完成。这会阻塞上下文线程
  6. 最终,REST 请求将完成,这完成了 GetStringAsync 返回的task
  7. GetJsonAsync 现在已准备好继续运行后续代码,并且它将等待上下文可用,以便可以在上下文中执行。
  8. 死锁产生,Button1_Click方法正在阻塞占用上下文线程,等待 GetJsonAsync 完成,而 GetJsonAsync 正在等待上下文空闲以便完成。

所以,在事件处理程序中或者在UI线程中,尽量不要使用同步阻塞的方式调用异步方法,即不要试图使用Task.Result和Task.Wait()来保持事件处理程序的同步性。

在ASP.NET Core中,也是类似的道理,不要在Endpoint处理程序中以同步方式调用异步方法,它也会导致死锁的产生,只不过它的上下文将会变成每个请求的上下文。

实际上,async void与async Task两种形式的无返回值异步方法,其区别在于是否能够等待。一个异步方法编译形成的状态机会把其内部代码根据await关键词分割成数个部分,每个部分的完成是通过await等待的那个task中的GetAwaiter()方法调用结果决定的,一个部分完成后,由状态机决定下一步的动作。因而,若一个方法由async void修饰,那么状态机就无法得知其完成与否,因此不会等待它的完成。而一个方法若由async Task修饰,状态机就可以根据返回的task获知其是否完成,从而可以决定等待与否。

调用使用async void修饰的异步方法,称之为“发起并遗忘(Fire-and-forget)”,它只管调用,而不管其后续执行状态和结果,通常只在异步事件处理程序中使用。同时,调用方无法捕获从该方法引发的异常,此类未经处理异常有可能导致整个应用程序崩溃1

调用使用async Task修饰的异步方法,称之为“等待并继续(Wait-and-continue)”,在发起调用后会继续跟踪任务的执行状态和结果。如果返回 Task 或 Task<TResult> 的方法引发异常,则该异常会存储在返回的task中。 在await 该 task时,将重新引发异常。

综上,在事件处理方法中等待异步调用完成最好使用await关键字并将事件处理方法标志为async(几乎都为async void),这种方式并没有什么问题,也是推荐的做法。

  1. 异步返回类型 – C# | Microsoft Learn ↩︎