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


注:這是大概四年前寫的文章了。而且我離開.net領域也有四年多了。本來不想再發表,但是這實際上是Active Object模式在.net中的一種重要實現方法,因此我把它掏出來發布一下。如果該模型有新的發展,望在評論中幫給出一個引用,以便其它讀者知曉。感激不盡

  基於事件的異步模型實際上是MSDN中講解異步編程時所提供的一個章節。但在閱讀這些章節時,我覺得MSDN在一開始就將所有組成全部列出,然后再聯系到一起的講解次序並不適合我們的思維方式。因此在本文中,我將按照從易到難的方式逐步對該異步模型進行講解。

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

 

模型簡介

  您首先需要明白的一點是,該異步模型並不是在為您提供一種新的多線程編程技術,而是為您提供了一些多線程編程規范方面的建議,從而為您的多線程解決方案提供更良好更靈活的接口和內部組織。就其實現而言,基於事件的異步模型實際上是對其內部所包含的異步操作的一個包裝。而按照該規范組織的異步調用對於用戶代碼而言則意義非常明確:如果需要以異步方式調用一個操作,並在操作完成后接到相應的通知,而不需要和工作項進行進一步的通訊,那么可以直接調用一個函數並偵聽一個事件即可。甚至我們更可以通過偵聽報告工作進度的事件來得到工作項進度更新的信息。

  首先我們來看看異步模型的使用。假設我們有一個異步模型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。您可以看到,這些成員的名稱常常與您所需要插入的工作項的名稱相關。

 

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

 

模型接口

  在大概了解了基於事件的異步模型之后,讓我們先從模型的表觀特征,模型的接口開始說起。經過這一節,相信您能了解該異步模型所提供的各個成員的確切使用方式。

  首先我們來講講異步操作的啟動函數。對於一個需要轉化為異步操作的同步方法,我們常常需要按照一定的步驟將其轉化為異步調用。

  第一步要轉化的是函數名稱。在同步方法的轉化過程中,我們常常需要對函數的名稱進行更改。更改后的函數名稱一方面可以提供較為明顯的“這是使用異步模型的成員函數”的提示,更可以令其與同步函數同時存在。一般情況下,異步函數會在相應的同步函數名之后添加-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

公眾號一定幫忙別標成原創,因為協調起來太麻煩了。。。

 


免責聲明!

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



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