2012年2月1日 星期三

非同步程式設計

非同步作業通常是用來執行可能需要很長時間才會完成的工作,例如,開啟大型檔案、連接到遠端電腦或查詢資料庫。 要設計非同步作業,除了自行利用 Thread 物件增加執行管道外,在 .NET 中已提供幾種設計模式來快速達到非同步作業的目的。

非同步程式設計

在.Net Framework裡,有許多類別都有提供 BeginXXXEndXXX 的方法來支援 APM。 例如:FileStream 的 BeginRead() 與 EndRead() 方法。 BeginXXX 會開始進行非同步作業,並回傳一個 IAsyncResult 介面的物件,可以用來判斷非同步作業非 (Asynchronous Operation) 的狀態。

上面提到過,總不能當非同步作業的執行緒執行到一半,就把結果拿來用。那該如何判斷,才知道非同步的呼叫已經執行完成了呢? 這時候就會用到會合點的技巧(Rendezvous Models)。

APM有下列三種程式設計方式來處理非同步呼叫之後的動作:等待完成、輪詢、回呼。

等待模式模型 (Wait-Until-Done Model)

這個模型的做法是,在啟動 APM 執行非同步作業後,主程序依然可以繼續執行其他的工作。 若主程序結束了其他的工作之後,可以試著呼叫結束 APM 的動作。 若非同步作業中的執行緒還在執行,則主程序就會 block 在此一直等待,直到非同步作業完成為止。

//等待模式:Wait until down model

byte[] buffer = new byte[100];
string filename = string.Concat(Environment.SystemDirectory, "\\mfc71.pdb");

// 使用APM開啟檔案資料流  (FileOptions 設定為 Asynchronous,表示以非同步讀取)
FileStream stream = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read, 1024, FileOptions.Asynchronous);

// 啟動非同步呼叫讀取檔案
IAsyncResult result = stream.BeginRead(buffer, 0, buffer.Length, null, null);

// 因為使用非同步呼叫,所以既使檔案還沒讀取結束,程式也會繼續下一行
// 所以等待階段,這裡可以繼續執行程式碼
// TO DO ....

// 呼叫 EndRead:在此進行等待。這行程式會被鎖住,直到非同步工作結束。
int numBytes = stream.EndRead(result);

stream.Close();     // close the stream

Console.WriteLine("Read {0} Bytes", numBytes);
Console.WriteLine(BitConverter.ToString(buffer));

輪詢模型 (Polling Model)

藉由不停的詢問 IAsyncResult 的狀態,以判斷非同步作業是否已完成。

byte[] buffer = new byte[100];
string filename = string.Concat(Environment.SystemDirectory, "\\mfc71.pdb");

// 使用APM開啟檔案資料流  (FileOptions 設定為 Asynchronous,表示以非同步讀取)
FileStream strm = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read, 1024, FileOptions.Asynchronous);

// 啟動非同步呼叫讀取檔案
IAsyncResult result = strm.BeginRead(buffer, 0, buffer.Length, null, null);

// 不停的輪詢,測試看看是否工作結束
while (!result.IsCompleted)
{
    // TO DO ....
    Application.DoEvents();
}

// 因為 IAsyncResult 狀態已經 Completed, EndRead 肯定不會被 block 住。
int numBytes = strm.EndRead(result);

strm.Close();       // close the stream
Console.WriteLine("Read {0} Bytes", numBytes);
Console.WriteLine(BitConverter.ToString(buffer));

回呼模型 (Callback Model)

上面方法,使用不停詢問的方式判斷非同步作業的狀態,技巧上似乎不怎麼有效率。 有時候可以使用回呼這個模型,它必須指定一個回呼方法給 AsyncCallback 委派,等到非同步作業完成,程式就會自動進入這個方法。

現有物件的回呼模型

下面例子,我們使用 FileStream 物件,執行回呼模式的非同步作業讀取作業。

public override IAsyncResult BeginRead(byte[] array, int offset, int numBytes, AsyncCallback userCallback, object stateObject);
[Serializable]
[ComVisible(true)]
public delegate void AsyncCallback(IAsyncResult ar);
private void button8_Click(object sender, EventArgs e)
{
    string filename = string.Concat(Environment.SystemDirectory, "\\mfc71.pdb");

    // 使用APM開啟檔案資料流  (Asynchronous:以非同步讀取)
    FileStream strm = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read, 1024, FileOptions.Asynchronous);

    // 這裡建立了一個 AsyncCallback 委派,並指定該執行緒結束時所要呼叫的方法。
    AsyncCallback callback = new AsyncCallback(CompleteRead);

    // 啟動非同步呼叫讀取檔案,
    IAsyncResult result = strm.BeginRead(buffer, 0, buffer.Length, callback, strm); //傳入自訂物件 strm
}
static void CompleteRead(IAsyncResult result)
{
    Console.WriteLine("Read Completed");
    FileStream strm = (FileStream)result.AsyncState;    //由AsyncState屬性,取得自訂的物件。

    // 因為 IAsyncResult 狀態已經 Completed, EndRead 肯定不會被 block 住。
    int numBytes = strm.EndRead(result);

    strm.Close();       // close the stream
    Console.WriteLine("Read {0} Bytes", numBytes);
    Console.WriteLine(BitConverter.ToString(buffer));
}

上面這個例子中的 BeginRead 方法中的最後一個參數,我們放入了 stream 物件,目的是希望傳給 CompleteRead 方法,讓它可以 close stream。 而 CompleteRead 是 AsyncCallback 委派的方法,必須使用相同簽名,所以僅接收 IAsyncResult 做為參數。 若要取得傳入的 stream 物件,則可以透過 IAsyncResult.AsyncState 屬性,以取得使用者定義的物件。

自訂物件的回呼模型

下面這個例子,假設 GetFileContents 方法是一個耗時的方法,用來讀取檔案並回傳內容。 若想要使用回呼模式的非同步執行,我們可以宣告一個委派,然後叫用它的 BeginInvoke 方法。

底下程式碼示範如何使用回呼方式,執行非同步作業,並取得執行結果。

public delegate string AsyncGetFileContents(string FileName);

private void btnBeginInvoke_Click(object sender, EventArgs e)
{
    string sFileName = "";

    AsyncGetFileContents GetFielContent = new AsyncGetFileContents(BeginGetFileContents);
    AsyncCallback callback = new AsyncCallback(EndGetFileContents);
    IAsyncResult result = GetFielContent.BeginInvoke(sFileName, callback, GetFielContent);
}

private string BeginGetFileContents(string FileName)
{
    // time consuming job.
    Thread.Sleep(1000*5);
    return "abc";
}
private void EndGetFileContents(IAsyncResult result)
{
    AsyncGetFileContents d = (AsyncGetFileContents)result.AsyncState;    //由AsyncState屬性,取得自訂的物件。
    string sContent = d.EndInvoke(result);
    Console.WriteLine(sContent);
}

APM 的例外狀況

若非同步執行的工作發生例外,一般來講,這些例外都可以在呼叫 EndXXX 方法時捕捉到。如果要捕捉例外狀況,可改成以下程式碼

int numBytes = 0;
try
{
    numBytes = strm.EndRead(result);
}
catch (IOException)
{
    Console.Write("An IO Exception occurred");
}

沒有留言:

張貼留言