前些天把 .NET 高級調試
方面的文章索引到 github 的過程當中,發現了一個有意思的評論,詳見 文章,截圖以下:html
大概就是說在 Winform 的主線程下執行 Task.Result
會形成死鎖,我也看了圖中的參考連接, Stephen
是絕對的大佬,不過這篇文章對死鎖的成因主要仍是大段的文字灌輸,沒有真的讓你眼見爲實,那這篇我就從 windbg 的角度來給它剖析下。git
看文章看截圖貌似真的會死鎖,固然我多年不玩 winform 了,也搞不清楚到底會不會,至少在 Console 中是不會的,得,先上一段測試代碼。github
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,果真界面卡住了,有點難以想象。json
接下來趕忙祭出 windbg 附加到進程上一探究竟吧。網絡
界面無響應了,天然是主線程卡住了,因此急需看一下此時的主線程在幹嗎? 用命令 ~0s + !clrstack
便可。async
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火力全開。。。🤔🤔🤔工具
判斷是否是網絡的問題,有一個好辦法,那就是直接暴力搜索託管堆,若是在託管堆上發現了 jsonString,那就說明是程序上的某些地方讓 Result
遲遲得不到結束,用命令 !dumpheap -type String -min 8500
+ !do 000001f19002fcf0
查看便可,以下圖所示:oop
從圖中能夠清晰的看出 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
究竟是什麼線程。ui
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 又被主線程持有,徹底是莫名其妙。
仍是回過頭下冷靜思考下這條 引用鏈
,我發現這裏有一個 Queue: -> 000001f18016c9c8 System.Collections.Queue
,有思路了,我能夠在入 Queue 的地方下個 斷點
來調試下源代碼,工具用 DnSpy
, 說幹就幹。
從圖中能夠看到,當前入Queue時,用的是線程 10
,也就是說此時 string 還沒被主線程持有,再仔細分析下這個調用棧,我想你應該就搞清楚了,反正我看完以後腦子中就有了這張圖。
從圖中能夠發現,延續的 Task
最後被 WindowsFormsSynchronizationContext.Post
調度到了 Control 下的 Queue 中,而這 Queue 中的數據須要 UI線程 去執行,因此就有了下面的對話:
主線程: task小弟,你何時執行完呀,我在等你信號呢?
task: 老哥,我已在你家啦,你何時過來接我呀?
總而言之:task須要主線程來執行它,主線程卻在傻傻的等待 task 的 complete 狀態,因此延續的task永遠得不到執行,這就出現了很尷尬的場面,不知道你明白了嗎? 🤔🤔🤔
知道了來龍去脈,這破解之法就簡單了,大致上分兩種。
要切斷這條路,言外之意就是讓線程池本身結束這個 task,這樣 UI線程
就能感知到這個task已完成,最終 UI線程
就能獲取最後的 html,作法就是在 await 後加上 ConfigureAwait(false)
, 參考以下:
若是不阻塞主線程,那麼主線程就能夠自由的在 Control.Queue
中獲取須要執行的任務,改法也很簡單,只須要在 GetJsonAsync 前加上 await
便可。
結論就是多本身實操實操,理論知識是別人強制灌輸給你的,到底對仍是不對,其實你本身內心也沒底,實操驗證纔是真正屬於你的,並且也很難忘記,畢竟你曾今真的體驗過,實操過,驗證過。
更多高質量乾貨:參見個人 GitHub: dotnetfly