原文地址:http://www.dozer.cc/2012/03/async-and-await-in-web-application/
歡迎大家到我的博客中查看,排版會更舒服一點!
.net 中的異步
關於 .net 的異步,一篇文章是講不完的,我這里就貼兩篇文章讓大家看一下:
《正確使用異步操作》、《C#客戶端的異步操作》、《細說ASP.NET的各種異步操作》
另外,在 .net 4.0 中還推出了新的任務並行庫(TPL),也是一種新異步模式:
最后,.net 4.5 又推出了全新的 async 和 await 關鍵字:
最后,在這幾篇文章的基礎上,想和大家談談 async 和 await 在 Web 下的應用,包括 WebForm 和 MVC。
async 與 await 的簡單介紹
仔細看完老趙的《C#與Visual Basic的未來》大家應該都能明白這兩個關鍵字的作用是什么了。
適用條件:只能適用於TPL異步模式
傳統的方法返回的就是需要返回的內容,而基於TPL模式的異步,返回的都是 Task<T>,其中的 T 類型就是你需要返回內容的類型。
在 Visual Studio 11 中,只要你調用的某個方法返回的類型是 Task 或者 Task<T>,它就會提示這是一個可等待的方法。
這時候,就可以利用 async 和 await 關鍵字了。
場景:解決基於事件的異步中回調函數嵌套使用中的問題
假設有這樣一個場景,一個 C# 應用程序中(WinForm Or WPF)我需要從一個網站上下載一個內容,然后再根據內容里的網址再下載里面的內容。
如果直接利用 WebClient 的 DownloadString 方法,很明顯 UI 線程會被阻塞,沒人會這么做。
如果只是一次下載,那利用 WebClient 的 DownloadStringAsync 就可以輕松解決了,但是如果是想這樣需要兩次下載,而且兩次下載是有關聯的呢?如果是三次四次呢?
我們先來看看用基於事件的異步來實現:
protected void DownloadAsync() { WebClient client = new WebClient(); client.DownloadStringCompleted += client_DownloadStringCompleted; client.DownloadStringAsync(new Uri("http://www.website.com")); } void client_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e) { WebClient client = new WebClient(); client.DownloadStringCompleted+=client_DownloadStringCompleted2; client.DownloadStringAsync(new Uri(e.Result)); } void client_DownloadStringCompleted2(object sender, DownloadStringCompletedEventArgs e) { var result = e.Result;//最終結果 //do more }
下面再來看看用 async 和 await 來實現:
protected async void DownloadTaskAsync() { WebClient client = new WebClient(); var result1 = await client.DownloadStringTaskAsync("http://www.website.com"); WebClient client2 = new WebClient(); var result2 = await client.DownloadStringTaskAsync(result1); //do more }
是不是簡單多了?
在 WebForm 和 MVC 中使用 async 和 await
在 .net 4.5 中,最新的 WebForm 和 MVC 都已經支持這兩個關鍵字了。
在 asp.net WebForm 中:
- 首先新建一個頁面
- 打開 aspx 文件,然后再頂部的屬性中加入:Async="true"
- 接下來在任何一個事件中,加入這兩個關鍵字即可
- 另外在 Web.Config 中有兩個奇怪的配置,有可能會導致出錯,去掉有正常,這兩個配置具體有什么用,我已經在 StackOverFlow 上問別人了
protected async void Page_Load(object sender, EventArgs e) { WebClient client = new WebClient(); var result1 = await client.DownloadStringTaskAsync("http://www.website.com"); WebClient client2 = new WebClient(); var result2 = await client.DownloadStringTaskAsync(result1); //do more }
在 asp.net MVC 中:
把原來繼承於 Controller 改成繼承於 AsyncController
在方法前加上 async,並把返回類型改成 Task<T>
public class HomeController : AsyncController { public async Task<ActionResult> Test() { var result = await Task.Run(() => { Thread.Sleep(5000); return "hello"; }); return Content(result); } }
在 IHttpHandlder 中:
微軟官方的 .net 4.5 releace note 中已經提到了:
public class MyAsyncHandler : HttpTaskAsyncHandler { // ... // ASP.NET automatically takes care of integrating the Task based override // with the ASP.NET pipeline. public override async Task ProcessRequestAsync(HttpContext context) { WebClient wc = new WebClient(); var result = await wc.DownloadStringTaskAsync("http://www.microsoft.com"); // Do something with the result } }
在 IHttpModule 中:
同樣是微軟官方的 .net 4.5 releace note 中,實現起來有點復雜,大家可以自己去看看。
在 Web 應用程序中使用 async 和 await 的注意事項
其實不僅僅是使用這兩個關鍵字的注意事項,而是在 Web 中只要用到了異步頁,就要注意一下問題!
Web 本來就是多線程的,為什么還要用異步編程?
多線程只是實現異步的一種手段,的確,Web 本來就是多線程的,所以在很多時候不用異步也沒什么問題。一般也不會有問題,只是有更好的方案。
大家看完《正確使用異步操作》后就會知道,異步有多種實現方式,但是它們底層只有兩種類型,一種是:“Compute-Bound Operation”,另一種是“IO-Bound Operation”。(具體的可以到文中查看)
在 Web 中,使用異步去處理“Compute-Bound Operation”是沒有意義的,因為 Web 本來就是多線程的,這樣做沒有任何效率上的提升。(除非你在處理這個異步的時候,不需要等待這個異步執行結束就可以返回頁面內容)
所以,在 Web 中,只有當你需要面對“IO-Bound Operation”的時候,去用異步頁才是真的有用的。因為它是在等待磁盤或者網絡響應,並不占據資源,甚至不占據工作線程。
如何區分呢?那篇文章中已經寫了,另外,大部分和磁盤&網絡打交道的異步操作都是“IO-Bound Operation”的。
但是,如果你真的想要提升效率,還需要你親自去測試一下,因為要實現“IO-Bound Operation”有一定的條件。
WebClient、WebService 和 WCF 支持嗎?
經過測試,上面這三種 Web 應用程序中使用最多的,是支持“IO-Bound Operation”的。其中,在 .net 4.5 中,WebClient 和 WCF 可以直接支持 async 和 await 關鍵字。(因為它們有相關的方法可以返回 Task 對象)
而 WebService(微軟不建議使用,但實際上還在被大量的應用),卻不支持,但是可以通過寫一些代碼后讓它支持。
數據庫操作支持嗎?
經過一定的配置后,它是可以支持的,但是具體的還需要進行大量的測試,畢竟不是調用幾個方法那么簡單。
如何把傳統的異步模式轉換成 TPL 模式,以實現 async 和 await
上面提到了 WebService 並沒有實現 TPL 模式,在 .net 4.5 中引用 WebService 后實現的是基於事件的異步。
(.net 2.0 以上程序在引用 WebService 的時候,需要點“添加服務引用”——“高級”——“添加Web引用”,如果直接在服務引用中添加,會出現一定的問題。並且,就算你添加了,它也沒有幫你實現基於 TPL 的異步。)
如何把 APM 模式轉換成 TPL 模式?
其實微軟在這篇文章中已經寫過如何把傳統的異步模式轉換成 TPL 模式了:TPL 和傳統 .NET 異步編程
其中 APM 轉 TPL 比較簡單,我就不多介紹了。
如何把 EAP 模式轉換成 TPL 模式?
EAP 就是基於事件的異步,上面那篇文章中其實也提到了,但是寫的並不是很清楚。
下面我用一段簡化的代碼來實現 EAP 轉 TPL:
namespace WebServiceAdapter.MyWebService { public partial class WebService { /// <summary> /// 無 CancellationToken 的調用 /// </summary> /// <returns></returns> public Task<string> HelloWorldTaskSync() { return HelloWorldTaskSync(new CancellationToken()); } /// <summary> /// 有 CancellationToken 的調用 /// </summary> /// <param name="token"></param> /// <returns></returns> public Task<string> HelloWorldTaskSync(CancellationToken token) { TaskCompletionSource<string> tcs = new TaskCompletionSource<string>(); token.Register(() => { //注冊 CancellationToken this.CancelAsync(null); }); //注冊完成事件 this.HelloWorldCompleted += (object sender, HelloWorldCompletedEventArgs args) => { if (args.Cancelled == true) { tcs.TrySetCanceled(); return; } else if (args.Error != null) { tcs.TrySetException(args.Error); return; } else { tcs.TrySetResult(args.Result); } }; //異步調用 this.HelloWorldAsync(); //返回 Task return tcs.Task; } } }
轉換好后再去配合使用 async 和 await 關鍵字就方便多了:
protected async void Page_Load(object sender, EventArgs e) { using (WebService service = new WebService()) { await service.HelloWorldTaskSync(); } }
性能測試
前期准備:
理論和實際代碼都講完了,是不是該拿出點東西來驗證一下了?
在做性能測試的時候我繞了很多彎路,碰到了很多問題,一度讓我懷疑它是不是真的可以提升性能。
但最終還是解決了!感謝老趙的一篇文章:體會ASP.NET異步處理請求的效果
異步頁最大的用處就是在處理“IO-Bound Operation”的時候可以不占據工作線程。
測試的理論:
- 限制網站應用程序的工作線程,然后同時請求一個頁面,請求數大於工作線程數。
- 請求的頁面會訪問一個 WebService ,這個 WebService 會延遲5秒,對於網站來說,這個5秒就是“IO-Bound Operation”。
- 如果限制了工作線程數后,異步頁所有請求都可以在5秒完成,那說明的確沒有占據工作線程。反之則說明理論錯誤!
我一開始限制的工作線程是10,然后同時請求50,但是無論是異步頁還是同步頁,總耗時都差不多…
后來仔細看老趙的文章才發現,原來在 Vista & Win7 中最大請求數被限制在10了,所以多於10的請求根本沒到達網站應用程序。
最后我把工作線程限制在2,然后同時請求10,終於得到了正確的理論數據!
工具准備:
我這里用的工具是 apache 下那只的 ab.exe,簡單好用!
另外我也寫了相關的代碼來支持測試。
開始測試:
運行 WebService,提供一個會延時5秒的服務。
然后運行網站,有三個頁面:
- NoAsyncPage.aspx :傳統的頁面
- AsyncPage_IO.aspx:異步頁面,和傳統頁面一樣,都是調用 WebService ,但是是用異步調用
- AsyncPage_CPU.aspx:為了驗證在異步中執行“Compute-Bound Operation”是沒有意義的
在CMD中,依次用 ab.exe 調用這三個頁面:
ab -c 10 -n 10 http://localhost:6360/noasyncpage.aspx ab -c 10 -n 10 http://localhost:6360/asyncpage_io.aspx ab -c 10 -n 10 http://localhost:6360/asyncpage_cpu.aspx
最終運行結果如下:
- NoAsyncPage.aspx :26.39秒
- AsyncPage_IO.aspx:5.29秒
- AsyncPage_CPU.aspx:26.54秒
數據分析:
仔細分析下數據,會發現都符合理論:
- NoAsyncPage.aspx :沒有采用異步,2個工作線程,10個請求,總事件在10*5/2=25左右。
- AsyncPage_IO.aspx:采用異步頁,不占據工作線程,10個請求同時執行。
- AsyncPage_CPU.aspx:雖然采用了異步頁,但是異步的時候依然占據了一個工作線程,而且還多了一些新建線程,切換線程的損耗。
最終結果非常讓人滿意,特別是AsyncPage_IO.aspx,如果我們把訪問量大,並且需要等在磁盤或者是網絡的頁面都改寫成這樣,那可以大大減少IIS管線的消耗!
源代碼和工具下載
請用 Visual Studio 11 打開