.NET - 基於事件的異步模型

注:這是大概四年前寫的文章了。並且我離開.net領域也有四年多了。原本不想再發表,可是這其實是Active Object模式在.net中的一種重要實現方法,所以我把它掏出來發佈一下。若是該模型有新的發展,望在評論中幫給出一個引用,以便其它讀者知曉。感激涕零html

  基於事件的異步模型其實是MSDN中講解異步編程時所提供的一個章節。但在閱讀這些章節時,我以爲MSDN在一開始就將全部組成所有列出,而後再聯繫到一塊兒的講解次序並不適合咱們的思惟方式。所以在本文中,我將按照從易到難的方式逐步對該異步模型進行講解。編程

  另外,咱們對模型的討論將大量使用兩個名詞:用戶代碼及非用戶代碼。在本文中,它們分別表明使用該模型所暴露接口的代碼以及模型的內部實現。多線程

 

模型簡介異步

  您首先須要明白的一點是,該異步模型並非在爲您提供一種新的多線程編程技術,而是爲您提供了一些多線程編程規範方面的建議,從而爲您的多線程解決方案提供更良好更靈活的接口和內部組織。就其實現而言,基於事件的異步模型其實是對其內部所包含的異步操做的一個包裝。而按照該規範組織的異步調用對於用戶代碼而言則意義很是明確:若是須要以異步方式調用一個操做,並在操做完成後接到相應的通知,而不須要和工做項進行進一步的通信,那麼能夠直接調用一個函數並偵聽一個事件便可。甚至咱們更能夠經過偵聽報告工做進度的事件來獲得工做項進度更新的信息。async

  首先咱們來看看異步模型的使用。假設咱們有一個異步模型EventAsyncModel,而其擁有一個能夠被異步執行的任務SomeTask:異步編程

 1 void StartTask(EventAsyncModel asyncModel)
 2 {
 3     asyncModel.SomeTaskCompleted += OnSomeTaskCompleted;
 4     asyncModel.SomeTaskProgressChanged += OnSomeTaskProgressChanged;
 5 
 6     asyncModel.SomeTaskAsync();
 7 }
 8 
 9 void OnSomeTaskProgressChanged(…)
10 {
11 12 }
13 
14 void OnSomeTaskCompleted(…)
15 {
16 17 }

  上面的代碼展現了用戶代碼如何對基於事件的異步模型進行使用:用戶經過一個成員函數SomeTaskAsync()插入一個工做項,並在工做項狀態發生變化時發出一系列事件,如任務的進度變化的SomeTaskProgressChanged或完成的SomeTaskCompleted。您能夠看到,這些成員的名稱經常與您所須要插入的工做項的名稱相關。函數

 

  同時也能夠看出,在基於事件的異步模型中,對多線程部分的處理被所有置於模型內部,而僅僅暴露按照同步調用方式使用所須要的各個接口。也就是說,對於該模型的用戶代碼而言,其僅僅須要按照普通的單線程處理邏輯調用該模型所提供的接口便可,而不須要考慮有關多線程的任何問題。this

 

模型接口spa

  在大概瞭解了基於事件的異步模型以後,讓咱們先從模型的表觀特徵,模型的接口開始提及。通過這一節,相信您能瞭解該異步模型所提供的各個成員的確切使用方式。.net

  首先咱們來說講異步操做的啓動函數。對於一個須要轉化爲異步操做的同步方法,咱們經常須要按照必定的步驟將其轉化爲異步調用。

  第一步要轉化的是函數名稱。在同步方法的轉化過程當中,咱們經常須要對函數的名稱進行更改。更改後的函數名稱一方面能夠提供較爲明顯的「這是使用異步模型的成員函數」的提示,更能夠令其與同步函數同時存在。通常狀況下,異步函數會在相應的同步函數名以後添加-Async後綴。例如對於同步函數SomeTask(),咱們須要提供異步函數SomeTaskAsync()。

  在肯定了異步函數的名稱以後,咱們要轉化的就是函數的參數。一個異步函數所使用的參數應與相應同步函數所使用的參數相同。例如對於同步函數SomeTask(string parameters),異步函數的簽名應爲SomeTaskAsync(string parameters)。

  該過程當中較爲例外的是out和ref參數。若是同步調用中包含一個out參數,那麼它將存在於表示返回值的組成中,即異步模型的Complete事件中。而若是同步調用中包含一個ref參數,那麼它須要同時存在於異步函數的參數列表以及表示返回值的組成中。例如對於同步函數SomeTask(string parameters, out int result),轉化後的異步調用則爲SomeTaskAsync(string parameters),同時Complete事件所傳回的參數將帶有參數result。一樣地,對於同步函數SomeTask(string parameters, ref int result),轉化後的異步調用應爲SomeTaskAsync(string parameters, int result),同時Complete事件所傳回的參數將帶有參數result。

  最後要說的是返回值。異步調用的返回值通常爲void,這是由於它在函數調用返回時尚未獲得工做項的最終執行結果。

  既然異步調用並不將執行結果經過返回值傳遞,那返回值應從哪裏獲得呢?答案是工做項的完成消息。對於一個須要返回執行結果的異步操做,軟件開發人員經常須要從AsyncCompletedEventArgs派生,並將表示執行結果的成員添加到該派生類中。在異步工做項執行完畢之後,AsyncCompletedEventArgs類的派生類將被Completed事件返回,並在派生類中記錄工做項的執行結果。例如對於一個返回類型爲int的同步函數int SomeTask(),咱們首先須要建立AsyncCompletedEventArgs的派生類:

 1 public class SomeTaskCompletedArgs : AsyncCompletedEventArgs
 2 {
 3     public SomeTaskCompletedArgs(int result, Exception error, bool cancelled, object userState)
 4         : base(error, cancelled, userState)
 5     {
 6         _result = result;
 7     }
 8 
 9     public int result
10     {
11         get { return _result; }
12     }
13 
14     int _result;
15 }

                另外,基於事件的異步模式經常提供了彙報進度的事件。該事件的名稱經常由異步模式的實現類是否具備多個異步操做來決定。對於一個具備多個異步操做的實現類來講,函數SomeTaskAsync()所對應的事件名應爲SomeTaskProgressChanged;而對於只有一個異步操做的實現類,該事件的名稱應爲ProgressChanged。這些事件所返回的ProgressChangedEventArgs帶有一個屬性ProgressPercentage,以容許用戶代碼根據當前的進度更新滾動條等UI組成。

 

異步模型的多調用

  異步模型中的另外一個很是重要的概念就是多調用:異步操做的實現通常來講分爲兩種方式:單調用和多調用。單調用在當前工做項沒有完成時不容許再次執行,而多調用則容許多個工做項同時在後臺執行。用戶代碼經常須要經過方法的簽名區分這兩種方式的函數異步操做:多調用接口經常提供了一個名爲userState的額外參數。咱們將在後面的章節中詳細介紹該參數。

  接下來要轉化的是函數的參數。異步函數的參數將根據其是否支持多調用而略有不一樣。若是一個異步函數僅僅是一個單調用函數,那麼該函數所使用的參數應與相應同步函數所使用的參數相同。若是異步函數支持多調用,那麼該函數須要在相應同步函數所使用的參數以後添加一個object類型的userState實例做爲參數。例如對於同步函數SomeTask(string parameters),不支持多調用的異步函數應爲SomeTaskAsync(string parameters),而支持多調用的異步函數應爲SomeTaskAsync(string parameters, object userState)。

  該過程當中較爲例外的是out和ref參數。若是同步調用中包含一個out參數,那麼它將存在於表示返回值的組成中,如Complete事件中。而若是同步調用中包含一個ref參數,那麼它須要同時存在於異步函數的參數列表以及表示返回值的組成中。例如對於同步函數SomeTask(string parameters, out int result),轉化後的僅支持單調用的異步調用則爲SomeTaskAsync(string parameters),同時Complete事件所傳回的參數將帶有參數result。一樣地,對於同步函數SomeTask(string parameters, ref int result),轉化後的支持單調用的異步調用應爲SomeTaskAsync(string parameters, int result),同時Complete事件所傳回的參數將帶有參數result。

  在介紹多調用和單調用時,咱們提到了參數userState。實際上,對參數userState的使用貫穿了整個基於事件的異步模型的實現中。從插入工做項到工做項完成,工做項取消以及工做項進度更新,該參數都會附加在相應的函數調用或事件參數中,從而容許模型的用戶代碼瞭解究竟是哪一個工做項發生了變化,即要求userState在整個異步模型的使用過程當中是惟一的,能惟一標明工做項。

  也正是因爲這種惟一性要求,傳入多調用接口的userState對象須要用戶代碼自行生成。通常狀況下,用戶須要自行提供對該參數的管理,如保證userState對象不會重複等等。而在模型的內部實現中,您經常須要將userState對象添加到一個集合中。而在執行完畢後,您須要從該集合中檢索該對象,執行相應的處理邏輯,並最終將其從集合中刪除。

  基於事件的異步模型經常須要執行對異步函數所插入的工做項的取消,而執行取消功能的函數則須要根據異步調用是否支持多調用以及表示異步模型的類型中是否僅有一個支持取消的異步操做。對於異步操做SomeTaskAsync(),取消工做項的各函數名將以下表所示:

 

支持多調用

不支持多調用

類只包含一個異步操做

void SomeTaskAsyncCancel(object userState)

void SomeTaskAsyncCancel()

類包含多個異步操做

void AsyncCancel(object userState)

void AsyncCancel()

  對於一個模型中所包含的多個異步函數,您能夠根據上表所列出的轉化方式依次執行轉化。最終的轉化結果可能包含上表列出的兩個函數。例如對於模型中的支持多調用的函數TaskAsyncA()和不支持多調用的函數TaskAsyncB(),模型中將同時出現AsyncCancel(object userState)及AsyncCancel()兩個函數。

  從上面的列表中也能夠看出,在類型包含多個異步操做而且支持多調用的函數的狀況下,AsyncCancel()所傳入的userState參數應能標示出全部異步函數插入的工做項,而不只僅區分單個異步方法所產生的工做項。

 

模型實現

  難道僅僅經過名稱就能獲得異步執行的功能?並非這樣。爲各組成指明命名規則僅僅會使代碼具備更爲明顯的特徵,使代碼具備更爲明顯的特徵,代碼更容易理解,減小出錯可能,從而下降維護開銷。而實際的多線程功能則由其所實際包含的邏輯中。您可使用您所熟悉的任何多線程編程方法。可是在本文中,咱們將會向您介紹一個您可能並不熟悉的方法:使用AsyncManager。

  該類型提供了一個靜態成員函數CreateOperation()。其用來建立AsyncOperation類型的實例。擁有這樣一個特色:經過調用該AsyncOperation類實例的Post()及PostOperationCompleted()函數,您能夠將委託調用轉發至建立AsyncOperation類型實例的線程中。也就是說,若是AsyncOperation類型實例是在A線程中建立的,卻被傳遞到B線程中,那麼B線程中的執行邏輯就能夠經過 Post()以及PostOperationComplete()函數向A線程發送消息,以在A線程中執行特定邏輯。

  如今咱們就來看看該如何經過AsyncOperation來實現基於事件的異步模型,咱們首先須要調用AsyncOperationManager的CreateOperation()函數,並將該函數建立的AsyncOperation實例做爲參數傳入異步調用中:

1 private void BeginDownloadAsync(string link)
2 {
3     AsyncOperation operation = AsyncOperationManager.CreateOperation(link);
4     RssDownloadWorkerHandler worker = new RssDownloadWorkerHandler(DownloadRss);
5     worker.BeginInvoke(link, operation, null, null);
6 }

  在上面的代碼中,link是須要下載的RSS的所在地址,而DownloadRss則是真正執行執行邏輯的函數。咱們經過調用委託的BeginInvoke()函數在線程池中啓動對該函數的執行。而在委託所包裝的函數中,傳入異步調用的AsyncOperation實例將做爲函數的參數:

1 private void DownloadRss(string link, AsyncOperation operation)…

  在這裏,因爲咱們容許同時下載多個RSS源,所以使用多調用的異步模型是較爲合適的。同時,重複下載同一個地址所指向的RSS源是沒有必要的,所以BeginDownloadAsync()函數所傳入的參數link則至關於userState參數,用以區別各工做項。做爲一個實現標準,若是當前下載項中已經擁有了BeginDownloadAsync()函數所標示的RSS地址,那麼異步模型須要拋出一個表示下載項重複的ArgumentException類型的異常。這裏須要注意的是,異常是線程相關的。爲了能讓用戶代碼探測到該異常,咱們應在主線程中拋出異常。這也就迫使咱們在主線程中管理當前的工做項ID並執行對多調用的檢查。

  反過來,若是異步模型須要將RSS的下載實現爲單調用,那麼在主線程中所須要執行的檢查就須要是當前是否擁有工做項沒有完成,並在當前具備工做項的狀況下拋出一個InvalidOperationException異常。同時咱們還須要爲模型添加IsBusy屬性,以用來提示用戶代碼是否能夠插入下一個工做項,從而避免屢次插入工做項所致使的異常。

  接下來要討論的是如何實現異步模型的執行邏輯。在編寫執行邏輯的過程當中,若是您但願觸發特定事件,那麼您須要經過AsyncOperation的Post()或PostOperationCompleted()函數向原線程插入一個委託。這兩個函數都接受兩個參數:第一個表示須要在AsyncOperation實例的建立線程上執行的函數,而第二個參數則表示須要傳遞給該函數的參數。就以DownloadRss()函數爲例:

 1 private void DownloadRss(string link, AsyncOperation operation)
 2 {
 3     ……
 4     // 對OnProgressChangedInternal的執行將從後臺線程轉至operation的建立線程中
 5     ProgressChangedEventArgs progressArgs = new ProgressChangedEventArgs(percentage);
 6     operation.Post(OnProgressChangedInternal, args);
 7 
 8     ……
 9     TaskCompleteEventArgs args = new TaskCompleteEventArgs(link, succeeded, source);
10     operation.PostOperationCompleted(OnTaskCompleteInternal, args);
11 }

  而在Post()和PostOperationComplete()函數所傳入的執行函數則會運行在建立AsyncOperation實例的線程中,兒不是當前線程。所以在該傳入的函數中,您應真正地發出事件。就以OnTaskCompleteInternal()爲例:

1 // 該函數在建立AsyncOperation實例的線程中執行
2 private void OnTaskCompleteInternal(object param)
3 {
4     TaskCompleteEventArgs args = param as TaskCompleteEventArgs;
5 
6     mTasks.Remove(args.Link);
7     if (TaskComplete != null)
8         TaskComplete(this, args); // 真正地引起事件
9 }

  另外一個須要考慮的操做就是工做項的取消。首先,您須要將工做項編寫爲可取消的形式。在工做項執行過程當中,您須要一種方式,如標誌位等,通知工做項當前任務應當取消,並在工做項自身執行過程當中對該標誌位進行探測。同時,在成功地取消了工做項的執行以後,您須要發送相應的Completed事件,並將其成員屬性Canceled設置爲true,以區別真正的工做項完成所發出的Completed事件。

  同時您還須要理解爭用條件這一名詞。在異步模型執行過程當中,下載可能剛好在發送了工做項取消這一請求後完成了。在這種狀況下,咱們會認爲其成功完成,從而再也不將AsyncCompletedEventArgs的Cancelled屬性設置爲true。

 

AsyncOperation內部實現

  總的來講,對AsyncOperation的使用就是經過AsyncOperation實例保持對主線程的引用,並在須要從後臺線程向主線程中發送消息時向AsyncOpertation實例所記錄的主線程註冊回調邏輯。這種經過記錄建立線程來進行線程管理的方法是在.net開發中很是經常使用的,也很是值得咱們借鑑。通過適當簡化後的相關代碼以下所示:

 1 public static class AsyncOperationManager
 2 {
 3     public static AsyncOperation CreateOperation(object userSuppliedState)
 4     {
 5         return AsyncOperation.CreateOperation(userSuppliedState, 
 6             SynchronizationContext.Current);
 7     }
 8     ……
 9 }
10 
11 public sealed class AsyncOperation
12 {
13     private AsyncOperation(object userSuppliedState, SynchronizationContext syncContext)
14     {
15         this.syncContext = syncContext;
16     }
17 
18     public void Post(SendOrPostCallback d, object arg)
19     {
20         this.syncContext.Post(d, arg);
21     }
22     ……
23 }

 

總結

  最後來一點總結。在不瞭解基於事件的異步模型的衆多組成以前,咱們並沒有法清晰地體會到其所具備的優勢。首先,基於事件的異步模型提供的是你們所最熟悉的事件/委託模型以及成員函數,從而對用戶代碼而言是最天然也最容易接受的。另外,事件/委託模型能夠將對多線程內容的處理隱藏到模型的內部,對多線程的處理侷限於模型內部,大大加強了代碼的可維護性。

 

轉載請註明原文地址並標明轉載:http://www.cnblogs.com/loveis715/p/5250217.html

商業轉載請事先與我聯繫:silverfox715@sina.com

公衆號必定幫忙別標成原創,由於協調起來太麻煩了。。。

相關文章
相關標籤/搜索