async 與 await 在 Web 下的應用


原文地址: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 關鍵字:

《C#與Visual Basic的未來(上)》

《C#與Visual Basic的未來(中)》

《C#與Visual Basic的未來(下)》 

最后,在這幾篇文章的基礎上,想和大家談談 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 中:

  1. 首先新建一個頁面
  2. 打開 aspx 文件,然后再頂部的屬性中加入:Async="true"
  3. 接下來在任何一個事件中,加入這兩個關鍵字即可
  4. 另外在 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管線的消耗!

 

源代碼和工具下載

AsyncSample

請用 Visual Studio 11 打開


免責聲明!

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



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