終於明白了 C# 中 Task.Yield 的用途
最近在閱讀ofollow,noindex" target="_blank">.NET Threadpool starvation, and how queuing makes it worse 這篇博文時發現文中程式碼中的一種 Task 用法之前從未見過,在網上看了一些資料後也是雲裡霧裡不知其解,很是困擾。今天在程式員節的大好日子裡終於想通了,於是寫下這篇隨筆分享給大家,也過過專心寫部落格的癮。
這種從未見過的用法就是下面程式碼中的await Task.Yield()
:
static async Task Process() { await Task.Yield(); var tcs = new TaskCompletionSource<bool>(); Task.Run(() => { Thread.Sleep(1000); tcs.SetResult(true); }); tcs.Task.Wait(); }
Task.Yield 簡單來說就是建立時就已經完成的 Task ,或者說執行時間為0的 Task ,或者說是空任務,也就是在建立時就將 Task 的 IsCompeted 值設定為0。
那 await 一個空任務會怎樣?我們知道在 await 時會釋放當前執行緒,等所 await 的 Task 完成時會從執行緒池中申請新的執行緒繼續執行 await 之後的程式碼,這本來是為了解決非同步操作(比如IO操作)霸佔執行緒實際卻用不到執行緒的問題,而 Task.Yield 卻產生了一個不僅沒有非同步操作而且什麼也不幹的 Task ,不是吃飽了撐著嗎?
今天吃晚飯的時候終於想明白了——吃飽了沒有撐。Task.Yield 產生的空任務僅僅是為 await 做嫁衣,而真正的圖謀是藉助 await 實現執行緒的切換,讓 await 之後的操作重新排隊從執行緒池中申請執行緒繼續執行。這樣做有什麼好處呢?執行緒是非常非常寶貴的資源,千金難買一執行緒,而且有優先順序,提高執行緒利用率的重要手段之一就是及時將執行緒分配給最需要的地方,而最奢侈的之一是讓一個優先順序低執行時間長的操作一直佔用著一個執行緒,await Task.Yield 可以讓你巧妙地藉助 await 的執行緒切換能力,將不太重要的比較耗時的操作放在新的執行緒(重新排隊從執行緒池中申請到的執行緒)中執行。打個比方,很多人排隊在外婆家就餐,你來的時候比較巧,正好有位置,但你本來就不著急肚子也不太餓準備慢慢吃慢慢聊,而排隊的人當中有些人很餓很著急吃完還有事,這時你如果先點幾個招牌菜解解饞,然後將座位讓出來,重新排隊,並且排隊的人當中像你這樣的都這麼做,那些排隊中心急如焚的人真是是幸福感爆棚,外婆家的老闆也笑彎了腰。你讓出座位重新排隊的愛心行為就是await Task.Yield()
。
祝大家程式設計師節快樂!