異步編程:基於事件的異步編程模式(EAP)


傳送門:異步編程系列目錄……

上一篇,我給大家介紹了“.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類,將應用程序模型映射到合適線程處理模型,以方便跨線程操作控件。

 

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

 

AsyncOperationManagerAsyncOperation

    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實例提供對特定異步任務的生存期進行跟蹤。可用來處理任務完成通知,還可用於在不終止異步操作的情況下發布進度報告和增量結果(這種不終止異步操作的處理是通過AsyncOperation Post() 方法實現)

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)       AsyncOperation Post() 方法中調用SynchronizationContextPost()

c)   AsyncOperationOperationCompleted()方法中調用SynchronizationContextOperationCompleted()

6.   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)   如要異步操作的同步版本中有 Out Ref 參數,它們應做為對應 ***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)   設置WorkerSupportsCancellationWorkerReportsProgress屬性為true,以聲明組件支持取消操作和進度報告;

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

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

a)   它僅僅是將BackgroudWorker.CancellationPending屬性設置為true。需要在具體DoWork事件中不斷檢查BackgroudWorker.CancellationPending來設置DoWorkEventArgsCancel屬性。

b)   DoWork事件處理程序中的代碼有可能在發出取消請求時完成其工作,輪詢循環可能會錯過設置為 true CancellationPending屬性。在這種情況下,即使發出了取消請求,RunWorkerCompleted事件處理程序中RunWorkerCompletedEventArgs Cancelled 標志也不會設置為 true。這種情況被稱作爭用狀態。(可以通過直接監控組件的CancellationPending屬性,來做判斷)

5)   確保在DoWork事件處理程序中不操作任何用戶界面對象。而應該通過ProgressChangedRunWorkerCompleted事件與用戶界面進行通信。

因為RunWorkerAsync() 是通過委托的BeginInvoke() 引發的DoWork事件,即DoWork事件的執行線程已不是創建控件的線程(我在異步編程:異步編程模型 (APM)中介紹了幾種誇線程訪問控件的方式)。而ProgressChangedRunWorkerCompleted事件是通過幫助器類AsyncOperation Post() 方法使其調用發生在合適的“線程或上下文”中。

 

自定義基於事件的異步組件

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

    我直接引用MSDN上的一則計算質數的異步組件示例,請從我提供的示例代碼中獲取

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

eg:判斷n是否為質數

110既非素數也非合數;

2、將23加入質數集合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)   展現如何構建一個基於事件的異步組件,並且支持多個異步操作的並行運行

 

感謝大家的觀賞,如本文對你有幫助還請多幫推薦支持下……

 

 

 

參考:MSDN

             書籍:《CLR via C#(第三版)


免責聲明!

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



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