C#異步委託

前些日子,看到園子裏面有人用老王喝茶的例子講解了一下同步和異步,雖然沒有代碼實現,可是可以通俗易懂的講解了同步、異步、阻塞、非阻塞的關係了,今天借題發揮,用一個熱水器加熱洗澡的例子來具體演示一下C#使用委託進行異步編程。javascript

首先引用MSDN中的一段話來描述一下如何使用異步方式
.NET Framework 容許您異步調用任何方法。 爲此,應定義與您要調用的方法具備相同簽名的委託;公共語言運行時會自動使用適當的簽名爲該委託定義 BeginInvoke 和 EndInvoke 方法。java

BeginInvoke 方法啓動異步調用。 該方法與您須要異步執行的方法具備相同的參數,還有另外兩個可選參數。 第一個參數是一個 AsyncCallback 委託,該委託引用在異步調用完成時要調用的方法。 第二個參數是一個用戶定義的對象,該對象將信息傳遞到回調方法。 BeginInvoke 當即返回,不等待異步調用完成。 BeginInvoke 返回一個 IAsyncResult,後者可用於監視異步調用的進度。編程

EndInvoke 方法檢索異步調用的結果。 在調用 BeginInvoke 以後隨時能夠調用該方法。 若是異步調用還沒有完成,則 EndInvoke 會一直阻止調用線程,直到異步調用完成。 EndInvoke 的參數包括您須要異步執行的方法的 out 和 ref 參數(在 Visual Basic 中爲 <Out> ByRef 和 ByRef)以及由 BeginInvoke 返回的 IAsyncResult。異步

上文中提到了一個 IAsyncResult 接口,這個就是今天的主角ide

複製代碼
public interface IAsyncResult
{
     object AsyncState { get; }

     WaitHandle AsyncWaitHandle { get; }

     bool CompletedSynchronously { get; }

     bool IsCompleted { get; }
}
複製代碼

 IAsyncResult 類型公開如下成員:異步編程

AsyncState :獲取用戶定義的對象,它限定或包含關於異步操做的信息
AsyncWaitHandle :獲取用於等待異步操做完成的 WaitHandle
CompletedSynchronously :獲取一個值,該值指示異步操做是否同步完成
IsCompleted :獲取一個值,該值指示異步操做是否已完成函數

 若是上面的介紹看不明白,沒有關係,下面來經過一個例子來進行演示,您必定會搞清晰明白的,先看一下程序主界面圖,以便後面的代碼說明較好理解。 post

咱們創建的是一個winform程序,咱們先用同步的方式來演示一下老王想洗澡這件事,洗澡就得用熱水器燒水,所以咱們先定義一個熱水器類 Heater,代碼以下:this

Heater熱水器代碼

 Heater類中有屬性,設定溫度,當前溫度和一個水是否燒好的狀態布爾值,並在燒水方法中讓線程休眠5秒鐘,其目的是符合實際狀況,燒水總要有個時間過程。 spa

下面咱們的老王閃亮登場,老王有兩個方法 分別是打開熱水器和看電視,代碼以下: 

老王代碼
public class LaoWang
    {
        public Heater heater { get; set; }

        public LaoWang(Heater heater)
        {
            this.heater = heater;
        }

        public int OpenHeater()
        { 
            return heater.BoilWater();
        }

        public string WatchTv()
        {
            return "老王去看電視了...\r\n";
        }
    }

  而後咱們在winform程序中編寫咱們的主代碼,咱們在同步調用按鈕的點擊事件中編寫以下代碼:

複製代碼
private void btnSync_Click(object sender, EventArgs e)
{
    this.txtSyncResult.AppendText("老王想洗澡了...\r\n");
    Heater heater = new Heater();
    heater.SetTemp = 70;
    LaoWang laowang = new LaoWang(heater);
    this.txtSyncResult.AppendText("老王打開了熱水器...\r\n");
    int curTemp = laowang.OpenHeater();
     //這裏阻塞了
    this.txtSyncResult.AppendText(laowang.WatchTv());
    if (laowang.heater.Flag)
    {
        this.txtSyncResult.AppendText("水燒好了...");
        this.txtSyncResult.AppendText("當前水溫 " + curTemp.ToString() + "");
    }
}
複製代碼

代碼編寫完成,咱們運行一下,結果以下:

 

 雖然結果是咱們預期的,貌似很合理。可是咱們會發現,當程序調用了int curTemp = laowang.OpenHeater() 方法的時候,程序就會發生阻塞,一直在等待返回值,並無當即執行老王看電視的方法,而是燒水方法完成後並返回當前水溫數值以後,纔會執行後面的代碼。哈哈,這不就說明老王很傻,在燒水準備洗澡的時候,一直再傻傻的等待在熱水器旁邊,等水燒好了,再去看電視,而後再準備洗澡。這種狀況就是咱們說的同步阻塞。

那麼這種狀況如何解決呢?下面聰明的老劉登場了,老劉玩的就是異步,燒水的期間去看電視了,不用傻傻的等着了,代碼以下:

複製代碼
public class LaoLiu
    {
        /// <summary>
        /// 熱水器類
        /// </summary>
        public Heater heater {private get; set; }

        //定義一個燒水的委託和委託變量
        private delegate int BoilWaterDelegate();
        private BoilWaterDelegate _dgBoilWater;

        public LaoLiu(Heater heater)
        {
            this.heater = heater;
            _dgBoilWater = new BoilWaterDelegate(heater.BoilWater);
        }

        /// <summary>
        /// 看電視
        /// </summary>
        public string WatchTv()
        {
            return "老劉去看電視了...\r\n";
        }

        /// <summary>
        /// 邊吃飯邊看電視
        /// </summary>
        /// <returns></returns>
        public string ListenToSong()
        {
            return "老劉去聽音樂了...\r\n";
        }

        /// <summary>
        /// 開始燒水
        /// </summary>
        /// <param name="callBack"></param>
        /// <param name="stateObject"></param>
        /// <returns></returns>
        public IAsyncResult BeginBoilWater(AsyncCallback callBack, Object stateObject)
        {
            try
            {
                return _dgBoilWater.BeginInvoke(callBack, stateObject);
            }
            catch (Exception e)
            {
               throw e;
            }
        }

        /// <summary>
        /// 燒水結束
        /// </summary>
        /// <param name="ar"></param>
        /// <returns></returns>
        public int EndBoilWater(IAsyncResult ar)
        {
            if (ar == null)
                throw new NullReferenceException("IAsyncResult 參數不能爲空");
            try
            {
                return _dgBoilWater.EndInvoke(ar);
            }
            catch (Exception e)
            {   
                throw e;
            }
        }
    }
複製代碼

咱們在老劉類中主要 聲明瞭個 委託 BoilWaterDelegate,並定義委託變量執行熱水器中的加熱方法,利用BeginBoilWater 和 EndBoilWater 方法來實現異步調用,這兩個方法與MSDN中的陳述是同樣同樣的。

BeginBoilWater 方法有兩個參數:
第一個參數是 AsyncCallback callBack,這就是個回調方法,您能夠這麼理解,就是異步完成後,調用callBack方法來繼續執行
第二個參數用戶定義的對象,該對象將信息傳遞到回調方法中。
返回值是返回一個 IAsyncResult,能夠用於監視異步是否完成。
因爲咱們的燒水方法中,沒有ref,out 等參數,所以EndBoilWater 目前只有一個參數,就是 BeginBoilWater 方法返回的 IAsyncResult,EndBoilWater 方法的返回值就是咱們熱水器類燒水方法的返回值當前溫度。

MSDN還說:EndInvoke 方法檢索異步調用的結果。 在調用 BeginInvoke 以後隨時能夠調用該方法。 若是異步調用還沒有完成,則 EndInvoke 會一直阻止調用線程,直到異步調用完成

咱們試驗一下是否是這樣呢,運行以下代碼: 

複製代碼
private void btnAsync_Click(object sender, EventArgs e)
{
      this.txtAsyncResult.AppendText("老劉想洗澡了...\r\n");
      Heater heater = new Heater();
      heater.SetTemp = 85;
      LaoLiu laoliu = new LaoLiu(heater);
      this.txtAsyncResult.AppendText("老劉開始燒水...\r\n");
      IAsyncResult ar = laoliu.BeginBoilWater(null, null);
      this.txtAsyncResult.AppendText(laoliu.WatchTv());
      this.txtAsyncResult.AppendText(laoliu.ListenToSong());
      int curTemp = laoliu.EndBoilWater(ar);
      this.txtAsyncResult.AppendText("水燒好了...");
      this.txtAsyncResult.AppendText("當前水溫 " + curTemp.ToString() + "");
}
複製代碼

  結果以下:

在運行過程當中,咱們會發現 調用BeginBoilWater(內部實際上是BeginInvoke)以後,程序沒有發生阻塞,而是繼續執行老王去看電視,老劉去聽音樂去兩個方法,當執行到EndBoilWater(內部實際上是EndInvoke方法),因爲異步操做沒有完成,程序仍是會發生阻塞,直到異步調用完成,返回數據,這和MSDN的陳述也是同樣的。

有沒有什麼辦法能夠判斷異步是否完成了呢?固然了,這就須要用到 IAsyncResult接口中的屬性了。

首先咱們用IAsyncResult中的IsCompleted 屬性進行輪詢判斷是否完成,爲了時間短一些,我把Heater中加熱方法設置成 100 毫秒,咱們執行以下代碼:          

複製代碼
private void btnAsync_Click(object sender, EventArgs e)
{
   this.txtAsyncResult.AppendText("老劉想洗澡了...\r\n");
   Heater heater = new Heater();
   heater.SetTemp = 85;
   LaoLiu laoliu = new LaoLiu(heater);
   this.txtAsyncResult.AppendText("老劉開始燒水...\r\n");
   IAsyncResult ar = laoliu.BeginBoilWater(null, null);
   this.txtAsyncResult.AppendText(laoliu.WatchTv());
   this.txtAsyncResult.AppendText(laoliu.ListenToSong());
   int i = 0;
   //輪詢判斷異步是否完成
   while (!ar.IsCompleted)
   {
     i++;
     this.txtAsyncResult.AppendText(" " + i.ToString() + " ");
     if (ar.IsCompleted)
     {
       this.txtAsyncResult.AppendText("\r\n水燒好了...\r\n");
      }
    }
   int curTemp = laoliu.EndBoilWater(ar);        
   this.txtAsyncResult.AppendText("當前水溫 " + curTemp.ToString() + "");
}
複製代碼

 結果以下:

運行過程當中,程序沒有發生阻塞,咱們在while循環中一直不停的判斷ar.IsCompleted 狀態,並打印i的值,當i=83的時候,異步調用完成了,打印出來了最後的結果

第二種方法,使用 WaitHandle 等待異步調用。

MSDN解釋,使用 IAsyncResult.AsyncWaitHandle 屬性獲取 WaitHandle,使用其 WaitOne 方法阻止執行,直至 WaitHandle 收到信號,而後調用 EndInvoke。

不少人不理解,其實它就是個信號量,當使用其Waitone()方法的時候,程序就會發生阻塞,若是異步完成,Waitone就會返回true,不然返回false。固然最方便的就是咱們能夠在Waitone中設置一個時間來作超時處理,好比咱們能夠在 IAsyncResult ar = laoliu.BeginBoilWater(null, null); 代碼後增長ar.AsyncWaitHandle.WaitOne(2000),因爲,異步方法的線程須要5000ms,主線程等待了2000ms後,認爲是超時了,便會繼續執行後面老王看電視,老王聽音樂的代碼。

爲了好玩一些,咱們把熱水器燒水的方法修改一下,把Thread.Sleep(5000); 註釋掉,在for 循環中增長Thread.Sleep(50);循環環一次,等待50ms,完整代碼以下:

複製代碼
/// <summary>
/// 燒水
/// </summary>
public int BoilWater()
{
    //Thread.Sleep(5000);
    for (int i = 0; i <= 100; i++)
     {
        Thread.Sleep(50);
         _currentTemp = i;
         if (_currentTemp >= SetTemp)
          {
             _flag = true;
             break;
           }
       }
      return _currentTemp;
 }
複製代碼

 用WaitHandle中waitone來等待異步完成,咱們來讓看電視的的老劉,每隔一段時間去看一下水是否燒好,代碼以下:

複製代碼
private void btnAsync_Click(object sender, EventArgs e)
{
  this.txtAsyncResult.AppendText("老劉想洗澡了...\r\n");
  Heater heater = new Heater();
  heater.SetTemp = 85;
  LaoLiu laoliu = new LaoLiu(heater);
  this.txtAsyncResult.AppendText("老劉開始燒水...\r\n");
  IAsyncResult ar = laoliu.BeginBoilWater(null, null);
  this.txtAsyncResult.AppendText(laoliu.WatchTv());
  this.txtAsyncResult.AppendText(laoliu.ListenToSong());

  //WaitOne 做用 等待句柄
  bool flag = true;
  while (flag)
  {
     this.txtAsyncResult.AppendText(string.Format("老劉去看了一眼,水還沒燒好,當前水溫 {0} 度...\r\n", heater.CurrentTemp));
     flag = !ar.AsyncWaitHandle.WaitOne(1000);
   }
  this.txtAsyncResult.AppendText("水燒好了...\r\n");
  int curTemp = laoliu.EndBoilWater(ar);
  this.txtAsyncResult.AppendText("當前水溫 " + curTemp.ToString() + "");
}
複製代碼

 執行結果以下:

最後咱們來演示一下如何在異步中使用回調方法和用戶定義對象:

咱們來這樣作,咱們在主界面代碼中增長一個顯示燒水完成後在文本框顯示最終狀態的方法ActionCallBack(int curTemp),在老劉類中增長BoilWaterCallBack(IAsyncResult ar) 回調方法,獲取異步完成後的結果值。將ActionCallBack方法做爲用戶自定義對象進行傳遞到回調函數BoilWaterCallBack 中,在BoilWaterCallBack方法中 獲取ActionCallBack 方法,再進行回調,讓界面輸出結果。

在老王類中新增打開熱水器方法OpenHeater 和回調方法BoilWaterCallBack,代碼以下:

複製代碼
/// <summary>
/// 打開熱水器
/// </summary>
/// <param name="callback"></param>
public void OpenHeater(Action<int> callback)
{
    BeginBoilWater(BoilWaterCallBack, callback);
}

/// <summary>
/// 燒水結束後顯示當前的水溫
/// </summary>
/// <param name="ar"></param>
private void BoilWaterCallBack(IAsyncResult ar)
{
    Action<int> callback = ar.AsyncState as Action<int>;
    int curtemp = EndBoilWater(ar);
    callback(curtemp);
}
複製代碼

 在界面代碼中增長ActionCallBack方法:

複製代碼
public void ActionCallBack(int curTemp)
{
   this.txtAsyncResult.Invoke((MethodInvoker)
    (() =>
    {
         this.txtCallBack.AppendText("水燒好了...\r\n");
          this.txtCallBack.AppendText("當前水溫 " + curTemp.ToString() + "");
    }));
}
複製代碼

 因爲該方法會在異步線程中執行,所以文本框須要利用invoke方式來進行賦值操做。

在主界面中的異步回調按鈕的點擊事件中調用該代碼:

複製代碼
 private void btnCallBack_Click(object sender, EventArgs e)
{
    this.txtCallBack.AppendText("老劉想洗澡了...\r\n");
    Heater heater = new Heater();
    heater.SetTemp = 85;
    LaoLiu laoliu = new LaoLiu(heater);
    this.txtCallBack.AppendText("老劉開始燒水...\r\n");
    //老劉打開熱水器,而後去看電視了
    laoliu.OpenHeater(ActionCallBack);
    this.txtCallBack.AppendText(laoliu.WatchTv());
    this.txtCallBack.AppendText(laoliu.ListenToSong());
}
複製代碼

 代碼運行結果以下:

至此,這個例子就演示完了,不足之處望你們多多指教!例子代碼在此下載

做者: Rising Sun
本文版權歸做者和博客園共有,歡迎轉載,但未經做者贊成必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接,不然保留追究法律責任的權利.
相關文章
相關標籤/搜索