非同步程式設計:基於事件的非同步程式設計模式(EAP)

風靈使發表於2018-06-12

上一篇,我給大家介紹了“.NET1.0 IAsyncResult非同步程式設計模型(APM)”,通過Begin*** 開啟操作並返回IAsyncResult物件,使用 End*** 方法來結束操作,通過回撥方法來做非同步操作後其它事項。然而最大的問題是沒有提供進度通知等功能及多執行緒間控制元件的訪問。為克服這個問題(並解決其他一些問題),.NET2.0 中引入了:基於事件的非同步程式設計模式(EAP,Event-based Asynchronous Pattern)。通過事件、AsyncOperationManager類和AsyncOperation類兩個幫助器類實現如下功能:

1) 非同步執行耗時的任務。

2) 獲得進度報告和增量結果。

3) 支援耗時任務的取消。

4) 獲得任務的結果值或異常資訊。

5) 更復雜:支援同時執行多個非同步操作、進度報告、增量結果、取消操作、返回結果值或異常資訊。

對於相對簡單的多執行緒應用程式,BackgroundWorker元件提供了一個簡單的解決方案。對於更復雜的非同步應用程式,可以考慮實現一個符合基於事件的非同步模式的類。

原始碼下載:非同步程式設計:基於事件的非同步模型(EAP).rar

EAP非同步程式設計模型的優點

EAP是為Windows窗體開發人員建立的,其主要優點在於:

  1. EAPMicrosoft Visual Studio UI設計器進行了很好的整合。也就是說,可將大多數實現了EAP的類拖放到一個Visual Studio設計器平面上,然後雙擊事件名,讓Visual Studio自動生成事件回撥方法,並將方法同事件關聯起來。

  2. EAP類在內部通過SynchronizationContext類,將應用程式模型對映到合適執行緒處理模型,以方便跨執行緒操作控制元件。

為了實現基於事件的非同步模式,我們必須先理解兩個重要的幫助器類:

AsyncOperationManager和AsyncOperation

AsyncOperationManager類和AsyncOperation類是System.ComponentModel名稱空間為我們提供了兩個重要幫助器類。在基於事件的非同步模式封裝標準化的非同步功能中,它確保你的非同步操作支援在各種應用程式模型(包括 ASP.NET、控制檯應用程式和 Windows 窗體應用程式)的適當“執行緒或上下文”呼叫客戶端事件處理程式。

AsyncOperationManager類和AsyncOperation類的API如下:

// 為支援非同步方法呼叫的類提供併發管理。此類不能被繼承。
public static class AsyncOperationManager
{
    // 獲取或設定用於非同步操作的同步上下文。
    public static SynchronizationContext SynchronizationContext { get; set; }

    // 返回可用於對特定非同步操作的持續時間進行跟蹤的AsyncOperation物件。
    // 引數:userSuppliedState:
    //     一個物件,用於使一個客戶端狀態(如任務 ID)與一個特定非同步操作相關聯。
    public static AsyncOperation CreateOperation(object userSuppliedState)
    {
        return AsyncOperation.CreateOperation(userSuppliedState,SynchronizationContext);
    }
}

// 跟蹤非同步操作的生存期。
public sealed class AsyncOperation
{
    // 建構函式
    private AsyncOperation(object userSuppliedState, SynchronizationContext syncContext);
    internal static AsyncOperation CreateOperation(object userSuppliedState
                                            , SynchronizationContext syncContext);

    // 獲取傳遞給建構函式的SynchronizationContext物件。
    public SynchronizationContext SynchronizationContext { get; }
    // 獲取或設定用於唯一標識非同步操作的物件。
    public object UserSuppliedState { get; }

    // 在各種應用程式模型適合的執行緒或上下文中呼叫委託。
    public void Post(SendOrPostCallback d, object arg);
    // 結束非同步操作的生存期。
    public void OperationCompleted();
    // 效果同呼叫 Post() + OperationCompleted() 方法組合
    public void PostOperationCompleted(SendOrPostCallback d, object arg);
}

先分析下這兩個幫助器類:

  1. AsyncOperationManager是靜態類。靜態類是密封的,因此不可被繼承。倘若從靜態類繼承會報錯“靜態類必須從 Object 派生”。(小常識,以前以為密封類就是 sealed 關鍵字)

  2. AsyncOperationManager為支援非同步方法呼叫的類提供併發管理,該類可正常執行於 .NET Framework 支援的所有應用程式模式下。

  3. AsyncOperation例項提供對特定非同步任務的生存期進行跟蹤。可用來處理任務完成通知,還可用於在不終止非同步操作的情況下發布進度報告和增量結果(這種不終止非同步操作的處理是通過AsyncOperationPost() 方法實現)。

  4. AsyncOperation類有一個私有的建構函式和一個內部CreateOperation() 靜態方法。由AsyncOperationManager類呼叫AsyncOperation.CreateOperation() 靜態方法來建立AsyncOperation例項。

  5. AsyncOperation類是通過SynchronizationContext類來實現在各種應用程式的適當“執行緒或上下文”呼叫客戶端事件處理程式。

// 提供在各種同步模型中傳播同步上下文的基本功能。
public class SynchronizationContext
{
    // 獲取當前執行緒的同步上下文。
    public static SynchronizationContext Current { get; }

    // 當在派生類中重寫時,響應操作已開始的通知。
    public virtual void OperationStarted();
    // 當在派生類中重寫時,將非同步訊息排程到一個同步上下文。
    public virtual void Post(SendOrPostCallback d, object state);
    // 當在派生類中重寫時,響應操作已完成的通知。
    public virtual void OperationCompleted();
    ……
}

a) 在AsyncOperation建構函式中呼叫SynchronizationContextOperationStarted()

b) 在AsyncOperationPost() 方法中呼叫SynchronizationContextPost()
c) 在AsyncOperationOperationCompleted()方法中呼叫SynchronizationContextOperationCompleted()

  1. SendOrPostCallback委託簽名:
// 表示在訊息即將被排程到同步上下文時要呼叫的方法。
public delegate void SendOrPostCallback(object state);

基於事件的非同步模式的特徵

1.基於事件的非同步模式可以採用多種形式,具體取決於某個特定類支援操作的複雜程度:

1) 最簡單的類可能只有一個 ***Async方法和一個對應的 ***Completed 事件,以及這些方法的同步版本。

2) 複雜的類可能有若干個 ***Async方法,每種方法都有一個對應的 ***Completed 事件,以及這些方法的同步版本。

3) 更復雜的類還可能為每個非同步方法支援取消(CancelAsync()方法)、進度報告和增量結果(ReportProgress() 方法+ProgressChanged事件)。

4) 如果您的類支援多個非同步方法,每個非同步方法返回不同型別的資料,您應該:
a) 將您的增量結果報告與您的進度報告分開。
b) 使用適當的EventArgs為每個非同步方法定義一個單獨的 ***ProgressChanged事件以處理該方法的增量結果資料。

5) 如果類不支援多個併發呼叫,請考慮公開IsBusy屬性。

6) 如要非同步操作的同步版本中有 OutRef 引數,它們應做為對應 ***CompletedEventArgs的一部分,eg:

public int MethodName(string arg1, ref string arg2, out string arg3);

public void MethodNameAsync(string arg1, string arg2);
public class MethodNameCompletedEventArgs : AsyncCompletedEventArgs
{
    public int Result { get; };
    public string Arg2 { get; };
    public string Arg3 { get; };
}

2.如果你的元件要支援多個非同步耗時的任務並行執行。那麼:

1) 為***Async方法多新增一個userState物件引數(此引數應當始終是***Async方法簽名中的最後一個引數),用於跟蹤各個操作的生存期。

2) 注意要在你構建的非同步類中維護一個userState物件的集合。使用 lock 區域保護此集合,因為各種呼叫都會在此集合中新增和移除userState物件。

3) 在***Async方法開始時呼叫AsyncOperationManager.CreateOperation並傳入userState物件,為每個非同步任務建立AsyncOperation物件,userState儲存在AsyncOperationUserSuppliedState屬性中。在構建的非同步類中使用該屬性標識取消的操作,並傳遞給CompletedEventArgsProgressChangedEventArgs引數的UserState屬性來標識當前引發進度或完成事件的特定非同步任務。

4) 當對應於此userState物件的任務引發完成事件時,你構建的非同步類應將AsyncCompletedEventArgs.UserState物件從集合中刪除。

3.異常處理

EAP的錯誤處理和系統的其餘部分不一致。首先,異常不會丟擲。在你的事件處理方法中,必須查詢AsyncCompletedEventArgsException屬性,看它是不是null。如果不是null,就必須使用if語句判斷Exception派生物件的型別,而不是使用catch塊。

另外,如果你的程式碼忽略錯誤,那麼不會發生未處理的異常,錯誤會變得未被檢測到,應用程式將繼續執行,其結果不可預知。

4.注意:

1) 確保 ***EventArgs類特定於***方法。即當使用 ***EventArgs類時,切勿要求開發人員強制轉換型別值。

2) 確保始終引發方法名稱Completed事件。成功完成、異常或者取消時應引發此事件。任何情況下,應用程式都不應遇到這樣的情況:應用程式保持空閒狀態,而操作卻一直不能完成。

3) 確保可以捕獲非同步操作中發生的任何異常並將捕獲的異常指派給 Error 屬性。

4) 確保 ***CompletedEventArgs 類將其成員公開為只讀屬性而不是欄位,因為欄位會阻止資料繫結。eg:public MyReturnType Result { get; }

5) 在構建 ***CompletedEventArgs 類屬性時,通過this.RaiseExceptionIfNecessary() 方法確保屬性值被正確使用。Eg:

private bool isPrimeValue;
public bool IsPrime
{
    get
    {
        RaiseExceptionIfNecessary();
        return isPrimeValue;
    }
}

所以,在***Completed事件處理程式中,應當總是先檢查 ***CompletedEventArgs.Error***CompletedEventArgs.Cancelled 屬性,然後再訪問RunWorkerCompletedEventArgs.Result屬性。

BackgroundWorker元件

System.ComponentModel名稱空間的BackgroundWorker元件為我們提供了一個簡單的多執行緒應用解決方案,它允許你在單獨的執行緒上執行耗時操作而不會導致使用者介面的阻塞。但是,要注意它同一時刻只能執行一個非同步耗時操作(使用IsBusy屬性判定),並且不能跨AppDomain邊界進行封送處理(不能在多個AppDomain中執行多執行緒操作)。

1.BackgroundWorker元件

public class BackgroundWorker : Component
{
    public BackgroundWorker();

    // 獲取一個值,指示應用程式是否已請求取消後臺操作。
    public bool CancellationPending { get; }
    // 獲取一個值,指示BackgroundWorker是否正在執行非同步操作。
    public bool IsBusy { get; }
    // 獲取或設定一個值,該值指示BackgroundWorker能否報告進度更新。
    public bool WorkerReportsProgress { get; set; }
    // 獲取或設定一個值,該值指示BackgroundWorker是否支援非同步取消。
    public bool WorkerSupportsCancellation { get; set; }

    // 呼叫RunWorkerAsync() 時發生。
    public event DoWorkEventHandlerDoWork;
    // 呼叫ReportProgress(System.Int32) 時發生。
    public event ProgressChangedEventHandlerProgressChanged;
    // 當後臺操作已完成、被取消或引發異常時發生。
    public event RunWorkerCompletedEventHandlerRunWorkerCompleted;

    // 請求取消掛起的後臺操作。
    public void CancelAsync();
    // 引發ProgressChanged事件。percentProgress:範圍從 0% 到 100%
    public void ReportProgress(int percentProgress);
    // userState:傳遞到RunWorkerAsync(System.Object) 的狀態物件。
    public void ReportProgress(int percentProgress, object userState);
    // 開始執行後臺操作。
    public void RunWorkerAsync();
    // 開始執行後臺操作。argument:傳遞給DoWork事件的DoWorkEventArgs引數。
    public void RunWorkerAsync(object argument);
}

2.相應的EventArgs

///1)   System.EventArgs基類
    // System.EventArgs是包含事件資料的類的基類。
    public class EventArgs
    {
        // 表示沒有事件資料的事件。
        public static readonly EventArgs Empty;
        public EventArgs(); 
    }

///2)   DoWorkEventArgs類
    // 為可取消的事件提供資料。
    public class CancelEventArgs : EventArgs
    {
        public CancelEventArgs();
        public CancelEventArgs(bool cancel);
        // 獲取或設定指示是否應取消事件的值。
        public bool Cancel { get; set; }
    }
    // 為DoWork事件處理程式提供資料。
    public class DoWorkEventArgs : CancelEventArgs
    {
        public DoWorkEventArgs(object argument);

        // 獲取表示非同步操作引數的值。
        public object Argument { get; }
        // 獲取或設定表示非同步操作結果的值。
        public object Result { get; set; }
    }

///3)   ProgressChangedEventArgs類
    // 為ProgressChanged事件提供資料。
    public class ProgressChangedEventArgs : EventArgs
    {
        public ProgressChangedEventArgs(int progressPercentage, object userState);

        // 獲取非同步任務的進度百分比。
        public int ProgressPercentage { get; }
        // 獲取唯一的使用者狀態。
        public object UserState { get; }
    }

///4)   RunWorkerCompletedEventArgs類
    // 為MethodNameCompleted事件提供資料。
    public class AsyncCompletedEventArgs : EventArgs
    {
        public AsyncCompletedEventArgs();
        public AsyncCompletedEventArgs(Exception error, bool cancelled, object userState);

        // 獲取一個值,該值指示非同步操作是否已被取消。
        public bool Cancelled { get; }
        // 獲取一個值,該值指示非同步操作期間發生的錯誤。
        public Exception Error { get; }
        // 獲取非同步任務的唯一識別符號。
        public object UserState { get; }

        // 訪問 AsyncCompletedEventArgs 及其派生類的屬性前呼叫此方法
        protected void RaiseExceptionIfNecessary()
        {
            if (this.Error != null)
            {
                throw new TargetInvocationException(……);
            }
            if (this.Cancelled)
            {
                throw new InvalidOperationException(……);
            }
        }
    }
    public class RunWorkerCompletedEventArgs : AsyncCompletedEventArgs
    {
        public RunWorkerCompletedEventArgs(object result, Exception error, bool cancelled);

        // 獲取表示非同步操作結果的值。
        public object Result { get; }
        // 獲取表示使用者狀態的值。
        public object UserState { get; }
    }

3.BackgroundWorker示例

示例程式碼中包含了BackgroundWorker原始碼及對應的使用示例,這裡不貼上程式碼了,會導致篇幅更大。來個示例截圖吧:

image

示例分析:

1) 首先我們為BackgroundWorker元件註冊DoWork(非同步操作)、ProgressChanged(進度報告) 和RunWorkCompleted(完成通知)事件;

2) 設定WorkerSupportsCancellation和WorkerReportsProgress屬性為true,以宣告元件支援取消操作和進度報告;

3) 使用RunWorkerAsync() 開啟非同步操作,通過IsBusy屬性判斷是否已經有非同步任務在執行;

4) 使用CancelAsync() 方法取消非同步操作,但要注意:

a) 它僅僅是將BackgroudWorker.CancellationPending屬性設定為true。需要在具體DoWork事件中不斷檢查BackgroudWorker.CancellationPending來設定DoWorkEventArgs的Cancel屬性。
b) DoWork事件處理程式中的程式碼有可能在發出取消請求時完成其工作,輪詢迴圈可能會錯過設定為 true 的CancellationPending屬性。在這種情況下,即使發出了取消請求,RunWorkerCompleted事件處理程式中RunWorkerCompletedEventArgs的 Cancelled 標誌也不會設定為 true。這種情況被稱作爭用狀態。(可以通過直接監控元件的CancellationPending屬性,來做判斷)
5) 確保在DoWork事件處理程式中不操作任何使用者介面物件。而應該通過ProgressChanged和RunWorkerCompleted事件與使用者介面進行通訊。

因為RunWorkerAsync() 是通過委託的BeginInvoke() 引發的DoWork事件,即DoWork事件的執行執行緒已不是建立控制元件的執行緒(我在《非同步程式設計:非同步程式設計模型 (APM)》中介紹了幾種誇執行緒訪問控制元件的方式)。而ProgressChanged和RunWorkerCompleted事件是通過幫助器類AsyncOperation的 Post() 方法使其呼叫發生在合適的“執行緒或上下文”中。

自定義基於事件的非同步元件

剛才我們介紹了BackgroundWorker元件,但是這個元件在一個時刻只能開啟一個非同步操作,那如果我們要想同時支援多個非同步操作、進度報告、增量結果、取消和返回結果值或異常資訊該怎麼辦呢?對的,我們可以為自己定義一個基於事件的非同步元件。

我直接引用MSDN上的一則計算質數的非同步元件示例,請從我提供的示例程式碼中獲取。

質數演算法:埃拉托色尼篩法

eg:判斷n是否為質數

1、1和0既非素數也非合數;

2、將2和3加入質數集合primes;從n=5開始,通過 n+=2 來跳過所有偶數;

3、迴圈集合primes中的質數並將其做為n的因子,能整除的為合數;

4、若不能整除,則繼續循步驟3直到“因子的平方>n”,即可判斷n為質數,並將其加入到集合primes。

來個示例截圖吧:

image

示例分析:(元件名:PrimeNumberCalculator

  1. 首先我們為PrimeNumberCalculator元件註冊ProgressChanged(進度報告) 和CalculatePrimeCompleted(完成通知)事件;

  2. 使用CalculatePrimeAsync(intnumberToTest, object taskId)開啟非同步任務,注意我們需要傳遞一個唯一標識Guid taskId = Guid.NewGuid();用於標識取消的操作,並傳遞給CompletedEventArgsProgressChangedEventArgs引數的UserState屬性來標識當前引發進度或完成事件的特定非同步任務;

  3. 取消操作CancelAsync(object taskId),只是將taskId對應的AsyncOperation例項移除內部任務集合,耗時操作通過判斷taskId是否存在於集合來判斷其是否被取消;

此文到此結束,通過此博文我們認識到:

1) 基於事件的非同步程式設計是通過AsyncOperationManager類和AsyncOperation類兩個幫助器類確保你的非同步操作支援在各種應用程式模型(包括 ASP.NET、控制檯應用程式和 Windows 窗體應用程式)的適當“執行緒或上下文”呼叫訪問控制元件;

2) BackgroundWorker元件構建、使用和缺點。

3) 展現如何構建一個基於事件的非同步元件,並且支援多個非同步操作的並行執行

相關文章