在開發winform和調用asp.net的web service引用的時候,會出現許多命名為 MethodNameAsync 的方法。
例如:
winform的按鈕點擊
this.button1.Click += new System.EventHandler(this.button1_Click);
private void button1_Click(object sender, EventArgs e) { //dosomething }
這就是基於事件的異步編程模式,它實現了不影響主線程的情況下異步調用耗時方法,在完成的時候通過事件進行函數回調,一般情況下,我們都應該使用該模式來公開類的異步方法。
那什么時候需要使用IAsyncResult 模式呢?微軟給出了很好的答案,見https://msdn.microsoft.com/zh-cn/library/ms228966(v=vs.110).aspx
接下來就讓我們通過代碼實現一個基於事件的異步模式
代碼場景
我們模擬一個下載器,下載我喜愛的影片,過程中實時展示下載進度,並且在下載完成后進行提醒。
核心代碼如下:
public class Downloader { //聲明事件參數 public class DownloadCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { private string m_result; public DownloadCompletedEventArgs(string result, Exception error, bool cancelled, Object userState) : base(error, cancelled, userState) { m_result = result; } public string Result { get { //只讀屬性在返回屬性值之前應調用 RaiseExceptionIfNecessary 方法。 如果組件的異步輔助代碼將某一異常指定給 Error 屬性或將 Cancelled 屬性設置為 true,則該屬性將在客戶端嘗試讀取它的值時引發異常。 這會防止客戶端因異步操作失敗而訪問可能無效的屬性。 RaiseExceptionIfNecessary(); return m_result; } } } //聲明委托 public delegate void ProgressChangedEventHandler( ProgressChangedEventArgs e);//ProgressChangedEventArgs自帶有了。 public delegate void DownloadCompletedEventHandler(object sender, DownloadCompletedEventArgs e); //內部下載處理委托 private delegate string DownLoadHandler(string url, string name, AsyncOperation asyncOp); //聲明事件 public event ProgressChangedEventHandler ProgressChanged; public event DownloadCompletedEventHandler DownloadCompleted; //聲明SendOrPostCallback委托,通過AsyncOperation.post會將這些調用正確地封送到應用程序模型的合適線程或上下文。 private SendOrPostCallback onProgressChangedDelegate; private SendOrPostCallback onDownloadCompletedDelegate; /// <summary> /// 構造函數 /// </summary> public Downloader() { onProgressChangedDelegate = new SendOrPostCallback(onProgressChanged); onDownloadCompletedDelegate = new SendOrPostCallback(onDownloadComplete); } /// <summary> /// 通過AsyncOperation調用onProgressChangedDelegate委托關聯該函數,保證運行在合適線程 /// </summary> /// <param name="state"></param> private void onProgressChanged(object state) { if (ProgressChanged != null) { ProgressChangedEventArgs e = state as ProgressChangedEventArgs; ProgressChanged(e); } } private void onDownloadComplete(object state) { if (DownloadCompleted != null) { DownloadCompletedEventArgs e = state as DownloadCompletedEventArgs; DownloadCompleted(this, e); } } /// <summary> /// 異步下載文件 /// </summary> /// <param name="url"></param> /// <param name="name"></param> public void DownloadAsync(string url, string name) { //url不能為null if (url == null) { throw new ArgumentNullException("url"); } //userSuppliedState 參數來唯一地標識每個調用,以便區分執行異步操作的過程中所引發的事件。 //不唯一的任務 ID 可能會導致您的實現無法正確報告進度和其他事件。 代碼中應檢查是否存在不唯一的任務 ID,並且在檢測到不唯一的任務 ID 時引發 SystemArgumentException。 //由於我們不用監控異步操作狀態,所以參數設為null AsyncOperation asyncOp = AsyncOperationManager.CreateOperation(null); //異步委托調用download,如果不想再聲明DownLoadHandler委托,用Action或Fun代替也行。 DownLoadHandler dh = new DownLoadHandler(DownLoad); dh.BeginInvoke("http://pan.baidu.com/movie.avi", "喬布斯傳", asyncOp, new AsyncCallback(DownloadCallBack), asyncOp); } private void DownloadCallBack(IAsyncResult iar) { AsyncResult aresult = (AsyncResult)iar; DownLoadHandler dh = aresult.AsyncDelegate as DownLoadHandler; string r = dh.EndInvoke(iar); AsyncOperation ao = iar.AsyncState as AsyncOperation; //特定任務調用此方法后,再調用其相應的 AsyncOperation 對象會引發異常。 ao.PostOperationCompleted(onDownloadCompletedDelegate, new DownloadCompletedEventArgs(r, null, false, null)); } /// <summary> /// 提供給外部調用的同步方法 /// </summary> /// <param name="url"></param> /// <param name="name"></param> /// <returns></returns> public string DownLoad(string url, string name) { return DownLoad(url, name, null); } private string DownLoad(string url, string name, AsyncOperation asyncOp) { //url不能為null if (url == null) { throw new ArgumentNullException("url"); } for (int i = 0; i < 10; i++) { int p = i * 10; Debug.WriteLine("執行線程:" + Thread.CurrentThread.ManagedThreadId + ",傳輸進度:" + p + "%"); Thread.Sleep(1000); //不為空則是異步 if (asyncOp != null) { //在適合於應用程序模型的線程或上下文中調用委托。 asyncOp.Post(onProgressChangedDelegate, new ProgressChangedEventArgs(p, null)); } } return name + "文件下載完成!"; } }
在客戶端調用:
private async void button1_Click_1(object sender, EventArgs e) { Downloader downloader = new Downloader(); downloader.DownloadCompleted += downloader_DownloadCompleted; downloader.ProgressChanged += downloader_ProgressChanged; Debug.WriteLine("調用線程:" + Thread.CurrentThread.ManagedThreadId); //異步調用 downloader.DownloadAsync("http://baidu.com", "喬布斯傳.avi"); //同步調用,UI線程卡死 //string r = downloader.DownLoad("http://baidu.com", "喬布斯傳.avi"); //textBox1.AppendText(r); } void downloader_ProgressChanged(ProgressChangedEventArgs e) { textBox1.AppendText(("事件回調線程:" + Thread.CurrentThread.ManagedThreadId + "下載了" + e.ProgressPercentage + "%\n")); } void downloader_DownloadCompleted(object sender, Downloader.DownloadCompletedEventArgs e) { textBox1.AppendText(("事件回調線程:" + Thread.CurrentThread.ManagedThreadId + "下載完成,文件為" + e.Result + "\n")); }
運行結果:
調用線程:9
執行線程:10,傳輸進度:0%
執行線程:10,傳輸進度:10%
執行線程:10,傳輸進度:20%
執行線程:10,傳輸進度:30%
執行線程:10,傳輸進度:40%
執行線程:10,傳輸進度:50%
執行線程:10,傳輸進度:60%
執行線程:10,傳輸進度:70%
執行線程:10,傳輸進度:80%
執行線程:10,傳輸進度:90%
總結:
1、我們通過EAP模式,實現了不影響UI情況下的異步調用,歸功於AsyncOperation,避免了Control.Invoke
2、如果不在應用程序模型(包括 ASP.NET、控制台應用程序和 Windows 窗體應用程序)下正常運行,可以免去AsyncOperation這個步驟,直接在callback通知相應event。
3、實現一個這樣的類有些麻煩,如果還需要監視狀態,需要一個數組維護AsyncOperation
4、微軟也提供了BackgroundWorker類來執行耗時的后台操作。
5、正因為麻煩,所以.net4.0后面又推出了Task,.net4.5中更加簡化,各種封裝,瞬間異步,如果使用Task,我們的異步函數將變成如下幾句話。
public async void DownloadTaskAsync(string url, string name) { AsyncOperation asyncOp = AsyncOperationManager.CreateOperation(null); string r = await Task<string>.Run(() => { return DownLoad(url, name, asyncOp); }); if (DownloadCompleted != null) { DownloadCompletedEventArgs e = new DownloadCompletedEventArgs(r, null, false, null); DownloadCompleted(this, e); } }
調用方式不變。
實現基於事件的異步模式的最佳做法,見https://msdn.microsoft.com/zh-cn/library/ms228969(v=vs.110).aspx
參考:
https://msdn.microsoft.com/zh-cn/library/ms228969(v=vs.110).aspx
https://msdn.microsoft.com/zh-cn/library/e7a34yad(v=vs.110).aspx
補充:
1、CreateOperation一定要在主線程調用,會自動設置上下文,否則上下文就會是另一個線程的。
2、這里AsyncOperation的主要作用其實就是將相應委托post到CreateOperation時的上下文執行,所以DownLoadHandler中傳入asyncOp參數。BeginInvoke相當於new Thread執行,尾部參數也傳入asyncOp,在callback的時候post結果。DownLoad也傳入asyncOp參數,post進度。
3、對於一些簡單需求,回調函數調用UI時直接判斷InvokeRequired通過主線程Invoke也行。
4、如果不需要監控異步操作狀態,並且需求簡單,那么可以在初始化的時候直接在UI主線程聲明一個全局AsyncOperation,這樣統一調用者一個對象post即可。
DEMO下載地址:
鏈接:http://pan.baidu.com/s/1qYrb81Q 密碼:xu6s