原文地址: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 中:- 首先新建一个页面
- 打开 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管线的消耗!
源代码和工具下载
AsyncSample
请用 Visual Studio 11 打开
本作品采用知识共享Attribution 2.5 China Mainland许可协议进行许可。欢迎转载,演绎或用于商业目的,但是必须保留本文的署名Dozer