我所知道的.NET異步

我所知道的.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

代碼下載

點擊這裏下載(原始下載)



本地下載

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章