要了解這篇文章,首先,您需要了解甚麼是 狀態機。
底下為該WPF上 MainWindows 的類別定義,在這個類別定義中,我們定義了一個按鈕的事件,當使用者按下了這個按鈕,會呼叫我們自訂的方法,透過 Async/Await 的非同步方式,取得特定網址的網頁,該非同步方法會回傳所取得網頁的字串到按鈕事件內,接著,在按鈕事件內會將這個字串從 Console內列印出來。
在非同步方法中 GetStringAsync中,我們使用了 HttpClient 物件,呼叫了 GetStringAsync 非同步方法,取得了微軟首頁的內容。
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private async void btnDownload_Click(object sender, RoutedEventArgs e) { string fooStr = await GetStringAsync(); Console.WriteLine(fooStr); } public async Task<string> GetStringAsync() { HttpClient client = new HttpClient(); var result = await client.GetStringAsync("http://www.microsoft.com"); return result; } }
接著,我們來看看,對於我們經常使用的 Async / Await 非同步呼叫,編譯器究竟做了甚麼處理,可以讓我們在寫非同步方法的時候,既然可以像使用同步方式來寫程式碼,變得這麼方便、好用。
我們先來看看按鈕事件的方法:
private async void btnDownload_Click(object sender, RoutedEventArgs e) { string fooStr = await GetStringAsync(); Console.WriteLine(fooStr); }
底下為編譯器處理過的上述方法,我們從底下程式碼中,看不到原來事件方法的任何程式碼了,例如: Console.WriteLine 。
我已經將編譯器產生的按鈕事件方法程式碼加上許多註解,您可以參考這些程式碼說明;其實,編譯器把原先該按鈕內的所有程式碼,都搬到另外一個新產生的類別中,這個類別其實就是一個狀態機的運作狀態。
底下的方法內容其實相當的簡單,它產生了一個狀態機物件,接著,對此狀態機進行初始化,並且設定這個狀態機的初始狀態值為 -1 (狀態機的型別為 int 整數),最後,呼叫 Start 方法,開始進行狀態機的運作。
狀態機的將會在第一行內產生 MainWindow.<btnDownload_Click>d__1 <btnDownload_Click>d__ = new MainWindow.<btnDownload_Click>d__1();
接著,針對這個狀態機變數 <btnDownload_Click>d__ 進行初始化。
// 這裡重新改寫了 btnDownload_Click 方法 // AsyncStateMachine 請參考 https://msdn.microsoft.com/zh-tw/library/system.runtime.compilerservices.asyncstatemachineattribute(v=vs.110).aspx [DebuggerStepThrough, AsyncStateMachine(typeof(MainWindow.<btnDownload_Click>d__1))]private void btnDownload_Click(object sender, RoutedEventArgs e) { // 類別 <GetStringAsync>d__1 為編譯器產生的一個類別,其中,是運用狀態機 State Machine 觀念來處理非同步呼叫運作 MainWindow.<btnDownload_Click>d__1 <btnDownload_Click>d__ = new MainWindow.<btnDownload_Click>d__1(); // 底下的程式碼為進行該非同步呼叫的 State Machine的各種預設值初始化 // // 呼叫非同步呼叫時候的物件本身 < btnDownload_Click >d__.<>4__this = this; // 設定該事件的 sender 物件 <btnDownload_Click>d__.sender = sender; // 設定該事件的 RoutedEventArgs 物件 <btnDownload_Click>d__.e = e; // 建立 AsyncTaskMethodBuilder 類別的執行個體 < btnDownload_Click >d__.<>t__builder = AsyncVoidMethodBuilder.Create(); // 該 State Machine 最初狀態值為 -1 <btnDownload_Click>d__.<>1__state = -1; // 表示非同步方法產生器,不會傳回值。 // https://msdn.microsoft.com/zh-tw/library/system.runtime.compilerservices.asyncvoidmethodbuilder(v=vs.110).aspx AsyncVoidMethodBuilder<>t__builder = <btnDownload_Click>d__.<>t__builder; // 開始執行具有相關聯狀態機器的產生器 <>t__builder.Start<MainWindow.<btnDownload_Click>d__1>(ref <btnDownload_Click>d__); }
原先按鈕事件內的方法都搬到了編譯器產生的狀態機類別內,也就是這個 <btnDownload_Click>d__1 類別,這個類別是繼承了 IAsyncStateMachine 這個類別,表示針對非同步方法所產生的狀態機器。 這個型別僅供編譯器使用;該類別程式碼如下所示:
這個編譯器產生的狀態機類別相當的長,不過,我也儘可能的在程式碼內加入註解說明。
這個類別所做的事情,我們簡單說明一下:狀態機最初的狀態值為 -1,一旦狀態機開始運作的時候(也就上面程式碼呼叫了 <>t__builder.Start 方法),就會開始進入狀態機的處理週期,也就是 MoveNext 這個方法;每次狀態機的狀態值有變更的時候,就會再次呼叫這個方法,進入到不同處理階段。
因為最初狀態值為 -1,所以,就會執行底下黃底內的 if 內程式碼,而這段程式碼也只會執行一次,之後就不會再進來了。在 if 內的程式碼,會呼叫我們自己寫的非同步方法 GetStringAsync(),而後會取得 等候非同步工作完成的物件 taskAwaiter,因為該非同步工作尚未完成,所以,就會將狀態機的狀態值變更為 0,接著透過 AwaitUnsafeOnCompleted 方法設定當工作完成後,繼續回到狀態機內繼續持行 MoveNext 方法。
當非同步工作完成之後,就會將非同步執行結果取出來,並且列應到 Console 內,也就是下面橘色底的程式碼,會執行這段程式法,是因為狀態機的值已經成為 0了。
// 這是編譯器產生出來的類別,用來處理非同步需求的狀態機 [CompilerGenerated] private sealed class <btnDownload_Click>d__1 : IAsyncStateMachine { // 該 State Machine 狀態值 public int <>1__state; // 建立 AsyncTaskMethodBuilder 類別的執行個體 public AsyncVoidMethodBuilder<> t__builder; // 該事件的 sender 物件 public object sender; // 該事件的 RoutedEventArgs 物件 public RoutedEventArgs e; // 呼叫非同步呼叫時候的物件本身 public MainWindow<>4__this; // 原先事件方法中,定義的本地變數 fooStr private string <fooStr>5__1; // 用來暫時儲存呼叫非同步方法的時候的字串值 private string <>s__2; // 用來暫時儲存 提供等候非同步工作完成的物件 // https://msdn.microsoft.com/zh-tw/library/system.runtime.compilerservices.taskawaiter(v=vs.110).aspx private TaskAwaiter<string> <>u__1; // 每當狀態機的狀態值有變動的時候,呼叫 MoveNext來執行下一個狀態機要執行的動作 void IAsyncStateMachine.MoveNext() { // 暫時儲存現在狀態機內的狀態值 int num = this.<>1__state; // 當在原先方法內用了 await 關鍵字,編譯器,會加入異常事件捕捉 try { TaskAwaiter<string> taskAwaiter; if (num != 0) { // 一開始進入狀態機,狀態機值為-1,所以,會先進入到這裡,若狀態機值為 0 ,表示此非同步工作已經完成 // 執行 GetStringAsync 方法&取得用來等候這個 Task 的 awaiter,回傳值為 提供等候非同步工作完成的物件 // https://msdn.microsoft.com/zh-tw/library/system.threading.tasks.task.getawaiter(v=vs.110).aspx taskAwaiter = this.<>4__this.GetStringAsync().GetAwaiter(); // 指出非同步工作是否已經完成 // https://msdn.microsoft.com/zh-tw/library/system.runtime.compilerservices.taskawaiter.iscompleted(v=vs.110).aspx if (!taskAwaiter.IsCompleted) { // 非同步工作尚未完成 this.<>1__state = 0; // 設定狀態機狀態值,標明狀態值為 0,下一個週期,就不會進入到這段程式碼了 this.<>u__1 = taskAwaiter; // 用來暫時儲存 提供等候非同步工作完成的物件 MainWindow.<btnDownload_Click>d__1 <btnDownload_Click>d__ = this; // 排程狀態機器以在指定的 awaiter 完成時繼續下一個動作 // 也就是說,當非同步呼叫完成後,會再度回到 MoveNext() 方法重頭執行一次,不過,因為狀態值有變動了,所以,執行結果會不同 this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<string>, MainWindow.<btnDownload_Click>d__1> (ref taskAwaiter, ref <btnDownload_Click>d__); return; } } else { // 因為 狀態機值為 0 ,表示此非同步工作已經完成 // 取得等候非同步工作完成的物件 taskAwaiter = this.<>u__1; // 設定等候非同步工作完成的物件的預設值 this.<>u__1 = default(TaskAwaiter<string>); // 因為非同步工作已經完成,所以,再將狀態機值設為 -1 this.<>1__state = -1; } // -------------------------------------------------------- // 底下的程式碼,為該事件內 await 呼叫後的相關程式碼,也就是說,當完成非同步呼叫之後,會繼續回到原先地方繼續執行 // -------------------------------------------------------- // 取得 等候非同步工作完成的物件 的執行結果 string result = taskAwaiter.GetResult(); taskAwaiter = default(TaskAwaiter<string>); this.<>s__2 = result; this.<fooStr>5__1 = this.<>s__2; this.<>s__2 = null; // 這段程式法為原先非同步呼叫方法內的,並沒做任何改變 Console.WriteLine(this.<fooStr>5__1); } catch (Exception exception) { // await 的非同步工作,可以捕捉異常事件,並且回報給呼叫者 this.<>1__state = -2; this.<>t__builder.SetException(exception); return; } this.<>1__state = -2; // 變更狀態值,表示已經完成所有的非同步工作 // 將方法產生器標記為成功完成。 this.<>t__builder.SetResult(); }
最後,我們來看看我們自己定義的非同步方法,編譯器會把它變成怎麼樣,首先,底下是原先我們在 WPF 內寫的非同步方法程式碼:
這個非同步方法相當的簡單,我們先定義了一個 HttpClient 物件,接著,透過該物件,取得微軟官網首頁內,並且傳回原先呼叫者。
public async Task<string> GetStringAsync() { HttpClient client = new HttpClient(); var result = await client.GetStringAsync("http://www.microsoft.com"); return result; }
底下為編譯器重新將 GetStringAsync 非同步方法進行包裝,並且產生了另外一個狀態機類別,以便處理非同步呼叫的相關動作,如同上面按鈕事件一樣,編譯器一樣會把我們寫的非同步方法的所有程式碼都包裝在新產生的狀態機類別內,將GetStringAsync這個方法改寫成只有進行狀態機初始化與啟動狀態機的動作。
由於這兩個非同步呼叫(一個是由按鈕事件來呼叫,一個是由自訂非同步方法來呼叫)程式碼很類似,因此,這兩個狀態機的內容很類似,您可以參考底下的程式碼註解說明,來了解到這些功能。
在此,特別說明一下,狀態機內的 if (num != 0){ ... } 程式碼 (底下黃底程式碼),也就是當 if 條件成立的時候,要執行該 if 內的程式碼內容,您可以當作,在原先寫的非同步程式碼中,所有在 await ... 這個等候非同步呼救前的所有程式碼,都會搬移到這個地方,這個 if 內的程式碼,透過狀態機的狀態值控制,就僅僅提供狀態機第一次啟動的時候才會執行,而(這個時候狀態機內的狀態值為 -1)後,就不會再執行到這段程式碼(因為,狀態機的值就會為 0了)。因此,我們就看到了,第一次進入到狀態機內,會先建立一個 HttClient 物件。
由於這個非同步方法需要回傳一個字串,因此,底下橘底程式碼,就是在整個非同步工作處理完成之後,通知最上層呼叫這個非同步方法,您可以取得這個非同步方法的傳回結果了。
// 這是編譯器產生出來的類別,用來處理非同步需求的狀態機 [CompilerGenerated]
private sealed class <GetStringAsync>d__2 : IAsyncStateMachine
{
// State Machine 的狀態值,表示處理到哪裡了
public int <>1__state;
// 表示非同步方法產生器,會傳回工作
// https://msdn.microsoft.com/zh-tw/library/system.runtime.compilerservices.asynctaskmethodbuilder(v=vs.110).aspx
public AsyncTaskMethodBuilder<string> <>t__builder;
// 當時呼叫非同步方法的物件
public MainWindow <>4__this;
// 這個非同步方法內用到的 HttpClient 物件
private HttpClient <client>5__1;
// 原先事件方法中,定義的本地變數 result
private string <result>5__2;
// 用來暫時儲存呼叫非同步方法的時候的字串值
private string <>s__3;
// 用來暫時儲存 提供等候非同步工作完成的物件
// https://msdn.microsoft.com/zh-tw/library/system.runtime.compilerservices.taskawaiter(v=vs.110).aspx
private TaskAwaiter<string> <>u__1;
// 每當狀態機的狀態值有變動的時候,呼叫 MoveNext來執行下一個狀態機要執行的動作
void IAsyncStateMachine.MoveNext()
{
// 暫時儲存現在狀態機內的狀態值
int num = this.<>1__state;
string result2;
// 當在原先方法內用了 await 關鍵字,編譯器,會加入異常事件捕捉
try {
TaskAwaiter<string> taskAwaiter;
if (num != 0)
{
// 一開始進入狀態機,狀態機值為-1,所以,會先進入到這裡,若狀態機值為 0 ,表示此非同步工作已經完成
this.<client>5__1 = new HttpClient();
// 執行 GetStringAsync 方法&取得用來等候這個 Task 的 awaiter,回傳值為 提供等候非同步工作完成的物件
taskAwaiter = this.<client>5__1.GetStringAsync("http://www.microsoft.com").GetAwaiter();
// 指出非同步工作是否已經完成
if (!taskAwaiter.IsCompleted)
{
// 非同步工作尚未完成
this.<>1__state = 0; // 設定狀態機狀態值,標明狀態值為 0,下一個週期,就不會進入到這段程式碼了
this.<>u__1 = taskAwaiter; // 用來暫時儲存 提供等候非同步工作完成的物件
MainWindow.<GetStringAsync>d__2 <GetStringAsync>d__ = this;
// 排程狀態機器以在指定的 awaiter 完成時繼續下一個動作
// 也就是說,當非同步呼叫完成後,會再度回到 MoveNext() 方法重頭執行一次,不過,因為狀態值有變動了,所以,執行結果會不同
this.<>t__builder.AwaitUnsafeOnCompleted <TaskAwaiter<string>, MainWindow.<GetStringAsync>d__2> (ref taskAwaiter, ref <GetStringAsync>d__);
return;
}
}
else {
// 因為 狀態機值為 0 ,表示此非同步工作已經完成
// 取得等候非同步工作完成的物件
taskAwaiter = this.<>u__1;
// 設定等候非同步工作完成的物件的預設值
this.<>u__1 = default(TaskAwaiter<string>);
// 因為非同步工作已經完成,所以,再將狀態機值設為 -1
this.<>1__state = -1;
}
// --------------------------------------------------------
// 底下的程式碼,為該事件內 await 呼叫後的相關程式碼,也就是說,當完成非同步呼叫之後,會繼續回到原先地方繼續執行
// --------------------------------------------------------
// 取得 等候非同步工作完成的物件 的執行結果
string result = taskAwaiter.GetResult();
taskAwaiter = default(TaskAwaiter<string>);
this.<>s__3 = result;
this.<result>5__2 = this.<>s__3;
this.<>s__3 = null;
result2 = this.<result>5__2;
}
catch (Exception exception)
{
this.<>1__state = -2;
this.<>t__builder.SetException(exception);
return;
}
this.<>1__state = -2;
this.<>t__builder.SetResult(result2);
}
[DebuggerHidden]
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
}
}