实验——async什么时候提高吞吐
async是一个语法糖,用来简化异步编程,主要是让异步编程在书写上接近于同步编程。总的来收,在await的时候,相当于附加上了一个.ContinueWith()。
至于为什么async能够提高吞吐,是因为通过async方法返回一个Task对象,IIS缩减了工作线程的处理时间长短(切换到了其他线程,且没有阻塞当前线程),从而提高了单位时间的处理量。这里还有其他的一些细节,详情见这篇博文:
【http://www.cnblogs.com/rosanshao/p/3728108.html】
关于async的使用,参考这篇博文:
【http://www.asp.net/mvc/overview/performance/using-asynchronous-methods-in-aspnet-mvc-4】
博主曾经花了一个下午点时间测试性能,就是没有获得期望的结果,用的就是此博文中举出的反例。当时博主心想“既然TPL中的Task+async就能提高性能,那么为什么EF还要特地的提供XXXAsync方法?这不是让别人更加困惑么?”所以博主就打算不用数据库,简单撸一个Task测一测,看看是不是和我想象中的一般逆天。
根据这篇博客的描述,IIS的线程分为工作线程和IO线程两种,其中工作线程总数被限制在一个阈值,所以减少工作线程的利用效率可以提高吞吐。而在asp.net中,切换线程就分为两种:工作线程->IO线程,工作线程->工作线程(反例)。假定一个工作线程每使用async之前每请求工作1秒,通过切换,IO线程工作的时候,他去处理其他请求,把平均工作时间降为了0.5秒,这样吞吐理想情况下就翻倍了。但是...如果是工作线程->工作线程,虽然对于单个线程而言是减少了,但是其他工作线程又会扔活过来,总体来说没有变化,反而因为交接的问题,性能有所下降...
先用几个负载测试来支持以上言论
首先定义一个提供各种操作的辅助类。
public class BaseFairHelper { public Task<string> SayHelloTask() { return Task<string>.Factory.StartNew(() => { Thread.Sleep(1000); return "Hello"; }); } public async Task<string> SayHelloAsync() { return await Task<string>.Factory.StartNew(() => { Thread.Sleep(1000); return "Hello"; }); } public string SayHello() { Thread.Sleep(1000); return "Hello"; } }
1.基础测试
假定我们任意启动一个Task就可以达到解放IIS工作线程的目的,那么,对于两个Action,一个执行工作量1的同步操作,一个执行工作量1的同步操作外带一个工作量1的一步操作,这两个Action在吞吐以及性能表现上应该相差无几。代码如下:
/// <summary> /// 异步 /// </summary> /// <returns></returns> public ActionResult BaseAsync() { var helper = new BaseFairHelper(); var task1 = helper.SayHelloTask(); var str = helper.SayHello(); task1.Wait(); return Content(str); } /// <summary> /// 对照 /// </summary> /// <returns></returns> public ActionResult BaseAsync_() { var helper = new BaseFairHelper(); var task1 = helper.SayHelloTask(); var task2 = helper.SayHelloTask(); var str = helper.SayHello(); Task.WaitAll(task1, task2); return Content(str); } /// <summary> /// 基础对照 /// </summary> /// <returns></returns> public ActionResult Base() { var helper = new BaseFairHelper(); return Content(helper.SayHello()); }
然后使用VS的负载测试,测试模式选为增量,结果如下:
工作量 | 情况 | 吞吐量(min) | 吞吐量(max) | 时长per请求(max) | 时长per请求(min) | 吞吐均值 | 时长均值 |
(base)1 | 同步 | 8 | 200 | 1.02 | 1.01 | 145 | 1.02 |
(baseasync)2 | 同步+异步 | 8 | 120 | 1.76 | 1.01 | 98.7 | 1.53 |
(baseasync_)3 | 同步+异步x2 | 0 | 89.4 | 2.59 | 1.01 | 69.5 | 2.1 |
可以发现性能相差明显,但是在低并发情况下,性能表现是我们预期的,高并发的时候,则不然。最大吞吐也不是我们预期的。这点上可以支持“IIS工作线程”有限的观点。
2.Fair测试
以上,这是一组对比测试,工作量并不同,现在进行一组工作量相同的测试。其中一个Action执行同步x2的操作,另一个执行同步+异步组合的操作。代码如下:
public ActionResult FairAsync() { var helper = new BaseFairHelper(); var task = helper.SayHelloTask(); var str = helper.SayHello(); task.Wait(); return Content(str); } public ActionResult Fair() { var helper = new BaseFairHelper(); var str = helper.SayHello(); str = helper.SayHello(); return Content(str); }
同样适用负载测试,测试模式选为高并发(200用户数):
工作量 | 情况 | 吞吐量(min) | 吞吐量(max) | 时长per请求(max) | 时长per请求(min) | 吞吐均值 | 时长均值 |
(fair)2 | 同步 | 8 | 112 | 2.03 | 2 | 85 | 2.01 |
(fairasync)2 | 同步+异步 | 20 | 125 | 1.85 | 1 | 107 | 1.59 |
和低并发(25用户数):
工作量 | 情况 | 吞吐量(min) | 吞吐量(max) | 时长per请求(max) | 时长per请求(min) | 吞吐均值 | 时长均值 |
(fair)2 | 同步 | 1 | 12.6 | 2.03 | 2 | 10.7 | 2.02 |
(fairasync)2 | 同步+异步 | 2 | 25 | 1.02 | 1 | 21.3 | 1.01 |
可以看到,由于工作线程争用,导致使用Task的异步方案在高并发的情况下,单个请求的性能有所下降(时长从1->1.85),这也从侧面证明了以上的观点。
async方法提供吞吐的情况
这里是我参考的文章:【http://www.dotnetcurry.com/aspnet-mvc/948/webapi-async-performance-aspnet-mvc-application】
以及这篇文章附带的代码:【http://pan.baidu.com/s/1ntxNX4t】
博主针对数据库(EF)的async做了很多次实验,结果发现同步和异步在吞吐以及性能表现上几乎一致(参考文章末尾附件中的测试结果截图)。于是最终返回这篇文章,并针对这篇文章中的代码进行测试,同时结合自己的思考重新编写了测试——结果仍然没有感受到duang一下的特效。所以暂时不纠结了。
【此处应该有跟进和更新】
使用async的几个姿势
对于以下两个异步方法:
public class AsyncMethods { public static async Task<string> Async1() { return await Task<string>.Factory.StartNew((t) => { Task.Delay(1000).Wait(); return "hello"; }, null); } public static async Task<string> Async2() { return await Task<string>.Factory.StartNew(t => { Thread.Sleep(1000); return "hello"; }, null); } }
1.对多个async方法进行同步等待
[ActionName("IndexAsync2")] public async Task<ActionResult> IndexAsync2() { var task1 = AsyncMethods.Async1(); var task2 = AsyncMethods.Async2(); await Task.WhenAll(task1, task2); return Content(task2.Result); }
2.有序执行多个async
[ActionName("IndexAsync1")] public async Task<ActionResult> IndexAsync1() { string result = await AsyncMethods.Async1(); result = result + await AsyncMethods.Async2(); return Content(result); }
3.死锁(反例)
简单将await方法迁移到同步方法中,都会导致线程死锁(ASP.NET环境下)。由于异步方法执行完成后的操作要求回到调用的上下文(线程),会等待调用上下文。而Wait()方法表示等待异步方法完成。所以你等我我等你,死锁。
public ActionResult Index1() { AsyncMethods.Async1().Wait(); return Content(""); } public ActionResult Index2() { var task1 = AsyncMethods.Async1(); var task2 = AsyncMethods.Async2(); Task.WhenAll(task1, task2); return Content(task1.Result); }
为何async能够防止ASP.NET工作线程等待
参考这篇文章:【http://blog.stevensanderson.com/2008/04/05/improve-scalability-in-aspnet-mvc-using-asynchronous-requests/】的图。
async提高性能的情况
这是并行编程的情况,总的来说就是充分利用CPU,个人认为这更多的是Task的功劳。async这个关键字更多的像是将一些列的ContinueWith连锁在同一个线程(上下文)之上,防止线程切换。
在CQRS中实现Command的异步执行
经过多日的实验和纠结(惭愧),对async的看法有了点转变。async关键字现在给我的感觉,更像是从“骨子里”的异步,因为调用async方法的时候,要求调用方也指明async(或者你可以开一个Task去执行...然而...太蠢)。这感觉是让C#的中的所有方法(指明async)天生就是异步架构的(无端想起了F#)。所以,为Cqrs添加异步功能就分为两块:
1.为CommandBus添加一个SendAsync的方法
2.实现一个完全基于异步的Cqrs【想法,想法,只是想法...】【此处应有后续跟进】
先撸第一个:
public interface ICommandBus { void Send<T>(T command) where T : ICommand; Task SendAsync<T>(T command) where T : ICommand; } void ICommandBus.Send<T>(T command) { var handler = CommandHandlerSearcher.Find<T>(); #region auditing var auditInfo = CommandEventAuditInfo.StartNewForCommand<T>(handler.GetType()); auditInfo.Start(); #endregion handler.Execute(command); #region audting auditInfo.Stop(); #endregion Test.Configuration.AuditStorage.Save(auditInfo); } public ICommandHandlerSearcher CommandHandlerSearcher { get; set; } Task ICommandBus.SendAsync<T>(T command) { ICommandBus bus = this; return Task.Factory.StartNew(() => bus.Send(command)); }
然后是测试结果:
同时,在修改Auditing支持异步的同时,发现了自己以前实现的Auditing有问题。
至于为什么不考虑实现EventBus支持异步...那是因为,博主当前的工作单元是基于线程的(简单粗暴的将一个Command视为原子操作)。
与async有关的代码:【http://pan.baidu.com/s/1sjA7gbN】
此篇完成时,所使用的代码:【http://pan.baidu.com/s/1sjsqiZV】