一句 Task.Result 就死锁, 这代码还怎么写?
一:背景
1. 讲故事
前些天把 .NET 高级调试
方面的文章索引到 github 的过程中,发现了一个有意思的评论,详见 文章,截图如下:
大概就是说在 Winform 的主线程下执行 Task.Result
会造成死锁,我也看了图中的参考链接, Stephen
是绝对的大佬,不过这篇文章对死锁的成因主要还是大段的文字灌输,没有真的让你眼见为实,那这篇我就从 windbg 的角度来给它剖析下。
二: windbg 分析
1. 真的会死锁吗?
看文章看截图貌似真的会死锁,当然我多年不玩 winform 了,也搞不清楚到底会不会,至少在 Console 中是不会的,得,先上一段测试代码。
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
var jsonTask = GetJsonAsync("http://cnblogs.com").Result;
textBox1.Text = jsonTask;
}
public async static Task<string> GetJsonAsync(string uri)
{
using (var client = new HttpClient())
{
var jsonString = await client.GetStringAsync(uri);
return jsonString;
}
}
}
代码非常简单,把程序跑起来,点一下 click,果然界面卡住了,有点不可思议。
2. 寻找死锁原因
接下来赶紧祭出 windbg 附加到进程上一探究竟吧。
1) 查看主线程
界面无响应了,自然是主线程卡住了,所以急需看一下此时的主线程在干嘛? 用命令 ~0s + !clrstack
即可。
0:000> !clrstack
OS Thread Id: 0x5a10 (0)
Child SP IP Call Site
0000004d10dfde00 00007ffb889a10e4 [GCFrame: 0000004d10dfde00]
0000004d10dfdf28 00007ffb889a10e4 [HelperMethodFrame_1OBJ: 0000004d10dfdf28] System.Threading.Monitor.ObjWait(Boolean, Int32, System.Object)
0000004d10dfe040 00007ffb66920d64 System.Threading.ManualResetEventSlim.Wait(Int32, System.Threading.CancellationToken)
0000004d10dfe0d0 00007ffb6691b4bb System.Threading.Tasks.Task.SpinThenBlockingWait(Int32, System.Threading.CancellationToken)
0000004d10dfe140 00007ffb672601d1 System.Threading.Tasks.Task.InternalWait(Int32, System.Threading.CancellationToken)
0000004d10dfe210 00007ffb6725cfa7 System.Threading.Tasks.Task`1[[System.__Canon, mscorlib]].GetResultCore(Boolean)
0000004d10dfe250 00007ffb18172a1b WindowsFormsApp4.Form1.button1_Click(System.Object, System.EventArgs) [E:\net5\ConsoleApp1\WindowsFormsApp4\Form1.cs @ 26]
0000004d10dfe2b0 00007ffb3a024747 System.Windows.Forms.Control.OnClick(System.EventArgs)
0000004d10dfe2f0 00007ffb3a027b83 System.Windows.Forms.Button.OnClick(System.EventArgs)
0000004d10dfe340 00007ffb3a837231 System.Windows.Forms.Button.OnMouseUp(System.Windows.Forms.MouseEventArgs)
0000004d10dfe400 00007ffb3a7e097d System.Windows.Forms.Control.WmMouseUp(System.Windows.Forms.Message ByRef, System.Windows.Forms.MouseButtons, Int32)
0000004d10dfe480 00007ffb3a0311cc System.Windows.Forms.Control.WndProc(System.Windows.Forms.Message ByRef)
0000004d10dfe540 00007ffb3a0b0c97 System.Windows.Forms.ButtonBase.WndProc(System.Windows.Forms.Message ByRef)
0000004d10dfe5c0 00007ffb3a0b0be5 System.Windows.Forms.Button.WndProc(System.Windows.Forms.Message ByRef)
0000004d10dfe5f0 00007ffb3a030082 System.Windows.Forms.NativeWindow.Callback(IntPtr, Int32, IntPtr, IntPtr)
0000004d10dfe690 00007ffb3a765a02 DomainBoundILStubClass.IL_STUB_ReversePInvoke(Int64, Int32, Int64, Int64)
0000004d10dfe9d0 00007ffb776d221e [InlinedCallFrame: 0000004d10dfe9d0] System.Windows.Forms.UnsafeNativeMethods.DispatchMessageW(MSG ByRef)
0000004d10dfe9d0 00007ffb3a0b9489 [InlinedCallFrame: 0000004d10dfe9d0] System.Windows.Forms.UnsafeNativeMethods.DispatchMessageW(MSG ByRef)
0000004d10dfe9a0 00007ffb3a0b9489 DomainBoundILStubClass.IL_STUB_PInvoke(MSG ByRef)
0000004d10dfea60 00007ffb3a046661 System.Windows.Forms.Application+ComponentManager.System.Windows.Forms.UnsafeNativeMethods.IMsoComponentManager.FPushMessageLoop(IntPtr, Int32, Int32)
0000004d10dfeb50 00007ffb3a045fc7 System.Windows.Forms.Application+ThreadContext.RunMessageLoopInner(Int32, System.Windows.Forms.ApplicationContext)
0000004d10dfebf0 00007ffb3a045dc2 System.Windows.Forms.Application+ThreadContext.RunMessageLoop(Int32, System.Windows.Forms.ApplicationContext)
0000004d10dfec50 00007ffb181708e2 WindowsFormsApp4.Program.Main() [E:\net5\ConsoleApp1\WindowsFormsApp4\Program.cs @ 19]
0000004d10dfee78 00007ffb776d6923 [GCFrame: 0000004d10dfee78]
从堆栈输出看,主线程最后是卡在 Task.Result
下的 Monitor.ObjWait
上,也就是说它还没有取到最后的 jsonString
,这就很奇怪了,都好几分钟了,难道网络出问题啦 ? 我这网可是100M火力全开。。。
2) jsonString 哪去了?
判断是不是网络的问题,有一个好办法,那就是直接暴力搜索托管堆,如果在托管堆上发现了 jsonString,那就说明是程序上的某些地方让 Result
迟迟得不到结束,用命令 !dumpheap -type String -min 8500
+ !do 000001f19002fcf0
查看即可,如下图所示:
从图中可以清晰的看出 html 回来了,既然都回来了,为啥还没让 Task.Result
结束呢? 下一步就是看一看这个 html 被谁持有,使用 !gcroot
即可。
0:000> !gcroot 000001f19002fcf0
Thread 5a10:
0000004d10dfe250 00007ffb18172a1b WindowsFormsApp4.Form1.button1_Click(System.Object, System.EventArgs) [E:\net5\ConsoleApp1\WindowsFormsApp4\Form1.cs @ 26]
rbp+10: 0000004d10dfe2b0
-> 000001f180007f78 WindowsFormsApp4.Form1
-> 000001f180070d68 System.ComponentModel.EventHandlerList
-> 000001f180071718 System.ComponentModel.EventHandlerList+ListEntry
-> 000001f1800716d8 System.EventHandler
-> 000001f1800716b0 System.Windows.Forms.ApplicationContext
-> 000001f180071780 System.EventHandler
-> 000001f18006ab38 System.Windows.Forms.Application+ThreadContext
-> 000001f18006b140 System.Windows.Forms.Application+MarshalingControl
-> 000001f18016c9c8 System.Collections.Queue
-> 000001f18016ca00 System.Object[]
-> 000001f18016c948 System.Windows.Forms.Control+ThreadMethodEntry
-> 000001f18016c8b8 System.Object[]
-> 000001f1800e6f80 System.Action
-> 000001f1800e6f60 System.Runtime.CompilerServices.AsyncMethodBuilderCore+MoveNextRunner
-> 000001f1800a77d0 WindowsFormsApp4.Form1+<GetJsonAsync>d__2
-> 000001f1800b4e50 System.Threading.Tasks.Task`1[[System.String, mscorlib]]
-> 000001f19002fcf0 System.String
Found 1 unique roots (run '!GCRoot -all' to see all roots).
从输出结果看,这个 System.String
最后被 5a10
线程的 WindowsFormsApp4.Form1
持有,可以用 !t
验证一下 5a10
到底是什么线程。
0:000> !t Lock
ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception
0 1 5a10 000001f1f1b01200 2026020 Preemptive 000001F1800E70E8:000001F1800E7FD0 000001f1f1ad5b90 0 STA
2 2 712c 000001f1f1b2a270 2b220 Preemptive 0000000000000000:0000000000000000 000001f1f1ad5b90 0 MTA (Finalizer)
我去,5a10
竟然是主线程,真的有点混乱,主线程被卡死,string 又被主线程持有,完全是莫名其妙。
3) 寻找突破点
还是回过头下冷静思考下这条 引用链
,我发现这里有一个 Queue: -> 000001f18016c9c8 System.Collections.Queue
,有思路了,我可以在入 Queue 的地方下个 断点
来调试下源代码,工具用 DnSpy
, 说干就干。
从图中可以看到,当前入Queue时,用的是线程 10
,也就是说此时 string 还没被主线程持有,再仔细分析下这个调用栈,我想你应该就搞清楚了,反正我看完之后脑子中就有了这张图。
从图中可以发现,延续的 Task
最后被 WindowsFormsSynchronizationContext.Post
调度到了 Control 下的 Queue 中,而这 Queue 中的数据需要 UI线程 去执行,所以就有了下面的对话:
主线程: task小弟,你什么时候执行完呀,我在等你信号呢?
task: 老哥,我已在你家啦,你什么时候过来接我呀?
总而言之:task需要主线程来执行它,主线程却在傻傻的等待 task 的 complete 状态,所以延续的task永远得不到执行,这就出现了很尴尬的场面,不知道你明白了吗?
三: 破解之法
知道了前因后果,这破解之法就简单了,大体上分两种。
1. 禁止将 延续task 丢到 Queue 中
要切断这条路,言外之意就是让线程池自己结束这个 task,这样 UI线程
就能感知到这个task已完成,最终 UI线程
就能获取最后的 html,做法就是在 await 后加上 ConfigureAwait(false)
, 参考如下:
2. 禁止阻塞主线程
如果不阻塞主线程,那么主线程就可以自由的在 Control.Queue
中获取需要执行的任务,改法也很简单,只需要在 GetJsonAsync 前加上 await
即可。
三:总结
结论就是多自己实操实操,理论知识是别人强制灌输给你的,到底对还是不对,其实你自己心里也没底,实操验证才是真正属于你的,而且也很难忘记,毕竟你曾今真的体验过,实操过,验证过。
更多高质量干货:参见我的 GitHub: dotnetfly
一句 Task.Result 就死锁, 这代码还怎么写?的更多相关文章
- async await task.Result 卡死
在如下代码中: public async Task<string> GetData() { return await DoWork(); } 在UI线程中调用 var data = Get ...
- 初步学习async/await,Task.GetAwaiter,Task.Result
网上关于async/await的知识有很多,看了很多但不如自己实践一遍来得快,所以这里记录下我的理解和大家学习下. 首先以最简单的同步方法来开始如下 private static void Test( ...
- 代码轮子之很简单但是挺管用的基于C# Task的模拟并发的代码
代码轮子之很简单但是挺管用的基于C# Task的模拟并发的代码
- .net core Task.Result Wait等造成502
这两天公众号项目上线,刚开始项目运行没什么问题,但几天之后,访问量激增,服务器崩溃了,每次请求都返回502,一脸懵逼,无从下手,赶紧开日志里的BUG,拿出来一个个改,BUG都改完之后,没有明显的效果, ...
- async,await与task.wait()或task.Result的区别
你是否曾经与我一样不理解async,await与task.wait()或者task.Result的区别? 接下来,一个Demo让你看出他们之间的区别. static void Main(string[ ...
- Task.Result跟 Task.GetAwaiter.GetResult()相同吗?怎么选?
前几天在用线程池执行一些任务时运到一种情形,就是回调方法中使用到了异步方法,但是回调方法貌似不支持async await的写法.这时候我应该如何处理呢?是使用Task.Result来获取返回结果,还是 ...
- java中异常抛出后代码还会继续执行吗
今天遇到一个问题,在下面的代码中,当抛出运行时异常后,后面的代码还会执行吗,是否需要在异常后面加上return语句呢? public void add(int index, E element){ i ...
- ClownFish:比手写代码还快的通用数据访问层
http://www.cnblogs.com/fish-li/archive/2012/07/17/ClownFish.html 阅读目录 开始 ClownFish是什么? 比手写代码还快的执行速度 ...
- Markdown: 用写代码的思维写文档
作者:吴香伟 发表于 2014/08/07 版权声明:可以任意转载,转载时务必以超链接形式标明文章原始出处和作者信息以及版权声明 本文不讲解Markdown的语法规则,只关注它带来的好处以及我使用的方 ...
随机推荐
- VSCode 微信小程序扩展开发
写在前面 为什么要开发这个扩展呢,是因为微信开发者工具自身不支持页面引入组件的跳转,人工根据引入组件路径查看对应代码的方式,效率偏低.就形如这样的json文件,引入了多个组件,比如要查看 " ...
- 官方正式发布 Java 16
前言 就在2021/03/16,官方正式发布了Java 16.我们可以下载使用Java 16了. 特性 向量API(孵化) 在运行期,Vector 表示向量计算可以可靠地编译成支持CPU架构上的最佳矢 ...
- js_笔记_8月7日记录_活动对象_作用域链_按值传递
活动对象:简单说就是这个函数的参数和显示声明的变量或函数. 函数内接受的参数实际是创建了一个局部变量:[形参名] = [传进来的值],js的函数传参只传值. 作用域链:执行流进入一个函数,会先创建出作 ...
- vue自定义插件封装,实现简易的elementUi的Message和MessageBox
vue自定义插件封装示例 1.实现message插件封装(类似简易版的elementUi的message) message组件 <template> <transition ...
- 微信小程序授权登录以及用户信息相关接口调整导致授权框不弹出
前言:4月8号升级了小程序业务后提交了版本并上线.突然一个同事说体验版的点击"登录"按钮无效.当时觉得应该不会呀,这几天一直用手机调试,每天也在不停的登录授权,弹框一直有的呀.然后 ...
- 多线程安全的单例模式(使用判断nullptr和call_once两种方法)
转载请注明: https://blog.csdn.net/Stephen___Qin/article/details/115583694 使用判断nullptr #include <thread ...
- 火爆外网的 DGS 框架使用
Netflix 已开放其 Domain Graph Service(DGS)框架的源代码 ,该框架是为了方便整合 GraphQL 使用,用于简化 GraphQL 的实现. GraphQL 主要是作用于 ...
- 「一站式」兼容所有云厂商文件存储Spring Boot 实现
背景 在互联网发展的今天,近乎所有的云厂商都提供对象存储服务.一种海量.安全.低成本.高可靠的云存储服务,适合存放任意类型的文件.容量和处理能力弹性扩展,多种存储类型供选择,全面优化存储成本. 当我们 ...
- Kubernetes部署metrics-server提示健康检测报错500,简单解决方式
为什么写? 最近有项目要用到HPA(Horizontal Pod Autoscaler)依赖了k8s的 metrics 指标才能做出自动缩扩容的动作,我这边用官方GitHub v0.4.2版本启动不起 ...
- CentOS8搭建FTP服务器
2021.2.20 更新 1 概述 文章核心: CentOS8使用vsftpd搭建FTP服务器 安装以及测试的详细过程 2 安装 2.1 安装vsftpd+ftp sudo yum install - ...