程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> C#基礎學習 —— 異步編程篇 (二)

C#基礎學習 —— 異步編程篇 (二)

編輯:關於C#

基於事件的異步模式是比 IAsyncResult 模式更高級的一種異步編程模式,也被用在更多的場合。對於相對簡單的應用程序可以直接用 .Net 2.0 新增的 BackgroundWorker 組件來很方便的實現,對於更復雜的異步應用程序則需要自己實現一個符合基於事件的異步模式的類。這兩者對我都是新東西,先從簡單的入手,下一篇裡我再去嘗試復雜類模型的實現

模式概述

支持基於事件的異步模式的類會有若干個 MethodNameAsync 方法表示開始異步操作,並有對應的 MethodNameCompleted 事件。類裡面還可能會有 CancelAsync或 MethodNameAsyncCancel 方法用於取消異步操作,並可以有 ProgressChanged 或 MethodNameProgressChanged事件來跟蹤執行進度。下面分別作一下解釋

MethodNameAsync 方法可以有兩個重載:單調用和多調用,多調用有一個額外的狀態對象參數 userState。userState 參數用來區分各次異步操作,使得我們可以多次調用多調用形式的方法而不需要等待任何異步操作的完成(在學習 IAsyncResult 模式時我把狀態對象僅僅當成傳給回調方法的一個條件來用,可能在使用模式時這麼做並沒有什麼關系,但在實現模式時不把狀態對象用作異常調用的唯一標識而另作他用就值得商榷了)。而單調用形式的方法如果在前一個調用尚未完成時調用將會拋出 InvalidOperationException 異常

如果有多個異步方法,則應使用 CancelAsync 方法來取消掛起的操作,並可使用 userState 來取消指定的掛起任務。如果只有一個異步方法則可以使用 MethodNameAsyncCancel 方法

另外 MSDN 上說:一次只支持一個掛起的操作的方法(如 Method1Async(string param))是不可取消的。這句話我還沒有理解,不可能說是單調用的異步方法就不能取消吧,BackgroundWorker 上都是這樣做的

先不管了,接著看ProgressChanged 事件。它有一個 ProgressChangedEventArgs 參數,事件處理程序通過檢查該參數的 ProgressPercentage屬性來獲取任務完成的百分比。如果有多個異步操作掛起,也可以通過檢查參數的UserState 屬性來分辨操作。如果需要用 ProgressChanged 事件來報告增量結果,則可以把結果保存在派生自 ProgressChangedEventArgs 的類中,並在事件處理程序中使用

BackgroundWorker

BackgroundWorker 很好的符合了事件異步操作模式。它有兩個重載版本的 RunWorkerAsync 方法(均為單調用形式)和 RunWorkerCompleted 事件,並有 CancelAsync 方法以及 ProcessChanged 事件。不同的是 BackgroundWorker增加了 DoWork 事件,在 RunWorkerAsync 方法調用時發生,以達到將實際執行的開始方法與 BackgroundWorker 分離的目的。還需要提一下的是 WorkerReportsProcess 屬性和ReportProcess 方法,前者指示能否報告進度更新,後者引發 ProcessChanged 事件,它們會在接下來的 Demo 裡用到

嘗試

因為平時經常要處理幾十兆的文本文件,這個 Demo 就做一個讀取文件並顯示進度的控制台程序。先看類名和字段

class BackgroundWorkerDemo
   {
     private BackgroundWorker m_bw;
     string m_FilePath;
   }

構造函數接收文件路徑為參數,設置文件路徑並初始化 BackgroundWorker

public BackgroundWorkerDemo(string filePath)
     {
       m_FilePath = filePath;
  m_bw = new BackgroundWorker();
       m_bw.WorkerReportsProgress = true;
       m_bw.DoWork += new DoWorkEventHandler(BackgroundWorker_DoWork);
       m_bw.ProgressChanged += new ProgressChangedEventHandler(BackgroundWorker_ProgressChanged);
       m_bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(BackgroundWorker_RunWorkerCompleted);
     }

接下來看這三個事件的處理程序。每一個事件都有各自的 EventArgs 參數類型,都很簡單就不多說了

第一個 BackgroundWorker_DoWork 方法寫得我有些郁悶。我在方法裡取文件長度,先是直接取 StreamReader.BaseStream.Length 或 FileInfo.Length ,結果卻導致很多文件讀不到 100% 就結束了,不得已改成先把整個文件讀一次得到字符串的長度。這樣的方法當然性能不好了,主要是因為自己對 IO 一直就不夠清楚,等下一個主題重新認識下 IO 再回頭過來改吧。也望有經驗的朋友賜教,感激不盡

/**//// <summary>
     /// DoWork event process method
     /// </summary>
     /// <param name="sender"></param>
     /// <param name="e"></param>
     private void BackgroundWorker_DoWork(object sender, DoWorkEventArgs e)
     {
       long length;
       using (StreamReader sr = new StreamReader(m_FilePath))
       {
         // Get file length
         length = sr.ReadToEnd().Length;
       }
  using (StreamReader sr = new StreamReader(m_FilePath))
       {
         long onePercentOfLength = length / 100;
         long currentPosition = 0;
         int i = 0;
  while (!sr.EndOfStream)
         {
           sr.Read();
           currentPosition ++;
  // Produce ProcessChanged event in each percent reading
           while (currentPosition > onePercentOfLength * i)
           {
             ((BackgroundWorker)sender).ReportProgress(i++);
           }
         }
  // e.Result will be used in RunWorkerCompleted event process method
         e.Result = currentPosition;
       }
     }
  BackgroundWorker_ProgressChanged 方法,簡單輸出當前進度
  /**//// <summary>
     /// ProgressChanged event process method
     /// </summary>
     /// <param name="sender"></param>
     /// <param name="e"></param>
     private void BackgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
     {
       Console.WriteLine("Reading percents: " + e.ProgressPercentage + "%");
     }

BackgroundWorker_RunWorkerCompleted 方法,輸出結果。這裡要注意如果 RunWorkerCompletedEventArgs 參數的 Error 屬性不為空則讀取其他屬性會產生異常,然後如果 Cancelled 屬性為 true 則讀取 Result 屬性也會產生異常,因此必須依次判斷各屬性的值

/**//// <summary>
     /// RunWorkerCompleted event process method
     /// </summary>
     /// <param name="sender"></param>
     /// <param name="e"></param>
     private void BackgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
     {
       if (e.Error != null)
       {
         Console.WriteLine("Error occurs: " + e.Error.Message);
       }
       else if(e.Cancelled)
       {
         Console.WriteLine("Work cancelled");
       }
       else
       {
         Console.WriteLine("Read finished, the file length is: " + e.Result);
       }
     }

向外提供一個入口方法

/**//// <summary>
     /// Test portal
     /// </summary>
     public void ReadAsync()
     {
       if (File.Exists(m_FilePath))
       {
         Console.WriteLine("Begin read");
         m_bw.RunWorkerAsync();
       }
       else
       {
         throw new FileNotFoundException("Can't find file: " + m_FilePath);
       }
     }

最後是 Main 方法,比昨天有了小小的改變,用 Console.ReadLine 代替了 Thread.Sleep 來達到阻止主線程退出的目的

class BackgroundWorkerTest
   {
     static void Main(string[] args)
     {
       Console.Write("Input file path: ");
       string filePath = Console.ReadLine();
  BackgroundWorkerDemo demo = new BackgroundWorkerDemo(filePath);
       demo.ReadAsync();
  // Thread waiting
       Console.ReadLine();
     }
   }

其他

回顧一下我用委托實現 IAsyncResult 模式的 Demo ,與用 BackgroundWorker 實現的基於事件的異步模式很相似吧。而且應用程序可以通過委托的 BeginInvoke 和 EndInvoke 方法來異步執行現有的同步方法而不需要作額外的修改,BackgroundWorker 也差不多是一樣。我把這兩者看成實現對應異步操作模式的范本,在性能要求不是很高的一些異步操作場合,用好委托和 BackgroundWorker 就可以簡單有效的完成開發了

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved