異步編程系列第05章 Await究竟做了什么?


寫在前面

  在學異步,有位園友推薦了《async in C#5.0》,沒找到中文版,恰巧也想提高下英文,用我拙劣的英文翻譯一些重要的部分,純屬娛樂,簡單分享,保持學習,謹記謙虛。

  如果你覺得這件事兒沒意義翻譯的又差,盡情的踩吧。如果你覺得值得鼓勵,感謝留下你的贊,願愛技術的園友們在今后每一次應該猛烈突破的時候,不選擇知難而退。在每一次應該獨立思考的時候,不選擇隨波逐流,應該全力以赴的時候,不選擇盡力而為,不辜負每一秒存在的意義。

   轉載和爬蟲請注明原文鏈接http://www.cnblogs.com/tdws/p/5659003.html,博客園 蝸牛 2016年6月27日。

目錄

第01章 異步編程介紹

第02章 為什么使用異步編程

第03章 手動編寫異步代碼

第04章 編寫Async方法

第05章 Await究竟做了什么

第06章 以Task為基礎的異步模式

第07章 異步代碼的一些工具

第08章 哪個線程在運行你的代碼

第09章 異步編程中的異常

第10章 並行使用異步編程

第11章 單元測試你的異步代碼

第12章 ASP.NET應用中的異步編程

第13章 WinRT應用中的異步編程

第14章 編譯器在底層為你的異步做了什么

第15章 異步代碼的性能

await究竟做了什么?

  我們有兩種角度來看待C#5.0的async功能特性,尤其是await關鍵字上發生了什么:

  ·作為一個語言的功能特性,他是一個供你學習的已經定義好的行為

  ·作為一個在編譯時的轉換,這是一個C#語法糖,為了簡略之前復雜的異步代碼

  這都是真的;它們就像同一枚硬幣的兩面。在本章,我們將會集中在第一點上來探討異步。在第十四章我們將會從另一個角度來探討,即更復雜的,但是提供了一些細節使debug和性能考慮更加清晰。

休眠和喚醒一個方法

   當你的程序執行遇到await關鍵字時,我們想要發生兩件事:

   ·為了使你的代碼異步,當前執行你代碼的線程應該被釋放。這意味着,在普通,同步的角度來看,你的方法應該返回。

   ·當你await的Task完成時,你的方法應該從之前的位置繼續,就像它沒在早些時候被返回。

  為了做到這個行為,你的方法必須在遇到await時暫停,然后在將來的某個時刻恢復執行。

  我把這個過程當做一個休眠一台計算機的小規模情況來看(S4 sleep)。這個方法當前的狀態會被存儲起來(譯者:狀態存儲起來,正如我們第二章廚房那個例子,廚師會把已放在烤箱中的食物的烹飪狀態以標簽的形式貼在上面),並且這個方法完全退出(廚師走了,可能去做其他事情了)。當一台計算機休眠,計算機的動態數據和運行數據被保存到磁盤,並且變得完全關閉。下面這段話和計算機休眠大概一個道理,一個正在await的方法除了用一點內存,不使用其他資源,那么可以看作這個正執行的線程已經被釋放。

       進一步采取類似上一段的類比:一個阻塞型方法更像你暫停一台計算機(S3 sleep),它雖然使用較少的資源,但從根本上來講它一直在運行着。

  在理想的情況下,我們希望編程者察覺不到這里的休眠。盡管實際上休眠和喚醒一個方法的中期執行是很復雜的,C#也將會確保你的代碼被喚醒,就像什么都沒發生一樣。(譯者:不得不贊嘆微軟對語法糖的封裝和處理)。

方法的 狀態

  為了准確的弄清楚在你使用await時C#到底為我們做了多少事情,我想列出所有關於方法狀態的所有我們記住和了解的細節。

  首先,你方法中本地的變量的值會被記住,包括以下值:

  ·你方法的參數

  ·在本范圍內所有你定義的變量

  ·其他變量包括循環數

  ·如果你的方法非靜態,那么包括this變量。這樣,你類的成員變量在方法喚醒時都是可用的。

  他們都被存在.NET 垃圾回收堆(GC堆)的一個對象上。因此當你使用await時,一個消耗一些資源的對象將會被分配,但是在大多數情況下不用擔心性能問題。

  C#也會記住在方法的什么位置會執行到await。這可以使用數字存儲起來,用來表示await關鍵字在當前方法的位置。

  在關於如何使用await關鍵字沒有什么特別的限制,例如,他們可以被用在一個長表達式上,可能包含不止一個await:

int myNum = await AlexsMethodAsync(await myTask, await StuffAsync());

  為了去記住剩余部分的表達式的狀態在await某些東西時,增加了額外的條件。比如,當我們運行await StuffAsync()時,await myTask的結果需要被記住。.NET中間語言(IL)在棧上存儲這種子類表達式,因此 ,這個棧就是我們await關鍵字需要存儲的。

  最重要的是,當程序執行到第一個await關鍵字時,方法便返回了(譯者:關於方法在遇到await時返回,建議讀者從第一章拆分的兩個方法來理解)。如果它不是一個async void方法,一個Task在這個時刻被返回,因此調用者可以等待我們以某種方式完成。C#也必須存儲一種操作返回的Task的方式,這樣當你的方法完成,這個Task也變得completed,並且執行者也可以返回到方法的異步鏈當中。確切的機制將會在第十四章中介紹。

上下文

  作為一個使await的過程盡量透明的部分,C#捕捉各種上下文在遇到await時,然后在恢復方法使將其恢復。

  在所有事情中最重要的還是同步上下文(synchronization context),即可以被用於恢復方法在一個特殊類型的線程上。這對於UI app尤其重要,就是那種只能在正確的線程上操作UI的(就是winform wpf之類的)。同步上下文是一個復雜的話題,第八章將會詳細解釋。

  其他類型的上下文也會被從當前調用的線程捕捉。他們的控制是通過一個相同名稱的類來實現的,所以我將列出一些重要的上下文類型:

  ExecutionContext

  這是父級上下文,所有其他上下文都是它的一部分。這是.NET的系統功能,如Task使用其捕捉和傳播上下文,但是它本身不包含什么行為。

  SecurityContext

  這是我們發現並找到通常被限制在當前線程的安全信息的地方。如果你的代碼需要運行在特定的用戶,你也許會,模擬或者扮演這個用戶,或者ASP.NET將會幫你實現扮演。在這種情況下,模擬信息會存在SecurityContext。

  CallContext(這個東西耳熟能詳吧,相信用過EF的都知道)

  這允許編程者存儲他們在邏輯線程的生命周期中一直可用的數據。即使考慮到在很多情況下有不好的表現,它仍然可以避免程序中方法的參數傳來傳去。(譯者:因為你存到callcontext里,隨時都可以獲取呀,不用通過傳參數傳來傳去了)。LogicalCallContextis是一個相關的可以跨用應用程序域的。

       值得注意的是線程本地存儲(TLS),它和CallContext的目標相似,但它在異步的情況下是不工作的,因為在一個耗時操作中,線程被釋放掉了,並且可能被用於處理其他事情了。你的方法也許被喚醒並執行在一個不同的線程上。

  C#將會在你方法恢復(resume,這里就是單純的“恢復”)的時候恢復(restore,我覺得這里指從內存中恢復)這些類型的上下文。恢復上下文將產生一些開銷,比如,一個程序在使用模擬(之前的模擬身份之類的)的時候並大量使用async將會變得更慢一些。我建議必變.NET創建上下文的功能,除非你認為這真的有必要。

await能用在哪兒?

  await可以用在任何標記async的方法和和方法內大部分的地方,但是有一些地方你不能用await。我將解釋為什么在某些情況下不允許await。

catch和finally塊

  雖然在try塊中使用await是完全允許的,但是他不允許在catch和finally塊中使用。通常在catch和finall塊中,異常依然在堆棧中未解決的狀態,並且之后將會被拋出。如果await在這個時刻前使用,棧將會有所不同,並且拋出異常的行為將會變得難以定義。

  請記住替代在catch塊中使用block的方法是在其后面,通過返回一個布爾值來記錄操作是否拋出一個異常。示例如下:

try
{
   page = await webClient.DownloadStringTaskAsync("http://oreilly.com");
}
catch (WebException)
{
   page = await webClient.DownloadStringTaskAsync("http://oreillymirror.com");
}

   你可以以如下方式替代:

bool failed = false;
try
{
   page = await webClient.DownloadStringTaskAsync("http://oreilly.com");
}
catch (WebException)
{
   failed = true;
}
if (failed)
{
   page = await webClient.DownloadStringTaskAsync("http://oreillymirror.com");
}

  lock塊

  lock是一種幫助編程人員防止其它線程和當前線程訪問相同對象的方式。因為異步代碼通常會釋放開始執行異步的線程,並且會被回調並且發生回調在一個不確定的時間量之后,即被釋放掉后和開始的線程不同(譯者:即使相同的線程,它也是釋放掉之后的了),所以在await上加鎖沒有任何意義。

   在一些情況下,保護你的對象不被並發訪問是很重要的,但是在沒有其他線程在await期間來訪問你的對象,使用鎖是沒有必要的。在這些情況下,你的操作是有些冗余的,顯式地鎖定了兩次,如下:

lock (sync)
{
    // Prepare for async operation
}
    int myNum = await AlexsMethodAsync();
lock (sync)
{
    // Use result of async operation
}

  另外,你可以使用一個類庫來進行處理並發控制,比如NAct,我們將會在第十章介紹

  如果你不夠幸運,你可能需要在執行異步操作時保持某種鎖。這時,你就需要苦思冥想並小心謹慎,因為通常鎖住異步調用資源,而不造成爭用和死鎖是非常困難的。也許遇到這種情況想其他辦法或者重構你的程序是最好的選擇。

  Linq Query表達式

  C#有一種語法幫助我們更加容易的去通過書寫querys來達到過濾,排序,分組等目的。這些query可以被執行在.NET平台上或者轉換成數據庫操作甚至其他數據源操作。

IEnumerable<int> transformed = from x in alexsInts
where x != 9
select x + 2;

  C#是在大多數位置是不允許在Query表達式中使用await關鍵字的。是因為這些位置會被編譯成lambda表達式,正因為如此,該lambda表達式需要標記為async關鍵字。只是這樣含蓄的lambda表達式不存在,即使如果真的這樣做也會讓人confuse。

  我們還是有辦法,你可以寫當量的表達式,通過使用Linq內部帶的拓展方法。然后lambda表達式變得明了可讀,繼而你也就可以標記他們為async,從而使用await了。(譯者:請對照上下代碼來閱讀)

IEnumerable<Task<int>> tasks = alexsInts
.Where(x => x != 9)
.Select(async x => await DoSomthingAsync(x) + await DoSomthingElseAsync(x));
IEnumerable<int> transformed = await Task.WhenAll(tasks);

  為了收集結果,我使用了Task.WhenAll,這是為Task集合所工作的工具,我將會在第七章介紹細節。

  不安全(unsafe)的代碼

  代碼被標記為unsafe的不能包含await,非安全的代碼應該做到非常罕見並且應該保持方法獨用和不需要異步。反正在編譯器對await做轉換的時候也會跳出unsafe代碼。(譯者:我覺得其實這里不用太在意啦,反正沒寫過unsafe關鍵字的代碼)

捕獲異常

  異步方法的異常捕獲被微軟設計的盡量和我們正常同步代碼一樣的。然而異步的復雜性意味着他們之間還會有些細微差別。在這里我將介紹異步如何簡單的處理異常,我也將在第九章詳細講解注意事項。

  當耗時操作結束時,Task類型會有一個概念來表明成功還是失敗。最簡單的就是由IsFaulted屬性來向外暴露,在執行過程中發生異常它的值就是true。await關鍵字將會察覺到這一點並且會拋出Task中包含的異常。

            如果你熟悉.NET異常機制,用也許會擔心異常的堆棧跟蹤在拋出異常時如何正確的保存。這在過去也許是不可能的。然而在.NET4.5中,這個限制被修改掉了,通過一個叫做ExceptionDispatchInfo的類,即一個協作異常的捕捉,拋出和正確的堆棧跟蹤的類。

  異步方法也能察覺到異常。在執行異步方法期間發生任何異常,都不會被捕捉,他們會隨着Task的返回而返回給調用者。當發生這種情況時,如果調用者在await這個Task,那么異常將會在此處拋出。(譯者:之前有講到異常在異步中會被傳遞)。在這種方式下,異常通過調用者傳播,會形成一個虛擬的堆棧跟蹤,完全就像它發生在同步代碼中一樣。

            我把它乘坐虛擬堆棧跟蹤,因為堆棧是一個單線程擁有的這樣的概念,並且在異步代碼中,當前線程實際的堆棧和產生異常那個線程的堆棧可能是非常不同的。異常捕捉的是用戶意圖中的堆棧跟蹤,而不是C#如何選擇執行這些方法的細節。

直到被需要前異步方法都是同步的

  我之前說的,使用await只能消費(調用)異步方法。直到await結果發生,這個調用方法的語句在調用他們的線程中運行,就像同步方法一樣。這非常具有現實意義,尤其是以一個同步的過程完成所有異步方法鏈時。(譯者:當使用await的時候,的確就是按照同步的順序來執行)

  還記得之前異步方法暫停在第一次遇到await時。即使這樣,它有時候也不需要暫停,因為有時await的Task已經完成了。一個Task已經被完成的情況如下:

   ·他是被創建完成的,通過Task.FromResult工具方法。我們將會在第七章詳細探討。

   ·由沒遇到async的async方法返回。

   ·它運行一個真正的異步操作,但是現在已經完成了(很可能是由於當前線程在遇到await之前已經做了某些事情)。

   ·它被一個遇到await的asunc方法返回,但是所await的這個之前就已經完成了。

  由於最后一個可能性,一些有趣的事情發生在你await一個已經完成的Task,很可能是在一個深度的異步方法鏈中。整個鏈很像完全同步的。這是因為在異步方法鏈中,第一個await被調用的方法總是異步鏈最深的一個。其他的方法到達后,最深的方法才有機會返回。( The others are only reached after the deepest method has had a chance to return synchronously.譯者:按照語法來講我的這句話貌似翻譯的不正確,但是我個人覺得實際情況就是我說的這個樣子。在遇到第一個await后,后面異步方法鏈中的await依次執行,逐個返回,最后才返回結果到最深的方法,也就是第一個方法,有高人來提出這里的見解嗎?)

   你也許會懷疑為什么在第一種或第二種情況下還使用async。如果這些方法承諾一直同步的返回,你是正確的,並且這樣寫同步的代碼效率高於異步並且沒有await的過程。然后,這只是方法同步返回的情況。比如,一個方法緩存其結果到內存中,並在緩存可用的時候,結果可以被同步地返回,但是當它需要異步的網絡請求。當你知道有一個好機會讓你使用異步方法,在某種程度上你也許還想要方法返回Task或者Task<T>。(異步:既然方法鏈中有一個要異步,那么就會影響整體都使用異步)。

寫在最后

  關於異步我還有很多疑惑,也是隨着文章逐步明白,我也希望能快一點啊。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM