一碼阻塞,萬碼等待:ASP.NET Core 同步方法調用異步方法「死鎖」的真相

在咱們 2015 年開始的從 .NET Framework 向 .NET Core 遷移的工程中,遇到的最大的坑就是標題中所說的——同步方法中調用異步方法發生」死鎖」。雖然在 .NET Framework 時代就知道不能在同步方法中調用異步方法,但咱們卻明知路有坑,偏向此路行。不是咱們自討苦吃,而是被迫無奈,由於在 .NET Core 2.0 以前,BCL(基礎類庫)中有些 API 只有異步實現沒有同步實現,好比用於將主機名解析爲 IP 地址的 API —— Dns.GetHostAddressesAsync() 。html

但最終「被迫無奈」變成「血的教訓」,這根本不是坑,而是無底洞。不管在開發與測試環境中多麼正常,只要一發布到生產環境有必定併發量就會發生「死鎖」 —— 大量請求無響應,一直處於等待狀態,線程池發飆,線程數持續不斷地增加,內存隨之增加,直至撐爆服務器(詳見當時的一篇隨筆 .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)。每一個線程接活(從隊列中取出任務執行)的順序是這樣的:先從本身的本地隊列中找活 -> 若是本地隊列爲空,則從全局隊列中找活 -> 若是全局隊列爲空,則從其餘線程的本地隊列中搶活。ui

咱們來想象一下異步方法等待同步方法的場景。當10個併發請求到達時(進入的是全局隊列),假設線程池中正好有10個空閒線程,這10個線程立馬把活接過來,但線程在執行過程當中遇到了同步方法等待異步方法(Task.Wait)的狀況而進入阻塞狀態,無奈地無所事事地在那乾等異步方法執行完成而沒法幫其餘線程幹活(這時狀況已經有些不妙,因爲阻塞線程池少了10個幹活的線程)。雪上加霜的是,這些阻塞的線程所等待的異步方法在完成異步操做執行 await 以後的代碼時也須要線程,不只幹活的線程少了,並且剩下的線程要乾的活更多了(狀況更不妙了)。隨着併發請求持續不斷地進來,形勢變得愈來愈嚴峻,被阻塞的線程愈來愈多,能幹活的線程愈來愈少並且要乾的活愈來愈多,因而愈來愈多的一線幹活的線程的隊列開始排起了長隊。火上澆油的是,那些阻塞着的線程要退出阻塞狀態須要等它們所等待的任務被正忙得不可開交的幹活線程執行,幹活線程越忙,它們被阻塞的時間越長。因而出現了一個奇怪的場面,一羣不幹活的線程圍觀並等待着少數幹活的線程,眼看着這些幹活線程的隊列排隊愈來愈長,雖然它們也能幹活,但因爲它們被關在小黑屋裏,沒法出手相助,要等它們的主人將它們釋放出來,而它們的主人就排在長隊中等着從幹活線程那拿到小黑屋的鑰匙。。。這樣的場面最終只有一個結局,全部幹活的線程的本地隊列都排起了長隊,沒有空閒的線程。線程

好戲開始了,不,是災難開始了。線程池中沒有空閒線程,全局隊列中的活沒人接,因而全局隊列開始排隊,線程池的線程不夠用,若是不趕忙補充線程進來,線程池會被餓死(Threadpool Starvation)。救援行動開始了,CLR 趕忙生產線程餵給線程池,因爲全局隊列享有最高優先級(根據以前所述的線程接活順序),一喂進去就被全局隊列吃了,但 CLR 一秒鐘只能生產1-2個線程,遠遠知足不了全局隊列的胃口,而最須要救援的各個幹活線程的本地隊列連湯都喝不到。除了 CLR 的外部救援,線程池也同時進行自救,有些線程玩命幹活,終於處理完了本身隊列中的任務,終於有機會能夠幫助其餘同伴了,可是它們當即接到了上級命令 —— 以最快速度去救援全局隊列,軍令不可違,它們眼睜睜地看着同伴絕望地處理着無邊無際的長隊中的任務,奔赴全局隊列,自救也救不到幹活線程的本地隊列。htm

這種徹底以全局隊列爲中心、救地位最高的、不救最須要的救援行動最終帶來了毀滅性的結果。那些解救全局隊列的線程又由於 Task.Wait 而阻塞而須要更多的線程執行阻塞所等待的任務。救援行動變成了自殺行動,線程池就這樣被活活餓死了(Threadpool Starvation)。blog

這就是我所推測的真相,真相背後的真正罪魁禍首實際上是對線程的阻塞,因此千萬千萬不要阻塞(blocking)線程。

相關文章
相關標籤/搜索