一码阻塞,万码等待:ASP.NET Core 同步方法调用异步方法“死锁”的真相
在我們 2015 年開始的從 .NET Framework 向 .NET Core 遷移的工程中,遇到的最大的坑就是標題中所說的——同步方法中調用異步方法發生”死鎖”。雖然在 .NET Framework 時代就知道不能在同步方法中調用異步方法,但我們卻明知路有坑,偏向此路行。不是我們自討苦吃,而是被迫無奈,因為在 .NET Core 2.0 之前,BCL(基礎類庫)中有些 API 只有異步實現沒有同步實現,比如用于將主機名解析為 IP 地址的 API —— Dns.GetHostAddressesAsync() 。
但最終“被迫無奈”變成“血的教訓”,這根本不是坑,而是無底洞。無論在開發與測試環境中多么正常,只要一發布到生產環境有一定并發量就會發生“死鎖” —— 大量請求無響應,一直處于等待狀態,線程池發飆,線程數持續不斷地增長,內存隨之增長,直至撐爆服務器(詳見當時的一篇隨筆?.NET Core 中遇到奇怪的線程死鎖問題:內存與線程數不停地增長)。
我們想盡一切方法,用盡網上能找到的同步方法調用異步方法避免死鎖的辦法,都于事無補,唯有去掉同步方法調用異步方法的代碼。當我們意識這是一個無底洞后,趕緊繞道而行,全面放棄在同步方法中調用異步方法,并將“千萬千萬不要在同步方法中調用異步方法”作為一條 .NET Core 開發準則。
這段踩坑踩到無底洞的血淚史,每當想起都很心痛,心痛不是當時的任何努力都是那么的蒼白無力,而是對問題背后原因的困惑 —— 為什么同步方法中 Wait 異步方法會產生如此致命的后果?如果真的千萬千萬不能這么干,那 .NET Core 為什么不直接在編譯時就報錯?“死鎖”的背后究竟發生了什么?
。。。
2018年10月20日偶然間發現一個網站 ——?dotNET Weekly?,在其中發現一篇10月17日發布的博文 ——?.NET Threadpool starvation, and how queuing makes it worse,在讀懂這篇博文之后,聯系到之前踩坑的經歷,終于想通了“死鎖”的背后(只是個人推測,并不一定正確)。
.NET Core 線程池有 n+1 個隊列,每個線程有自己的本地隊列(n),整個線程池有一個全局隊列(1)。每個線程接活(從隊列中取出任務執行)的順序是這樣的:先從自己的本地隊列中找活 -> 如果本地隊列為空,則從全局隊列中找活 -> 如果全局隊列為空,則從其他線程的本地隊列中搶活。
我們來想象一下異步方法等待同步方法的場景。當10個并發請求到達時(進入的是全局隊列),假設線程池中正好有10個空閑線程,這10個線程立馬把活接過來,但線程在執行過程中遇到了同步方法等待異步方法(Task.Wait)的情況而進入阻塞狀態,無奈地無所事事地在那干等異步方法執行完成而無法幫其他線程干活(這時情況已經有些不妙,由于阻塞線程池少了10個干活的線程)。雪上加霜的是,這些阻塞的線程所等待的異步方法在完成異步操作執行 await 之后的代碼時也需要線程,不僅干活的線程少了,而且剩下的線程要干的活更多了(情況更不妙了)。隨著并發請求持續不斷地進來,形勢變得越來越嚴峻,被阻塞的線程越來越多,能干活的線程越來越少而且要干的活越來越多,于是越來越多的一線干活的線程的隊列開始排起了長隊。火上澆油的是,那些阻塞著的線程要退出阻塞狀態需要等它們所等待的任務被正忙得不可開交的干活線程執行,干活線程越忙,它們被阻塞的時間越長。于是出現了一個奇怪的場面,一群不干活的線程圍觀并等待著少數干活的線程,眼看著這些干活線程的隊列排隊越來越長,雖然它們也能干活,但由于它們被關在小黑屋里,無法出手相助,要等它們的主人將它們釋放出來,而它們的主人就排在長隊中等著從干活線程那拿到小黑屋的鑰匙。。。這樣的場面最終只有一個結局,所有干活的線程的本地隊列都排起了長隊,沒有空閑的線程。
好戲開始了,不,是災難開始了。線程池中沒有空閑線程,全局隊列中的活沒人接,于是全局隊列開始排隊,線程池的線程不夠用,如果不趕緊補充線程進來,線程池會被餓死(Threadpool Starvation)。救援行動開始了,CLR 趕緊生產線程喂給線程池,由于全局隊列享有最高優先級(根據之前所述的線程接活順序),一喂進去就被全局隊列吃了,但 CLR 一秒鐘只能生產1-2個線程,遠遠滿足不了全局隊列的胃口,而最需要救援的各個干活線程的本地隊列連湯都喝不到。除了 CLR 的外部救援,線程池也同時進行自救,有些線程玩命干活,終于處理完了自己隊列中的任務,終于有機會可以幫助其他同伴了,但是它們立即接到了上級命令 —— 以最快速度去救援全局隊列,軍令不可違,它們眼睜睜地看著同伴絕望地處理著一望無際的長隊中的任務,奔赴全局隊列,自救也救不到干活線程的本地隊列。
這種完全以全局隊列為中心、救地位最高的、不救最需要的救援行動最終帶來了毀滅性的結果。那些解救全局隊列的線程又因為 Task.Wait 而阻塞而需要更多的線程執行阻塞所等待的任務。救援行動變成了自殺行動,線程池就這樣被活活餓死了(Threadpool Starvation)。
這就是我所推測的真相,真相背后的真正罪魁禍首其實是對線程的阻塞,所以千萬千萬不要阻塞(blocking)線程。
原文地址:https://www.cnblogs.com/dudu/p/9860959.html
.NET社區新聞,深度好文,歡迎訪問公眾號文章匯總 http://www.csharpkit.com
總結
以上是生活随笔為你收集整理的一码阻塞,万码等待:ASP.NET Core 同步方法调用异步方法“死锁”的真相的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: .NET Core使用IO合并技巧轻松实
- 下一篇: 鬼才项斌,用人工智能推动教育服务创新