Async in C# 5.0(C#中的非同步編程Async) 蝸牛翻譯之第五章 ...
寫在前面
在學非同步,有位園友推薦了《async in C#5.0》,沒找到中文版,恰巧也想提高下英文,用我拙劣的英文翻譯一些重要的部分,純屬娛樂,簡單分享,保持學習,謹記謙虛。
如果你覺得這件事兒沒意義翻譯的又差,盡情的踩吧。如果你覺得值得鼓勵,感謝留下你的贊,願愛技術的園友們在今後每一次應該猛烈突破的時候,不選擇知難而退。在每一次應該獨立思考的時候,不選擇隨波逐流,應該全力以赴的時候,不選擇儘力而為,不辜負每一秒存在的意義。
轉載和爬蟲請註明原文鏈接http://www.cnblogs.com/tdws/p/5659003.html,博客園 蝸牛 2016年6月27日。
目錄 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>。(非同步:既然方法鏈中有一個要非同步,那麼就會影響整體都使用非同步)。
寫在最後關於非同步我還有很多疑惑,也是隨著文章逐步明白,我也希望能快一點啊。