我所知道的.NET異步
對於異步,相信大家都不十分陌生。準確點來說就是方法執行後立即返回,待到執行完畢會進行通知。就是當一個任務在執行的時候,尤其是需要耗費很長的時間進行處理的任務,如果利用單線程進行操作的話,勢必造成界面的阻塞;而利用異步方式,則不會出現這種情況。 區別於同步處理,可以說阻塞的異步其實就相當於同步。
同步方式的實現
先來看一個同步的例子:
假設現在我們需要導入文本文件的內容,然後對文件內容做處理。那麼這就需要分爲兩步來進行,第一步是導入文本內容,我們利用函數A表示;第二部就是處理文本,我們利用函數B來表示。假設現在A不執行完,B不能進行。而且由於文本內容非常大,導入需要十幾到幾十分鐘不等,那麼我們得提示用戶導入進度,這裏就涉及到了界面交互問題。利用同步方式來做,效果如何呢?首先請看運行效果:
其實上面的圖片是我運行了一段時間的程序的截圖,但是由於作用在了同步模式下,導致界面阻塞,從而產生極差的用戶體驗。
代碼如下:
#region 第一步:加載進入內存 private void ReadIntoMemory() { if (String.IsNullOrEmpty(fileName)) { MessageBox.Show("文件名不能爲空!"); return; } string result; long mainCount = 0; using (StreamReader sr = new StreamReader(fileName, Encoding.Default)) { while ((result = sr.ReadLine()) != null) { mainCount++; recordList.Add(result); //添加記錄到List中存儲,以便在下一步進行處理。 double statusResult = (double)mainCount / (double)totalCount; lblCurrentRecords.Text = mainCount.ToString(); lblStatus.Text = statusResult.ToString("p"); pbMain.Value = Int32.Parse((Math.Floor(statusResult)*100).ToString()); } } } #endregion #region 第二步:處理數據 private void ProcessRecords() { if (recordList ==null) { throw new Exception("數據不存在!"); } if (recordList.Count==0) { return; } int childCount = 0; int recordCount = recordList.Count; for (int i = 0; i < recordCount; i++) { string thisRecord=recordList[i]; if (String.IsNullOrEmpty(thisRecord) || !thisRecord.Contains(",")) { return; } string[] result = thisRecord.Split(','); ListViewItem lvi = new ListViewItem(result[0]); for (int j = 1; j < result.Length; j++) { lvi.SubItems.Add(result[j]); } listItem.Add(lvi); childCount++; double percentage = (double)childCount / (double)recordCount; pbChild.Value = Int32.Parse((Math.Floor(percentage) * 100).ToString()); } } #endregion
那麼我們是如何運行的呢:
#region 開始進行處理 private void btnLoad_Click(object sender, EventArgs e) { GetTotalRecordNum(); //得到總條數 ReadIntoMemory(); ProcessRecords(); } #endregion
看到了沒,我們是直接順序運行的。之所以出現上面的情況,最主要就是界面處理和後臺處理均糅合在了同一個線程之中,這樣當後臺進行數據處理的時候,會造成前臺UI線程無法更新UI。要解決這種情況,當然是使用異步方式類處理。
那麼在.net編程中,有哪幾種模式可以實現異步呢?
4種異步方式
- ThreadPool.QueueUserworkItem實現
- APM模式(就是BeginXXX和EndXXX成對出現。)
- EAP模式(就是Event based, 準確說來就是任務在處理中或者處理完成,會拋出事件)
- Task
上面總共4種方式中,其中在.net 2.0中常用的是(1),(2),(3),而在.net 4.0中支持的是(4),注意(4)在.net 2.0中是不能使用的,因爲不存在。
首先來說說ThreadPool.QueueUserWorkItem方式,也是最簡單的一種方式。
系統將需要運行的任務放到線程池中,那麼線程池中的任務就有機會通過並行的方式進行運行。
其次來說說APM模式
這種模式非常常見,當然也是Jeff Richter極力推薦的一種方式。同時我也是這種模式的粉絲。這種模式的使用非常簡單,就是利用Begin***的方式將需要進行異步處理的任務放入,然後通過End***的方式來接受方法的返回值。同時在Begin***和End***任務進行的過程中,如果涉及到界面UI的更新的時候,我們完全可以加入通知的功能。
在Begin***和End***進行處理的時候,傳遞的是IAsyncResult對象,這種對象在Begin***中會承載一個委託對象,然後在End***中進行還原並得到返回值。
如果你在設計的時候,需要有多個方法用到異步,並且想控制他們的運行順序,請參考ManualResetEvent 和 AutoResetEvent方法,他們均是通過設置信號量來進行同步的。
下面來看一個例子:
假設現在我們需要導入文本文件的內容,然後對文件內容做處理。那麼這就需要分爲兩步來進行,第一步是導入文本內容,我們利用函數A表示;第二部就是處理文本,我們利用函數B來表示。假設現在A不執行完,B不能進行。而且由於文本內容非常大,導入需要十幾到幾十分鐘不等,那麼我們得提示用戶導入進度,這裏就涉及到了界面交互問題。利用APM模式如何來做呢?首先請看運行效果:
代碼如下:
#region 典型的APM處理方式,利用Action作爲無參無返回值的委託 private void BeginReadIntoMemory() { Action action = new Action(ReadIntoMemory); action.BeginInvoke(new AsyncCallback(EndReadIntoMemory), action); } private void EndReadIntoMemory(IAsyncResult iar) { Action action = (Action)iar.AsyncState; action.EndInvoke(iar); } private void BeginProcessRecords() { Action action = new Action(ProcessRecords); action.BeginInvoke(new AsyncCallback(EndProcessRecords), action); } private void EndProcessRecords(IAsyncResult iar) { Action action = (Action)iar.AsyncState; action.EndInvoke(iar); } #endregion
我們是如何調用的呢:
#region 開始進行處理,需要通過ManualResetEvent設置xinhaoilang的方式進行同步 private void btnLoad_Click(object sender, EventArgs e) { GetTotalRecordNum(); //得到總條數 BeginReadIntoMemory(); //讀取數據到內存 BeginProcessRecords(); //處理數據內容 } #endregion
在上面的代碼段中,APM模式的處理方式很明顯,Begin×××和End×××成對出現,這種方式使用簡便,所以很推薦。並且如果涉及到順序執行的情況,請參加我的前一篇文章:淺談C#中常見的委託
然後來說說EAP模式
這種模式也很常見,準確來說就是在系統中通過申明委託事件,然後在執行過程中或者執行完畢後拋出事件。最常見的莫過於WebClient類的DownloadStringCompleted
事件,這裏我們將使用BackgroundWorker來進行講解,雖然它本身就能夠實現異步操作。在這裏,我們只是用到了一個從文本中讀取大數據量到內存的操作。圖示如下:
這裏是進行中的操作:
這裏是撤銷後的操作:
那麼是如何實現的呢?我們先從BackgroundWorker註冊的幾個事件說起:
首先是DoWork事件,他的註冊方式如下:
bgWorker.DoWork += new DoWorkEventHandler(worker_DoWork);
這個主要是用來開啓任務的:
private void worker_DoWork(object sender, DoWorkEventArgs e) { BackgroundWorker worker = sender as BackgroundWorker; ReadIntoMemory(worker, e); //開始工作 }
然後就是ProgressChanged事件,註冊方式如下:
bgWorker.ProgressChanged += new ProgressChangedEventHandler(worker_ProgressChanged);
從字面上就知道是進行進度報告:
private void worker_ProgressChanged(object sender, ProgressChangedEventArgs e) { pbMain.Value = e.ProgressPercentage; //利用PrograssBar報告導入進度 }
最後就是任務完成報告,註冊方式爲:
bgWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(worker_RunWorkerCompleted);
這裏可以進行錯誤捕獲以及任務取消方面的處理:
private void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { if (e.Error != null) { MessageBox.Show(e.Error.Message); } else if (e.Cancelled) { tsInfo.Text = "Data Loading Canceled..."; } else { tsInfo.Text = "Data Loading Completed..."; } }
當然,這個組件在函數運行的過程中,需要向組件傳送當前進度的信息,並且在運行過程中,需要檢測任務有沒有被取消,以達到自動取消任務的功能:
#region 第一步:加載數據到內存 private void ReadIntoMemory(BackgroundWorker worker, DoWorkEventArgs e) { if (String.IsNullOrEmpty(fileName)) { MessageBox.Show("文件名不能爲空!"); return; } string result; long mainCount = 0; using (StreamReader sr = new StreamReader(fileName, Encoding.Default)) { while ((result = sr.ReadLine()) != null) { mainCount++; recordList.Add(result); //添加記錄到List中存儲,以便在下一步進行處理。 double statusResult = (double)mainCount / (double)totalCount; syncContext.Send(new SendOrPostCallback((s) => { if (worker.CancellationPending) //檢測到用戶取消任務 { e.Cancel = true; //任務取消 } else { lblCurrentRecords.Text = mainCount.ToString(); lblStatus.Text = statusResult.ToString("p"); int thisPercentange = Int32.Parse((Math.Floor(statusResult * 100)).ToString()); //pbMain.Value = thisPercentange; worker.ReportProgress(thisPercentange); //報告當前的進度 tsNotify.Text = "| 當前導入"; } }), null); } } } #endregion
再說說利用task的實現的方式
關於Task類,可以說在4.0之前從來沒有見過,使用起來非常的簡單,也很方便。其實,對於Task類,我也是參考了諸多文章,下面的這句話,引用自另外一篇文章:
Task在並行計算中的作用很凸顯,首次構造一個Task對象時,他的狀態是Created。以後,當任務啓動時,他的狀態變成WaitingToRun。Task在一個線程上運行時,他的狀態變成Running。任務停止運行,並等待他的任何子任務時,狀態變成WaitingForChildrenToComplete。任務完全結束時,它進入以下三個狀態之一:RanToCompletion,Canceled或者Faulted。一個Task<TResult>運行完成時,可通過Task<TResult>的Result屬性來查詢任務的結果,一個Task或者Task<TResult>出錯時,可以查詢Task的Exception屬性來獲得任務拋出的未處理的異常,該屬性總是返回一個AggregateException對象,他包含所有未處理的異常。
爲簡化代碼,Task提供了幾個只讀的Boolean屬性,IsCanceled,IsFaulted,IsCompleted。注意,當Task處於RanToCompleted,Canceled或者Faulted狀態時,IsCompleted返回True。爲了判斷一個Task是否成功完成,最簡單的方法是if(task.Status == TaskStatus.RanToCompletion)。
當然,我們還是以上面的例子來進行編程與講解。
首先,我們要開啓一個Task,那麼Task taskOne = new Task(ReadIntoMemory);表示將ReadIntoMemory函數註冊成爲了任務來運行,然後利用taskOne.Start();來開啓任務。那麼如何運行第二個任務,並且還要等到第一個運行完成之後呢? 這裏我們就需要用到其ContinueWith方法:
Task taskTwo = taskOne.ContinueWith(Action => { ProcessRecords(); });
這樣,就行了那麼當運行的時候,程序的確會按照順序來啓動任務。圖示和APM模式中的圖片相同,我就不貼了,下面是代碼:
Task taskOne = new Task(ReadIntoMemory);
taskOne.Start();
Task taskTwo = taskOne.ContinueWith(Action => { ProcessRecords(); });
Task<TResult>泛型方法中的TResult爲返回值類型,承載的是一個無參,但是有返回值的任務。所以傳入的函數要麼是有一個參數帶返回值的;要麼就是無參數帶返回值的,要麼就是無參數無返回值的。如果是一個參數,有返回值的話,可以利用下面的方式來進行:
Task<int> taskOne = new Task<int>(a=>ReadIntoMemory((int)a),5);
參考資料
同時大家也可以參看我在StackOverflow中的提問,以期起到拋磚引玉的作用:
參考博客:http://hi.baidu.com/jackeyrain/blog/item/828ec3f70bfa8635730eec0a.html
代碼下載
本地下載