我所知道的.NET異步


對於異步,相信大家都不十分陌生。准確點來說就是方法執行后立即返回,待到執行完畢會進行通知。就是當一個任務在執行的時候,尤其是需要耗費很長的時間進行處理的任務,如果利用單線程進行操作的話,勢必造成界面的阻塞;而利用異步方式,則不會出現這種情況。 區別於同步處理,可以說阻塞的異步其實就相當於同步。

同步方式的實現

先來看一個同步的例子:

假設現在我們需要導入文本文件的內容,然后對文件內容做處理。那么這就需要分為兩步來進行,第一步是導入文本內容,我們利用函數A表示;第二部就是處理文本,我們利用函數B來表示。假設現在A不執行完,B不能進行。而且由於文本內容非常大,導入需要十幾到幾十分鍾不等,那么我們得提示用戶導入進度,這里就涉及到了界面交互問題。利用同步方式來做,效果如何呢?首先請看運行效果:

其實上面的圖片是我運行了一段時間的程序的截圖,但是由於作用在了同步模式下,導致界面阻塞,從而產生極差的用戶體驗。

代碼如下:

View Code
 #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種異步方式

  1. ThreadPool.QueueUserworkItem實現
  2. APM模式(就是BeginXXX和EndXXX成對出現。)
  3. EAP模式(就是Event based, 准確說來就是任務在處理中或者處理完成,會拋出事件)
  4. 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...";
}
}

當然,這個組件在函數運行的過程中,需要向組件傳送當前進度的信息,並且在運行過程中,需要檢測任務有沒有被取消,以達到自動取消任務的功能:

View Code
#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

代碼下載

點擊這里下載


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM