kotlin runBlocking引起的死鎖

近來我在自己負責的項目中大量應用了協(xié)程,提高了很多服務的響應時間。。直到一次需求,測試環(huán)境一切都很美好,上線第二天線下反饋一個聚合頁經(jīng)常超時卡死。分析日志發(fā)現(xiàn),其中一個服務實例雖然進程沒掛,但日志已經(jīng)停止打印,卡死在聚合頁的邏輯,下游的遠程服務一切正常,至此原因很明顯了:死鎖。

原因分析:

接口業(yè)務邏輯內(nèi)使用了大量的async{...}異步協(xié)程,用來執(zhí)行rpc調(diào)用、提高吞吐,很多底層方法用了runBlocking{...}來構造(其實為了少寫點suspend掛起函數(shù)...方便調(diào)用)。另一方面runBlocking{...}可以保證塊內(nèi)邏輯順序、阻塞執(zhí)行,直覺是這里出現(xiàn)了問題。
搜到了一篇kotlin社區(qū)內(nèi)的帖子,很多回復點出了問題所在:

默認的CommonPool線程數(shù)有限,如果底層方法使用runBlocking{...}執(zhí)行阻塞邏輯、并且頂層方法大量啟動并行任務調(diào)用這個方法,此時,這些并行的阻塞任務、底層協(xié)程均被調(diào)度到CommonPool,協(xié)程本質(zhì)上還是需要在線程下才能執(zhí)行的,可此時線程資源已經(jīng)全部被阻塞任務占用,阻塞任務又在等待其內(nèi)的協(xié)程返回結果,自此形成了死鎖。

As a general rule, the code that runs inside parallel streams should never block (runBlocking should not be used there).

解決方案:

  1. 協(xié)程均調(diào)度到一個單獨的自定義線程池,并將線程數(shù)調(diào)高。
  2. 底層方法消除runBlocking{...}的使用、均使用suspend重構為掛起函數(shù)。(推薦)

方案1改動量較小,但若訪問量繼續(xù)加大,很容易再次復現(xiàn)問題,并且大量的線程切換會適得其反,因此只適合像我這樣已經(jīng)出了問題的情況下的臨時處理方案。
根本的處理則是如方案2,在并行任務中徹底消除runBlocking{...}的使用。

附一個協(xié)程死鎖的簡單實現(xiàn):

fun main(args: Array<String>) = runBlocking {
    println("--- main start ---")
    //創(chuàng)建任務list,若默認CommonPool線程數(shù)很多,可加大任務數(shù)量模擬,p.s. List(50)
    val deferredList = List(10) {
        serviceAsync(it)
    }
    //并行啟動任務,模擬大量請求下的并發(fā)情況
    deferredList.parallelStream().forEach {
        runBlocking {
            println("start")
            println("${it.await()} end")
        }
    }
    //死鎖發(fā)生、永遠不會執(zhí)行到這里
    println("--- main end ---")
}

/**
 * 異步并行任務
 */
fun serviceAsync(order: Int) = async(CommonPool, CoroutineStart.LAZY) {
    blokingIoWork()
    order
}

/**
 * 模擬耗時的io操作
 */
fun blokingIoWork() = runBlocking {
    delay(2, TimeUnit.SECONDS)
}
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容