架构师_程序员_码农网

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 2468|回复: 2

[交流] 【转】一句 Task.Result 就死锁, 这代码还怎么写?

[复制链接]
发表于 2022-8-7 21:11:58 | 显示全部楼层 |阅读模式
一:背景

1. 讲故事

前些天把 .NET 高级调试 方面的文章索引到 github 的过程中,发现了一个有意思的评论,详见 文章,截图如下:

QQ截图20220807210545.jpg

大概就是说在 Winform 的主线程下执行 Task.Result 会造成死锁,我也看了图中的参考链接, Stephen 是绝对的大佬,不过这篇文章对死锁的成因主要还是大段的文字灌输,没有真的让你眼见为实,那这篇我就从 windbg 的角度来给它剖析下。

二: windbg 分析

1. 真的会死锁吗?

看文章看截图貌似真的会死锁,当然我多年不玩 winform 了,也搞不清楚到底会不会,至少在 Console 中是不会的,得,先上一段测试代码。

代码非常简单,把程序跑起来,点一下 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 查看即可,如下图所示:

QQ截图20220807210733.jpg

从图中可以清晰的看出 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, 说干就干。

QQ截图20220807210911.jpg

从图中可以看到,当前入Queue时,用的是线程 10,也就是说此时 string 还没被主线程持有,再仔细分析下这个调用栈,我想你应该就搞清楚了,反正我看完之后脑子中就有了这张图。

QQ截图20220807210941.jpg

从图中可以发现,延续的 Task 最后被 WindowsFormsSynchronizationContext.Post 调度到了 Control 下的 Queue 中,而这 Queue 中的数据需要 UI线程 去执行,所以就有了下面的对话:

主线程: task小弟,你啥时候执行完呀? 我在等你完成信号呢?

task: 老哥,你不执行我,我怎么完成呀?

主线程: 艹...

总而言之:延续task 已经到了 queue 中排队等待主线程执行,而此时主线程是个愣头青一直死死的等着 延续Task 的 Complete=true ,问题就在这里:延续task 不被执行怎么可能 Complete=true 呢? 所以它们就这样海枯石烂的天荒地老。

三: 破解之法

知道了前因后果,这破解之法就简单了,大体上分两种。

1. 禁止将 延续task 丢到 Queue 中

要切断这条路,言外之意就是让线程池自己结束这个 task,这样 UI线程 就能感知到这个task已完成,最终 UI线程 就能获取最后的 html,做法就是在 await 后加上 ConfigureAwait(false) , 参考如下:

QQ截图20220807211039.jpg

2. 禁止阻塞主线程

如果不阻塞主线程,那么主线程就可以自由的在 Control.Queue 中获取需要执行的任务,改法也很简单,只需要在 GetJsonAsync 前加上 await 即可。

QQ截图20220807211111.jpg

三:总结

结论就是多自己实操实操,理论知识是别人强制灌输给你的,到底对还是不对,其实你自己心里也没底,实操验证才是真正属于你的,而且也很难忘记,毕竟你曾今真的体验过,实操过,验证过。

原文:https://www.cnblogs.com/huangxincheng/p/14754776.html





上一篇:【实战】Docker 修改和限制容器CPU、内存等资源
下一篇:JS 数组 every 和 some 区别和使用
码农网,只发表在实践过程中,遇到的技术难题,不误导他人。
发表于 2022-8-8 17:01:32 | 显示全部楼层
学习学习。。。
码农网,只发表在实践过程中,遇到的技术难题,不误导他人。
发表于 2022-9-2 11:39:19 | 显示全部楼层
学习一下看看
码农网,只发表在实践过程中,遇到的技术难题,不误导他人。
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

免责声明:
码农网所发布的一切软件、编程资料或者文章仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。

Mail To:help@itsvse.com

QQ|手机版|小黑屋|架构师 ( 鲁ICP备14021824号-2 )|网站地图

GMT+8, 2024-4-27 03:31

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表