從.Net4.0開始,.Net提供了一個Task類來封裝一個異步操作,用來簡化異步方法的調用。.Net4.5更進一步,添加了async和await兩個關鍵字,異步編程同步化,不用再寫一堆散亂的回調或者完成事件處理。Silverlight5開始支持Task類,但是要用await的話就需要編譯器的支持,VS2012直接支持,如果是VS2010,那就要安裝Async CTP,而Silverlight4沒有Task類,但安裝Async CTP后,得到的AsyncCtpLibrary_Silverlight.dll會帶有適合Silverlight4使用的Task類。下面介紹怎么使用。
使用Task封裝異步方法
可能大家都知道,Task不是一個靜態的工具類,我們不是調用它的一個靜態方法來簡化異步的調用,而是首先要將異步操作封裝到一個Task中,然后再使用這個封裝好的Task,一個Task就包含了開始和回調(或者是開始和完成事件處理),這樣對於Task的消費者來說,異步操作的復雜性就隱藏在Task的背后了。TaskFactory<TResult>類能幫助我們快速封裝異步方法到一個Task中。例如:
public Task<UserInfo> CreateUserTaskAsync(UserInfo user) { return Task<UserInfo>.Factory.FromAsync(this.Channel.BeginCreateUser, this.Channel.EndCreateUser, user, null); }
對於Wcf服務的方法,我暫時發現微軟提供了兩種方式自動生成這些基於Task方法。
1.VS2012+.Net4.5
VS2012下.Net4.5的項目在添加服務引用時有如下設置可以生成基於Task的方法
生成后的XXXAsync方法的返回類型就是Task<T>。例如:
public System.Threading.Tasks.Task<string> SayHelloToAsync(string name) { return base.Channel.SayHelloToAsync(name); }
2.Net4.0+TaskWsdlImportExtension.dll
如果使用VS2010或者在2012下的項目的目標框架是.Net4.0的話,是沒有上面那個選項的。這時可以借助安裝Async CTP后得到的TaskWsdlImportExtension.dll來幫助生成。首先對應的項目中引用TaskWsdlImportExtension.dll,然后在web.config中添加如下配置:
<system.serviceModel> <client> <metadata> <wsdlImporters> <extension type="TaskWsdlImportExtension.TaskAsyncWsdlImportExtension, TaskWsdlImportExtension" /> </wsdlImporters> </metadata> </client> </system.serviceModel>
更新服務引用后,同樣得到基於Task的異步方法。
很可惜的是這個dll不適合Silverlight使用,所以我參考TaskWsdlImportExtension的封裝方式(使用Task.Factory.FromAsync),自己做了個小工具來生成這些方法。
自定義工具生成Silverlight項目的基於Task的操作
這個工具本來想寫成T4模版的,但對T4不是很熟,最后寫成了一個exe。
大概原理如下,首先遍歷項目下的所有叫Reference.cs的文件,動態把它編譯為Assembly,再反射這個Assembly,找那些BeginXXX方法,把這些方法封裝為Task方法放到一個跟原Service同命名空間,同一一個Client的部分類中。
使用方式可能有點麻煩,但個人認為可以接受。
1.在有服務引用的Silverlight類庫中添加一個類
2.右鍵這個類,選擇打開方式,添加,選擇小工具的exe
然后確定,即可生成一堆XXXTaskAsync方法。
當然,上面是第一次使用,以后使用時,打開方式上存在這個程序,只需右擊打開方式,再在列表選擇這個程序兩步即可。
生成的代碼大概如下:
下面再看看怎么使用這些方法。
1.使用async和await
Silverlight項目使用vs2012或者VS2010安裝了Async CTP的話,就可以使用async和await。
1.vs2010的話首先在在Async CTP下找到AsyncCtpLibrary_Silverlight.dll或者AsyncCtpLibrary_Silverlight5.dll,並添加引用
vs2012就在Nuget里搜索await,引用下面的這個包
1.普通使用
await的使用方式可能大家都知道,就不詳細介紹,就是在方法里加async關鍵字,在Task方法前加await關鍵字
async private void OnMainPageLoaded(object sender, RoutedEventArgs e) { var users = await userServiceClient.GetAllTaskAsync(); dataGrid1.ItemsSource = users; }
async private void OnMultiTaskClick(object sender, RoutedEventArgs e) { var num = await systemServiceClient.GetNumTaskAsync(3); var result = await systemServiceClient.SayHiToTaskAsync(num.ToString("n2")); var time = await systemServiceClient.GetSerivceTimeTaskAsync(); var users = await userServiceClient.GetAllTaskAsync(); MessageBox.Show(string.Format("Num:{1},{0}{2},{0}Time:{3},{0}UserCount:{4}", Environment.NewLine, num.ToString("n2"), result, time.ToString(), users.Count.ToString())); }
可以看到OnMultiTaskClick的第二個wcf方法是依賴於第一個wcf方法的結果的,這里感覺完全是”同步編程“,結果沒有問題:
2.Debug
使用await做Debug時跟同步編程一樣,無論單步或是其它,得心應手:
3.異常
調用wcf時產生的異常信息也跟不使用Task時一樣的,對於Wcf返回來的異常類型是FaultException<ExceptionDetail>。把null傳到下面這個方法產生的異常信息。
[OperationContract] public string SayHiTo(string name) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException("name"); return string.Format("Hi,{0}!", name); }
2.后台線程+Task.Wait
如果沒有VS2012或者不想安裝Async CTP的話,就可以使用這種方式。由於Task.Wait是會阻塞線程的,在Silverlight的主線程是不允許這樣做的,所以把操作放到后台線程去做,既然是后台線程,所以就會存在跨線程操作UI的問題,要記得使用Dispatcher.BeginInvoke來操作UI。
1.普通使用
private void OnMainPageLoaded(object sender, RoutedEventArgs e) { ThreadPool.QueueUserWorkItem((s) => { var task = userServiceClient.GetAllTaskAsync(); task.Wait(); Dispatcher.BeginInvoke(() => { dataGrid1.ItemsSource = task.Result; }); }); }
private void OnMultiTaskClick(object sender, RoutedEventArgs e) { ThreadPool.QueueUserWorkItem((s) => { var task1 = systemServiceClient.GetNumTaskAsync(5); task1.Wait(); var task2 = systemServiceClient.SayHiToTaskAsync(task1.Result.ToString()); var task3 = systemServiceClient.GetSerivceTimeTaskAsync(); var task4 = userServiceClient.GetAllTaskAsync(); task2.Wait(); task3.Wait(); task4.Wait(); Dispatcher.BeginInvoke(() => { MessageBox.Show(string.Format("Num:{1},{0}{2},{0}Time:{3},{0}UserCount:{4}", Environment.NewLine, task1.Result.ToString(), task2.Result, task3.Result.ToString(), task4.Result.Count.ToString())); }); }); }
跟上面一樣,OnMultiTaskClick的第二個方法依賴於第一個方法,結果雷同。
這種方法感覺沒有上面的await那么好,但也比直接編寫的APM(Begin/End)或者EAP(完成事件)要方便不少。
2.Debug
使用這種方式Debug時要把斷點放到委托內,其它一樣,Wait后得到結果。
3.異常
這里調用wcf產生的異常有點不一樣,原本的異常變成了AggregateException,而真正的FaultException<ExceptionDetail>跑到了InnerException里去了
使用這個方式還有一點要注意,調用wcf的產生異常跳到Application_UnhandledException時也是后台線程,需要使用Deployment.Current.Dispatcher.BeginInvoke來操作UI,例如:
private void Application_UnhandledException(object sender, ApplicationUnhandledExceptionEventArgs e) { var msgFormat = "Message:{0} {1} StackTrace:{2}"; var msg = e.ExceptionObject.Message; var stackTrace = e.ExceptionObject.StackTrace; if (e.ExceptionObject.InnerException != null && e.ExceptionObject.InnerException is FaultException<ExceptionDetail>) { var ex = e.ExceptionObject.InnerException as FaultException<ExceptionDetail>; msg = ex.Detail.Message; stackTrace = ex.Detail.StackTrace; } Deployment.Current.Dispatcher.BeginInvoke(() => { MessageBox.Show(string.Format(msgFormat, msg, System.Environment.NewLine, stackTrace)); }); e.Handled = true; }
總結
1.對於生成Task方法的方式,如果是.Net項目可以選擇微軟提供的方法,VS2012+.Net4.5直接在服務引用生成,其它使用TaskWsdlImportExtension.dll。
2.對於Silverlight項目的話,只能自己制造工具去包裝了。
3.條件滿足的話盡量使用await的方式來使用Task(VS2012或者VS2010+Async CTP)
4.使用VS2010又不想安裝Async CTP就用后台線程+Task.Wait的方式,多注意后台線程不能調用UI的問題和異常處理的一點差異。