也來說說C#異步委托


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

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

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

EndInvoke 方法檢索異步調用的結果。 在調用 BeginInvoke 之后隨時可以調用該方法。 如果異步調用尚未完成,則 EndInvoke 會一直阻止調用線程,直到異步調用完成。 EndInvoke 的參數包括您需要異步執行的方法的 out 和 ref 參數(在 Visual Basic 中為 <Out> ByRef 和 ByRef)以及由 BeginInvoke 返回的 IAsyncResult。

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

public interface IAsyncResult
{
     object AsyncState { get; }

     WaitHandle AsyncWaitHandle { get; }

     bool CompletedSynchronously { get; }

     bool IsCompleted { get; }
}

 IAsyncResult 類型公開以下成員:

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

 如果上面的介紹看不明白,沒有關系,下面來通過一個例子來進行演示,您一定會搞清晰明白的,先看一下程序主界面圖,以便后面的代碼說明較好理解。 

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

Heater熱水器代碼
public class Heater
    {
        /// <summary>
        /// 設定的溫度
        /// </summary>
        public int SetTemp { get; set; }

        /// <summary>
        /// 當前水溫
        /// </summary>
        private int _currentTemp;
        public int CurrentTemp
        {
            get { return _currentTemp; }
        }

        private bool _flag;
        public bool Flag
        {
            get { return _flag; }
            
        }

        public Heater()
        {
            this._flag = false;
        }

        /// <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;
        }
    }

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

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

老王代碼
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());
}

 代碼運行結果如下:

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


免責聲明!

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



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