[C#] 將同步方法改寫為非同步方法

[C#] 將同步方法改寫為非同步方法

前言

當我們遇到所需要花費較長時間的程式時,

我們通常會透過非同步的方式來呼叫它 (比如讀取資料流的BeginRead, EndRead),

那如果本來是同步的方法,我們有沒有辦法可以改寫它呢?

這篇的主題就是要來介紹如何將同步(sync)的程式改寫為非同步(async)的程式。

 

實際演練

在.Net Framework中,非同步模型(APM)大致上可以區分為兩類,

基於IAsyncResult的非同步模型,以及Event Based的非同步模型,

我們先來建立一個很簡單的加法函式,並且為它加入Thread.Sleep來模擬花費大量的時間,


public class Calculator
{
        public int Calculate(int a, int b)
        {
            // 模擬花費大量時間運算
            System.Threading.Thread.Sleep(10 * 1000);

            return a + b;
        }
}

通常在程式中執行此函式時看起來會像這樣,程式會卡在這邊非常久

Calculator calculator = new Calculator();
// 程式會停在這邊很長的時間
int result = calculator.Calculate(1, 2);

接下來,將介紹如何將這支函式改寫為可用非同步的方式呼叫
 

1. 改寫為基於IAsyncResult的非同步模型

首先,我們必須要先建立一個與函式Input和Output相同的delegate,

因為在3.5之後的版本已經有內建Func,所以在今天的範例就直接使用Func來當作delegate,


//In 2.0, sample, not work
//private delegate int CalculateDelegate(int a,int b);
//CalculateDelegate m_calculateDelegate = new CalculateDelegate(Calculate);
private Func<int, int, int> m_calculateDelegate;

之後我們可以仿照資料流讀取的BeginRead和EndRead一樣,分別撰寫BeginCalculate和EndCalculate方法,


public IAsyncResult BeginCalculate(int a, int b)
{
    this.m_calculateDelegate = this.Calculate;

    return this.m_calculateDelegate.BeginInvoke(a, b, null, null);
}

public int EndCalculate(IAsyncResult asyncResult)
{
    return this.m_calculateDelegate.EndInvoke(asyncResult);
}

如此一來,我們就可以在程式中使用非同步的方法呼叫BeginCalculate和EndCalculate,

並且可以在程式計算完畢之前,繼續做其他的事情,


Calculator calculator = new Calculator();
IAsyncResult asyncResult = calculator.BeginCalculate(1, 2);

// Do something else

int result = calculator.EndCalculate(asyncResult);

 2. 改寫為Event Based的非同步模型 


Event Based的非同步模型,在執行完畢之後會將主控權交還給程式,

不會阻塞目前的Thread,一直到執行完畢之後,才會觸發註冊的事件。

首先我們建立一個當執行完畢時會觸發的事件,並且會傳入具有計算結果的EventArgs

public event EventHandler<CalculateFinishedEventArgs> CalculateFinished;

public class CalculateFinishedEventArgs : EventArgs
{
    public int Result { get; private set; }

    public CalculateFinishedEventArgs(int result)
    {
        this.Result = result;
    }
}

再來我們建立一個供程式呼叫的方法CalculateAsync,在執行之後並不會阻塞Thread,

並且會在執行完畢之後,觸發CalculateFinished事件,


public void CalculateAsync(int a, int b)
{
      this.m_calculateDelegate = Calculate;

      AsyncCallback callback = new AsyncCallback(OnCalculateFinished);
      this.m_calculateDelegate.BeginInvoke(a, b, callback, m_calculateDelegate);
}

private void OnCalculateFinished(IAsyncResult asyncResult)
{
      Func calculateDelegate = (Func)asyncResult.AsyncState;

      int result = calculateDelegate.EndInvoke(asyncResult);

      EventHandler temp = CalculateFinished;
      if (temp != null)
      {
           temp(this, new CalculateFinishedEventArgs(result));
      }
}

執行程式就可改寫如下


Calculator calculator = new Calculator();         

calculator.CalculateFinished += (o, args) => MessageBox.Show(string.Format("Result is {0}", args.Result));
calculator.CalculateAsync(1, 2);

// Do other works...

 

結語

為了有效的利用系統資源,並行作業,以及提供使用者良好的使用者體驗,

在撰寫程式時使用非同步的方法呼叫是不可或缺的,

但在撰寫非同步程式的同時,卻往往會造成程式碼較為零碎,

而比較不容易閱讀和維護的困擾,在下次的文章之中,

會來介紹如何使用Iterator來改善這種情況,

當然也可以利用之前介紹的非同步呼叫方法並跳出處理中視窗

透過BackgroundWorker或是Thread的方式來處理,

也歡迎大家一起多多討論和指教囉 ^_^

 

參考資料:

1. http://msdn.microsoft.com/zh-tw/library/2e08f6yc(v=VS.100).aspx 以非同步的方式呼叫同步方法

2. http://msdn.microsoft.com/zh-tw/magazine/cc163467.aspx 實作 CLR 非同步程式撰寫模型