15.5.1【Task實現細節】 生成的程式碼
還在嗎?我們開始吧。由於深入講解需上百頁的篇幅,因此這裡我不會講得太深。但我會提 供足夠的背景知識,以有助於你對整個結構的理解。之後可通過閱讀我近些年來撰寫的部落格文章, 來了解更加錯綜複雜的細節,或簡單地編寫一些非同步程式碼並反編譯。同樣地,這裡我只介紹非同步 方法,它包含了所有有趣的機制,並且不需要處理非同步匿名函式所處的間接層。
說明 警告,勇敢的旅行者—— 前方是實現細節! 本節將描述微軟C# 5編譯器(隨著.NET 4.5的釋出而推出)內實現的相關內容。從CTP版到beta版,有些細節變化很大,並且在未 來仍有可能發生改變。但我認為其基本理念並不會發生太大的變動。充分了解本節內容 後,你會發現並不存在什麼魔法,只不過是一些編譯器生成的聰明程式碼罷了。這之後便 可以從容應對未來變化的細節內容了。 正如我之前多次提到過的,它的實現(包括近似實現和真實編譯器生成的程式碼)基本上可以 說是一個狀態機。編譯器將生成一個私有的內嵌結構,來表示這個非同步方法。這個結構還必須包 含一個方法,其簽名與所宣告的方法簽名相同。我稱其為骨架方法,該方法本身沒有多少內容, 但其他東西都依賴於它。
骨架方法需要建立狀態機,並執行一個步驟(此處的步驟指執行第一個 await 表示式之前的 程式碼),然後返回一個表示狀態機進度的任務。(別忘了,在第一次到達真正需要等待的 await 表 達式之前,執行過程是同步的。)此後,骨架方法的運作就此結束。狀態機會負責其餘事項,後 續操作附加到其他非同步操作後,可通知狀態機去執行另一個步驟。當之前返回的任務被賦予適當 的值後,方法就執行到最後了, 狀態機可隨即發出訊號。
當然,“執行方法體中的程式碼”這一步,只有在骨架方法中第一次呼叫時,才會從方法的開 頭執行。以後每次到達該塊,都是由後續操作從之前中斷的地方開始繼續執行。 現在有兩個概念需要關注,即骨架方法和狀態機。在本節的剩餘篇幅中,我將使用單個非同步 方法作為示例,如程式碼清單15-11所示。
1 static async Task<int> SumCharactersAsync(IEnumerable<char> text) 2 { 3 int total = 0; 4 foreach (char ch in text) 5 { 6 int unicpde = ch; 7 await Task.Delay(unicpde); 8 total += unicpde;9 } 10 await Task.Yield(); 11 return total; 12 }
程式碼清單15-11沒有什麼實際意義,但我們只關注流控制。在開始之前,有必要指出以下幾點。
該方法包含一個引數( text )。
該方法包含一個迴圈,後續操作執行時需跳回該迴圈內。
該方法包含兩個不同型別的 await 表示式: Task.Delay 返回一個 Task ,而 Task.Yield()則返回一個 YieldAwaitable 。
該方法包含顯式的區域性變數( total 、 ch 和 unicode ),需在不同的呼叫間關注其變化。
該方法包含一個通過呼叫 text.GetEnumerator() 方法建立的隱式區域性變數。
該方法最終返回一個值。
這段程式碼最初的版本將 text 作為 string 型別的引數,但C#編譯器會對字串的迭代進行優
化,並使用 Length 屬性和索引器,這會使反編譯後的程式碼變得更加複雜。