我們怎樣進行異步編程/開發?
現在擴充下上篇文章的類(AsyncTest),提供更多的例子並從中做下簡單的對比, 從新的認識下異步的內部機制,下面我們增加一個新的委托
1步,我們添加一個新方法(計算年薪YearlySalary)
public decimal YearlySalary(decimal salary, int monthCount, decimal bonus);
2步,為這個方法增加異步的功能,這樣我們仍然使用委托(Delegate)
public delegate decimal SalaryEventHandler(decimal salary, int monthCount, decimal bonus);
經過簡單修改後,下面是我們新的AsyncTest類
Code1
1//我們使用委托來提供.Net的異步機制 2public delegate string AsyncEventHandler(string name); // 對應Hello 方法 3public delegate decimal SalaryEventHandler(decimal salary, int monthCount, decimal bonus); // 對應YearlySalary方法 4public class AsyncTest 5{ 6 public string Hello(string name) 7 { 8 return "Hello:" + name; 9 } 10 11 /**//// <summary> 12 /// 計算一年的薪水 13 /// </summary> 14 /// <param name="salary">月薪</param> 15 /// <param name="monthCount">一年支付月數量</param> 16 /// <param name="bonus">獎金</param> 17 /// <returns></returns> 18 public decimal YearlySalary(decimal salary, int monthCount, decimal bonus) 19 { 20 //添加輔助方法,查看當前的線程ID 21 Console.WriteLine("Thread ID:#{0}", Thread.CurrentThread.ManagedThreadId); 22 23 return salary * monthCount + bonus; 24 } 25}
這裡用.NET Reflector 5 來反編譯,之所以用這個,因為比微軟的會更加清晰明了.如果想了解這個工具的朋友可查看(http://reflector.red-gate.com/)
圖1
開始我先對圖1中的小圖標進行個簡單的解釋
= 類(Class) = 類繼承的基類 = sealed(委托)
= 類的構造函數 = 方法 =virtual方法
下面我們先比較下SalaryEventHandler與 AsyncEventHandler委托的異同.
1) SalaryEventHandler
public delegate decimal SalaryEventHandler(decimal salary, int monthCount, decimal bonus);
圖2.1
編譯器生成的類Code2.1(圖2.1)
Code 2.1
1 public sealed class SalaryEventHandler : MulticastDelegate 2 { 3 public SalaryEventHandler(object @object, IntPtr method) 4 {.} 5 public virtual IAsyncResult BeginInvoke(decimal salary, int monthCount, decimal bonus, AsyncCallback callback, object @object) 6 {} 7 public virtual decimal EndInvoke(IAsyncResult result) 8 {} 9 public virtual decimal Invoke(decimal salary, int monthCount, decimal bonus) 10 {} 11 }
2) AsyncEventHandler
public delegate string AsyncEventHandler(string name);
圖2.2
編譯器生成的類Code2.2(圖2.2)
Code2.2
1 public sealed class AsyncEventHandler : MulticastDelegate 2 { 3 public AsyncEventHandler(object @object, IntPtr method) 4 {.} 5 public virtual IAsyncResult BeginInvoke(string name, AsyncCallback callback, object @object) 6 {} 7 public virtual string EndInvoke(IAsyncResult result) 8 {} 9 public virtual string Invoke(string name) 10 {} 11 }
對比兩個委托(事實上是一個sealed 的類),都繼承於System.MuliticaseDelegate, 三個virtual的 Invoke / BeginInvoke / EndInvoke 方法.
//同步方法
Invoke : 參數的個數,類型, 返回值都不相同
//異步方法,作為一組來說明
BeginInvoke : 參數的個數和類型不同,返回值相同
EndInvoke : 參數相同,返回值不同
這裡我們先介紹下 Invoke這個方法, 我們用SalaryEventHandler委托為例(直接調用Code 1 的類)
Code 3
1class Program 2{ 3 static void Main(string[] args) 4 { 5 //添加輔助方法,查看當前的線程ID 6 Console.WriteLine("Main Thread ID:#{0}", Thread.CurrentThread.ManagedThreadId); 7 8 AsyncTest test = new AsyncTest(); 9 //[1],我們習慣的調用方式 10 decimal v1 = test.YearlySalary(100000, 15, 100000); 11 //使用委托調用 12 SalaryEventHandler salaryDelegate = test.YearlySalary; 13 //[2],編譯器會自動的把[2]轉變成[3]Invoke的調用方式,[2]和[3]是完全相同的 14 decimal v2 = salaryDelegate(100000, 15, 100000); 15 //[3] 16 decimal v3 = salaryDelegate.Invoke(100000, 15, 100000); 17 18 Console.WriteLine("V1:{0},V2:{1},V3:{2}", v1, v2, v3); 19 Console.ReadLine(); // 讓黑屏等待,不會直接關閉.. 20 } 21}
輸出的結果
圖3
從結果可以看出,他們是同一個線程調用的(都是#10).這就說明[1],[2],[3]是同步調用
[2],[3]對比[1], 只不過[2],[3]是通過委托的方式(其實我們可以說成“通過代理的方式完成”),[1]是直接的調用.舉一個我們平常生活中例子:買機票,我們到代理點購買機票而不是直接跑到機場購買,就好像我們叫別人幫我們買機票一樣,最後到手的機票是一樣的, SalaryEventHandler就是我們的代理點.所以用”代理”的方式還是直接調用的方式,他們提供的參數和返回值必須是一樣的.
接下來我們開始講異步機制核心的兩個方法BeginInvoke/EndInvoke,他們作為一個整體來完成Invoke方法的調用,不同於Inoke方法的是他們是異步執行(另外開一個線程執行)的,下面先解釋下他們的作用
BeginInvoke : 開始一個異步的請求,調用線程池中一個線程來執行
EndInvoke : 完成異步的調用, 處理返回值 和 異常錯誤.
注意: BeginInvoke和EndInvoke必須成對調用.即使不需要返回值,但EndInvoke還是必須調用,否則可能會造成內存洩漏.
我們來對比下 SalaryEventHandler與 AsyncEventHandler委托反編譯BeginInoke後的異同.
SalaryEventHandler 委托:
public virtual IAsyncResult BeginInvoke(decimal salary, int monthCount, decimal bonus, AsyncCallback callback, object @object)
AsyncEventHandler 委托:
public virtual IAsyncResult BeginInvoke(string name, AsyncCallback callback, object @object)
可以看出參數的個數和類型是不同的,我們把焦點放到他們的相同點上,
1,返回值是相同的: 返回IAsyncResult 對象(異步的核心). IAsyncResult是什麼呢? 簡單的說,他存儲異步操作的狀態信息的一個接口,也可以用他來結束當前異步.具體的可以看下 http://msdn.microsoft.com/zh-cn/library/system.iasyncresult(VS.80).aspx
2,編譯器會根據委托的參數個數和類型生成相應的BeginInvoke方法,只有最後兩個參數是永遠相同的,他提供一個AsyncCallback 委托(public delegate void AsyncCallback(IAsyncResult ar);) 和一個 Object 對象.
我們再來看看EndInvoke的異同.
SalaryEventHandler 委托:
public virtual decimal EndInvoke(IAsyncResult result)
AsyncEventHandler 委托:
public virtual string EndInvoke(IAsyncResult result)
EndInvoke的參數是一樣的, 唯一是在是返回值不同(他們會根據自己委托的返回值類型生成自己的類型)
好,下面我會通過例子來說明BeginInvoke/EndInvoke,還是使用SalaryEventHandler委托為例(直接調用Code 1 的類)
.Net Framework 提供了兩種方式來使用異步方法
第一種: 通過IAsyncResult 對象
Code 4.1
1class Program 2{ 3 static IAsyncResult asyncResult; 4 5 static void Main(string[] args) 6 { 7 8 AsyncTest test = new AsyncTest(); 9 SalaryEventHandler dele = test.YearlySalary; 10 //異步方法開始執行,返回IAsyncResult(存儲異常操作的狀態信息) 接口,同時EndInvoke 方法也需要他來作為參數來結束異步調用 11 asyncResult = dele.BeginInvoke(100000, 15, 100000, null, null); 12 //獲取返回值 13 decimal val = GetResult(); 14 Console.WriteLine(val); 15 Console.ReadLine(); // 讓黑屏等待,不會直接關閉.. 16 } 17 18 static decimal GetResult() 19 { 20 decimal val = 0; 21 //獲取原始的委托對象:先是獲取AsyncResult對象,再根據他的AsyncDelegate屬性來調用當前的(那一個)委托對象 22 AsyncResult result = (AsyncResult)asyncResult; 23 SalaryEventHandler salDel = (SalaryEventHandler)result.AsyncDelegate; 24 25 //調用EndInvoke獲取返回值 26 val = salDel.EndInvoke(asyncResult); 27 28 return val; 29 } 30}
第二種: 通過回調函數. 使用倒數第二個參數AsyncCallback 委托(public delegate void AsyncCallback(IAsyncResult ar);) ,建議使用這種方法.
Code 4.2
1class Program 2{ 3 static void Main(string[] args) 4 { 5 AsyncTest test = new AsyncTest(); 6 SalaryEventHandler dele = test.YearlySalary; 7 8 //異步方法開始執行,使用BeginInvoke 倒數第二個參數(AsyncCallback委托對象) ,而不用返回值 9 dele.BeginInvoke(100000, 15, 100000, GetResultCallBack, null); 10 //和上面相同的 11 //AsyncCallback callback = new AsyncCallback(GetResultCallBack); 12 //dele.BeginInvoke(100000, 15, 100000, callback, null); 13 14 Console.ReadLine(); // 讓黑屏等待,不會直接關閉.. 15 } 16 17 //必須遵循AsyncCallback 委托的定義:返回值為空,一個IAsyncResult對象參數 18 static void GetResultCallBack(IAsyncResult asyncResult) 19 { 20 decimal val = 0; 21 //獲取原始的委托對象 22 AsyncResult result = (AsyncResult)asyncResult; 23 SalaryEventHandler salDel = (SalaryEventHandler)result.AsyncDelegate; 24 25 //調用EndInvoke獲取返回值 26 val = salDel.EndInvoke(asyncResult); 27 28 Console.WriteLine(val); 29 } 30}
BeginInvoke最後一個參數是做什麼的呢?我把Code 4.2 方法修改下.
Code 4.3
1class Program 2{ 3 static void Main(string[] args) 4 { 5 AsyncTest test = new AsyncTest(); 6 SalaryEventHandler dele = test.YearlySalary; 7 8 //異步方法開始執行,看最後一個參數(Object對象) [Note1:],這裡我們傳遞2000(int) 9 dele.BeginInvoke(100000, 15, 100000, GetResultCallBack, 2000); 10 11 Console.ReadLine(); // 讓黑屏等待,不會直接關閉.. 12 } 13 14 static void GetResultCallBack(IAsyncResult asyncResult) 15 { 16 //[Note1:],他的作用就是來 "傳遞額外的參數",因為他本身是Object對象,我們可以傳遞任何對象 17 int para = (int)asyncResult.AsyncState; 18 Console.WriteLine(para);//輸出:2000 19 } 20}
異步的異常處理
接下來再講講EndInvoke,獲取最後的返回值之外,他的一個重要的應用在”引發異常來從異步操作返回異常”
Code 5
1class Program 2{ 3 static void Main(string[] args) 4 { 5 AsyncTest test = new AsyncTest(); 6 SalaryEventHandler dele = test.YearlySalary; 7 8 dele.BeginInvoke(100000, 15, 100000, GetResultCallBack, null); 9 Console.ReadLine(); // 讓黑屏等待,不會直接關閉.. 10 } 11 12 static void GetResultCallBack(IAsyncResult asyncResult) 13 { 14 decimal val = 0; 15 //獲取原始的委托對象 16 AsyncResult result = (AsyncResult)asyncResult; 17 SalaryEventHandler salDel = (SalaryEventHandler)result.AsyncDelegate; 18 try 19 { 20 //如果EndInvoke發生異常,會在EndInvoke得到原始的異常. 21 val = salDel.EndInvoke(asyncResult); 22 Console.WriteLine(val); 23 } 24 catch (Exception ex) 25 { 26 Console.WriteLine(ex.Message); 27 } 28 } 29} 30public delegate decimal SalaryEventHandler(decimal salary, int monthCount, decimal bonus); // 對應YearlySalary方法 31public class AsyncTest 32{ 33 public decimal YearlySalary(decimal salary, int monthCount, decimal bonus) 34 { 35 throw new Exception("error"); //引發異常 36 return salary * monthCount + bonus; 37 } 38}
我們主動在YearlySalary方法中引發異常,BeginInvoke開始異步調用的時候捕獲到了這個異常,.Net Framework會在EndInvoke得到原始的異常.
說到這裡,大家是否可以簡單的應用委托來開始自己的異步操作呢? 下面看看我是怎樣為我自己的類添加異步的.
第1步, 類的定義,需要遵循.Net Framework 的規則
1)同步和異步是同時並存的
2)從最上面的兩個委托SalaryEventHandler與 AsyncEventHandler生成的BeginInvoke / EndInvoke 對比中看出,我們也來定義我們自己的異步方法,我們遵循微軟設計師異步方法設計的規則,Begin+同步方法名 / End+同步方法名
BeginXXX 必須返回IAsyncResult對象,後兩位參數必須為AsyncCallback callback, object state,前面的參數和同步方法的參數一樣
EndXXX 參數必須為IAsyncResult對象,返回值為同步方法的返回值
Code 6.1
1 public class AsyncTest
2{
3 private delegate string AsyncEventHandler(string name);
4 private AsyncEventHandler _Async;
5 public string Hello(string name)
6 {
7 return "Hello:" + name;
8 }
9
10 //按照.Net Framework的規則 ,編寫我們自己的BeginInvoke方法
11 public virtual IAsyncResult BeginHello(string name, AsyncCallback callback, object state)
12 {
13 AsyncEventHandler del = Hello;
14
15 this._Async = del;
16
17 return del.BeginInvoke(name, callback, state);
18 }
19 //編寫我們自己的EndInvoke方法
20 public virtual string EndHello(IAsyncResult asyncResult)
21 {
22 if (asyncResult == null)
23 throw new ArgumentNullException("asyncResult");
24 if (this._Async == null)
25 throw new ArgumentException("_Async");
26
27 string val = string.Empty;
28 try
29 {
30 val = this._Async.EndInvoke(asyncResult);
31 }
32 finally
33 {
34 this._Async = null;
35 }
36 return val;
37 }
38}
第2步: 調用我們編寫的類
Code 6.2
1 class Program
2{
3 static void Main(string[] args)
4 {
5 AsyncTest test = new AsyncTest();
6 //使用回調函數,就是上面提到的"第二種"
7 AsyncCallback callback = new AsyncCallback(OnHelloCallback);
8 test.BeginHello("Andy Huang", callback, test);
9 //和上面一樣
10 //IAsyncResult result = test.BeginHello("Andy Huang", OnHelloCallback, test);
11
12 Console.ReadLine(); // 讓黑屏等待,不會直接關閉..
13 }
14
15 static void OnHelloCallback(IAsyncResult asyncResult)
16 {
17 //獲取額外的參數
18 AsyncTest obj = (AsyncTest)asyncResult.AsyncState;
19 string val = obj.EndHello(asyncResult);
20 Console.WriteLine(val);
21 }
22}
附:Hello的方法的異步重構在下面的代碼中可以下載到.
本文配套源碼