早上無意中看到了async和await關鍵字,花了十幾分鍾看了一下msdn,大概明白了是什么一個東西,和大家分享一下。
await關鍵字的中文是期待的意思。在我們編程中想表達“我待會期待這里會有一個值,但我不是現在就要,我先去做其他事情,你完成的時候告訴我”。其實異步模式非常符合現實中場景,現實生活中還真的很少東西是同步的。等車的時候沒事干可以拿手機出來玩一下,首發下郵件,而不是直愣愣的干在那里等着車過來。
話說回來,在C# 5中借助await可以更好的輔助我們進行異步編程模式的開發,通過改變執行流程,使得異步執行看起來更像同步執行。
一個async方法里通常包含一個或多個的對應的await操作符,但如果沒有 await表達式也不會導致編譯錯誤。但如果調用一個async方 法,卻不使用await關鍵字來標記一個掛起點的話,程序將會忽略async關鍵字並以同步的方式執行。編譯器會對類似的問題發出警告。
我直接拿msdn里的代碼和圖來舉例吧。
public partial class MainWindow : Window { private async void StartButton_Click(object sender, RoutedEventArgs e) { int contentLength = await AccessTheWebAsync(); resultsTextBox.Text += String.Format("\r\nLength of the downloaded string: {0}.\r\n", contentLength); } async Task<int> AccessTheWebAsync() { HttpClient client = new HttpClient(); Task<string> getStringTask = client.GetStringAsync("http://msdn.microsoft.com"); DoIndependentWork(); string urlContents = await getStringTask; return urlContents.Length; } void DoIndependentWork() { resultsTextBox.Text += "Working . . . . . . .\r\n"; } }
可以從圖中看出,和正常的執行流程是不一樣的,我們甚至還會回到“看似已經執行完的方法中”,接着await下面執行
但其實這也不難理解,如果你用過yield return的話。可以看做執行到await的時候(箭頭第6步),方法就返回了。等await后面的動作執行完以后我們再回來繼續執行下面的步驟。
也就是說,await 表達式標記一個點,在該點上直到等待的異步操作完成方法才能繼續。 同時,方法掛起,並且返回到方法的調用方。
值得注意的是,返回並不意味着方法已經執行完成,也就是方法還沒退出。await 表達式中異步方法的掛起不能使該方法退出,並且 finally 塊不會運行。
但天有不測風雲,壯士有可能一去不復返,改一下StartButton_Click代碼,看下面的這個情況
1 private void StartButton_Click(object sender, RoutedEventArgs e) 2 { 3 int contentLength = AccessTheWebAsync().Result; 4 5 resultsTextBox.Text += 6 String.Format("\r\nLength of the downloaded string: {0}.\r\n", contentLength); 7 }
我們調用了task的Result,這個屬性內部會調用await的方法,也就是會阻塞線程,也就是說線程執行到第3行的時候,就會被阻塞了(阻塞線程是罪大惡極的事情,要牢記在心)。一直等待AccessTheWebAsync()方法完成。
但是AccessTheWebAsync()能完成嗎?答案是不能!因為AccessTheWebAsync方法需要依賴這個已經被阻塞的線程回去完成。好吧,一個互相等待的死鎖出現了。
你媽媽喊你回家吃飯,快點來接着執行await下面的代碼
我靠,我被Result定住了,沒辦法回去了。
。。。。。。。。
不指定SynchronizationContext
要解決這個問題,就要知道為什么會產生這種情況。
要產生這種情況需要滿足兩個條件,
1.我指定了要接下來執行下面代碼的SynchronizationContext
2.SynchronizationContext里面包含的所有線程被阻塞
SynchronizationContext是什么東西呢?簡單來說就是.NET提供的一個類,能讓任務在指定的線程里面運行。詳細的話看我的這篇多線程之旅七——GUI線程模型,消息的投遞(post)與處理(IOS開發前傳)
一些SynchronizationContext只封裝了單獨的一個線程,比如UI thead, 還有一些封裝的是一組線程,比如說線程池,可以挑選任意一個線程池的線程來完成指定的任務。
所以說為啥這里悲劇了?因為UI thread所在的SynchronizationContexts只有它這么一個獨苗,一旦它被阻塞,就沒轍了。
所以解決這種問題的方式有這些,一個就是在await 指定的時候,選擇”哥不care你那個線程回來執行,反正來線程啊!“
1 async Task<int> AccessTheWebAsync() 2 { 3 4 HttpClient client = new HttpClient(); 5 6 Task<string> getStringTask = client.GetStringAsync("http://msdn.microsoft.com"); 7 8 DoIndependentWork(); 9 10 string urlContents = await getStringTask.ConfigureAwait(false)
; 11 return urlContents.Length; 12 }
第10行新加的.ConfigureAwait(false) 表達的就是這個意思,也就是我不在乎你接下來執行的線程是什么線程,來自哪個SynchronizationContext。默認是true,也就是我指定就要原來的那個SynchronizationContext,其他的寧可死也不要。
第二種方式就是讓它原來的SynchronizationContext本身就封裝了多個線程,這樣即使阻塞了一個線程,也可以調用其他線程來完成任務。
int contentLength = Task.run(AccessTheWebAsync()).Result;
覺得這樣寫會改變了原來的調用的話,也可以把這層封裝在AccessTheWebAsync方法里,這樣外部調用的方式可以保持不變。
async Task<int> AccessTheWebAsync() { return Task.Run(async ()=> {
HttpClient client = new HttpClient(); Task<string> getStringTask = client.GetStringAsync("http://msdn.microsoft.com"); DoIndependentWork(); string urlContents = await getStringTask; return urlContents.Length; }) }
好吧,如果你十分鍾沒看懂的話,那咱們來看看好玩的yield return,再回來看看這個await。
為什么yield return返回的是IEnumerable<T>類型?就是因為IEnumerable表達的意思是“我只關心你當前的和下一個,我不管你總共有多少”,就和數學歸納法一樣,有第一個(n=1),有下一個(n+1),就能推斷出所有情況。所以當我們foreach遍歷一個IEnumerable類型的時候,無論是在foreach之前就計算好所有元素,還是調用一次MoveNext時才計算並返回一個,都符合這個意思。如果我們的輸入序列和輸出序列都是用yield return來實現的,那么就相當於構造了一條延遲執行的pipeline。恩,沒錯,和Linq的實現一樣的道理。
C#編譯器是通過一個狀態機來實現的,每次調用MoveNext方法return 出去之前,都修改當前的狀態,使得下次進入的時候是從其他狀態進入。
class Foo { public IEnumerable<string> AnIterator() { yield return "1"; yield return "2"; yield return "3"; } } class Program { static void Main() { var collection = new Foo(); foreach (var s in collection.AnIterator()) { Console.WriteLine(s); } } }
通過reflactor查看編譯器幫我們做的事情,我們每寫一個yield return就對應產生一個分支的case
private bool MoveNext() { switch (this.<>1__state) { case 0: this.<>1__state = -1; this.<>2__current = "1"; this.<>1__state = 1; //修改當前狀態為1 return true; case 1: this.<>1__state = -1; this.<>2__current = "2"; this.<>1__state = 2; //修改當前狀態為2 return true; case 2: this.<>1__state = -1; this.<>2__current = "3"; this.<>1__state = 3; //修改當前狀態為3 return true; case 3: this.<>1__state = -1; break; } return false; }
其實Jeffrey Richter以前就借助過yield return實現的狀態機來簡化異步編程,使得它看起來像同步編程。
下面的代碼yield return 1看起來是不是很像是await?
private static IEnumerator<Int32> PipeServerAsyncEnumerator(AsyncEnumerator ae) { // Each server object performs asynchronous operations on this pipe using (var pipe = new NamedPipeServerStream( "Echo", PipeDirection.InOut, -1, PipeTransmissionMode.Message, PipeOptions.Asynchronous | PipeOptions.WriteThrough)) { // Asynchronously accept a client connection pipe.BeginWaitForConnection(ae.End(), null); yield return 1;
// A client connected, let's accept another client var aeNewClient = new AsyncEnumerator(); aeNewClient.BeginExecute(PipeServerAsyncEnumerator(aeNewClient), aeNewClient.EndExecute); // Accept the client connection pipe.EndWaitForConnection(ae.DequeueAsyncResult()); // Asynchronously read a request from the client Byte[] data = new Byte[1000]; pipe.BeginRead(data, 0, data.Length, ae.End(), null); yield return 1;
// The client sent us a request, process it. Int32 bytesRead = pipe.EndRead(ae.DequeueAsyncResult()); // My sample server just changes all the characters to uppercase // But, you can replace this code with any compute-bound operation data = Encoding.UTF8.GetBytes( Encoding.UTF8.GetString(data, 0, bytesRead).ToUpper().ToCharArray()); // Asynchronously send the response back to the client pipe.BeginWrite(data, 0, data.Length, ae.End(), null);
yield return 1; // The response was sent to the client, close our side of the connection pipe.EndWrite(ae.DequeueAsyncResult()); } // Close happens in a finally block now! }
終於,Jeffrey Richter在C#5里面終於用關鍵字await更好的實現了他的想法(他去年來上海宣講的時候說await是他創建的)
有興趣的話可以看看await創建的狀態機長啥樣干了什么
一段這樣的代碼,
private static async Task<String> MyMethodAsync(Int32 argument) { Int32 local = argument; try { Type1 result1 = await Method1Async(); for (Int32 x = 0; x < 3; x++) { Type2 result2 = await Method2Async(); } } catch (Exception) { Console.WriteLine("Catch"); } finally { Console.WriteLine("Finally"); } return "Done"; }
Type1 result1 = await Method1Async(); 經過編譯以后,會成這樣
// This is the state machine method itself void IAsyncStateMachine.MoveNext() { String result = null; // Task's result value // Compilerinserted try block ensures the state machine’s task completes try { // Assume we're logically leaving the 'try' block // If 1st time in state machine method, // execute start of original method Boolean executeFinally = true; if (m_state == 1) { m_local = m_argument; } // Try block that we had in our original code try { TaskAwaiter<Type1> awaiterType1; TaskAwaiter<Type2> awaiterType2; switch (m_state) { case 1: // Start execution of code in 'try' // Call Method1Async and get its awaiter awaiterType1 = Method1Async().GetAwaiter(); if (!awaiterType1.IsCompleted) { m_state = 0; // 'Method1Async' is completing asynchronously m_awaiterType1 = awaiterType1; // Save the awaiter for when we come back
// Tell awaiter to call MoveNext when operation completes m_builder.AwaitUnsafeOnCompleted(ref awaiterType1, ref this); // The line above invokes awaiterType1's OnCompleted which approximately calls ContinueWith(t => MoveNext()) on the Task being awaited. executeFinally = false; // We're not logically leaving the 'try' block return; // Thread returns to caller } // 'Method1Async' completed synchronously break; case 0: // 'Method1Async' completed asynchronously awaiterType1 = m_awaiterType1; // Restore mostrecent awaiter break; case 1: // 'Method2Async' completed asynchronously awaiterType2 = m_awaiterType2; // Restore mostrecent awaiter goto ForLoopEpilog; } //下面省略的是loop部分和異常處理部分,分開看比較容易看懂
..... } m_builder.SetResult(result); }
恩,實現咋看上去有點復雜,不過注釋也已經寫得挺明白了,基本上關鍵就是。
m_builder.AwaitUnsafeOnCompleted(ref awaiterType1, ref this) invokes awaiterType1's OnCompleted which approximately calls ContinueWith(t => MoveNext()) on the Task being awaited.
然后就return出去了,接着在task完成之后再跳轉回來。基本原理和上面的類似。
這里是loop部分的代碼
// After the first await, we capture the result & start the 'for' loop m_resultType1 = awaiterType1.GetResult(); // Get awaiter's result ForLoopPrologue: m_x = 0; // 'for' loop initialization goto ForLoopBody; // Skip to 'for' loop body ForLoopEpilog: m_resultType2 = awaiterType2.GetResult(); m_x++; // Increment x after each loop iteration // Fall into the 'for' loop’s body ForLoopBody: if (m_x < 3) { // 'for' loop test // Call Method2Async and get its awaiter awaiterType2 = Method2Async().GetAwaiter(); if (!awaiterType2.IsCompleted) { m_state = 1; // 'Method2Async' is completing asynchronously m_awaiterType2 = awaiterType2; // Save the awaiter for when we come back // Tell awaiter to call MoveNext when operation completes m_builder.AwaitUnsafeOnCompleted(ref awaiterType2, ref this); executeFinally = false; // We're not logically leaving the 'try' block return; // Thread returns to caller } // 'Method2Async' completed synchronously goto ForLoopEpilog; // Completed synchronously, loop around }
完整的代碼

void IAsyncStateMachine.MoveNext() { String result = null; try { Boolean executeFinally = true; if (m_state == 1) { m_local = m_argument; } try { TaskAwaiter<Type1> awaiterType1; TaskAwaiter<Type2> awaiterType2; switch (m_state) { case 1: awaiterType1 = Method1Async().GetAwaiter(); if (!awaiterType1.IsCompleted) { m_state = 0; m_awaiterType1 = awaiterType1; m_builder.AwaitUnsafeOnCompleted(ref awaiterType1, ref this); executeFinally = false; return; } break; case 0: awaiterType1 = m_awaiterType1; break; case 1: awaiterType2 = m_awaiterType2; goto ForLoopEpilog; } m_resultType1 = awaiterType1.GetResult(); ForLoopPrologue: m_x = 0; goto ForLoopBody; ForLoopEpilog: m_resultType2 = awaiterType2.GetResult(); m_x++; ForLoopBody: if (m_x < 3) { awaiterType2 = Method2Async().GetAwaiter(); if (!awaiterType2.IsCompleted) { m_state = 1; m_awaiterType2 = awaiterType2; m_builder.AwaitUnsafeOnCompleted(ref awaiterType2, ref this); executeFinally = false; return; } goto ForLoopEpilog; } } catch (Exception) { Console.WriteLine("Catch"); } finally { if (executeFinally) { Console.WriteLine("Finally"); } } result = "Done"; } catch (Exception exception) { m_builder.SetException(exception); return; } m_builder.SetResult(result); }
好吧,我承認十分鍾學會有點標題黨,但是看到這里,我想大家基本都有了個印象吧,目的達到了。