接上篇:《C#异步的世界【上】》
上篇主要分析了async\await之前的一些异步模式,今天说异步的主要是指C#5的async\await异步。在此为了方便的表述,我们称async\await之前的异步为“旧异步”,async\await为“新异步”。
新异步的使用只能说新异步的使用太简单(如果仅仅只是说使用)
方法加上async修饰符,然后使用await关键字执行异步方法,即可。对就是如此简单。像使用同步方法逻辑一样使用异步。
public async Task<int> Test() { var num1 = await GetNumber(1); var num2 = await GetNumber(num1); var task = GetNumber(num2); num3 = await task; return num1 + num2 + num3; }
新异步的优势在此之前已经有了多种异步模式,为什么还要引入和学习新的async\await异步呢?当然它肯定是有其独特的优势。
我们分两个方面来分析:WinForm、WPF等单线程UI程序和Web后台服务程序。
对于WinForm、WPF等单线程UI程序代码1(旧异步)
private void button1_Click(object sender, EventArgs e) { ); request.BeginGetResponse(new AsyncCallback(t => { //(1)处理请求结果的逻辑必须写这里 label1.Invoke((Action)(() => { label1.Text = ; }));//(2)这里跨线程访问UI需要做处理 }), null); }
代码2(同步)
private void button3_Click(object sender, EventArgs e) { HttpClient http = new HttpClient(); ).Result; //(1)处理请求结果的逻辑可以写这里 label1.Text = ;//(2)不在需要做跨线程UI处理了 }
代码3(新异步)
button2_Click(object sender, EventArgs e) { HttpClient http = new HttpClient(); ); //(1)处理请求结果的逻辑可以写这里 label1.Text = ;//(2)不在需要做跨线程UI处理了 }
新异步的优势:
是的,说得再多还不如看看实际效果图来得实际:(新旧异步UI线程没有阻塞,同步阻塞了UI线程)
【思考】:旧的异步模式是开启了一个新的线程去执行,不会阻塞UI线程。这点很好理解。可是,新的异步看上去和同步区别不大,为什么也不会阻塞界面呢?
【原因】:新异步,在执行await表达式前都是使用UI线程,await表达式后会启用新的线程去执行异步,直到异步执行完成并返回结果,然后再回到UI线程(据说使用了SynchronizationContext)。所以,await是没有阻塞UI线程的,也就不会造成界面的假死。
【注意】:我们在演示同步代码的时候使用了Result。然,在UI单线程程序中使用Result来使异步代码当同步代码使用是一件很危险的事(起码对于不太了解新异步的同学来说是这样)。至于具体原因稍候再分析(哎呀,别跑啊)。
对于Web后台服务程序也许对于后台程序的影响没有单线程程序那么直观,但其价值也是非常大的。且很多人对新异步存在误解。
【误解】:新异步可以提升Web程序的性能。
【正解】:异步不会提升单次请求结果的时间,但是可以提高Web程序的吞吐量。
1、为什么不会提升单次请求结果的时间?
其实我们从上面示例代码(虽然是UI程序的代码)也可以看出。
2、为什么可以提高Web程序的吞吐量?
那什么是吞吐量呢,也就是本来只能十个人同时访问的网站现在可以二十个人同时访问了。也就是常说的并发量。
还是用上面的代码来解释。[代码2] 阻塞了UI线程等待请求结果,所以UI线程被占用,而[代码3]使用了新的线程请求,所以UI线程没有被占用,而可以继续响应UI界面。
那问题来了,我们的Web程序天生就是多线程的,且web线程都是跑的线程池线程(使用线程池线程是为了避免不断创建、销毁线程所造成的资源成本浪费),而线程池线程可使用线程数量是一定的,尽管可以设置,但它还是会在一定范围内。如此一来,我们web线程是珍贵的(物以稀为贵),不能滥用。用完了,那么其他用户请求的时候就无法处理直接503了。
那什么算是滥用呢?比如:文件读取、URL请求、数据库访问等IO请求。如果用web线程来做这个耗时的IO操作那么就会阻塞web线程,而web线程阻塞得多了web线程池线程就不够用了。也就达到了web程序最大访问数。
此时我们的新异步横空出世,解放了那些原本处理IO请求而阻塞的web线程(想偷懒?没门,干活了。)。通过异步方式使用相对廉价的线程(非web线程池线程)来处理IO操作,这样web线程池线程就可以解放出来处理更多的请求了。
不信?下面我们来测试下:
【测试步骤】:
1、新建一个web api项目
2、新建一个数据访问类,分别提供同步、异步方法(在方法逻辑执行前后读取时间、线程id、web线程池线程使用数)
public class GetDataHelper { 同步方法获取数据 GetData() { var beginInfo = GetBeginThreadInfo(); using (HttpClient http = new HttpClient()) { http.GetStringAsync().Wait();//注意:这里是同步阻塞 } return beginInfo + GetEndThreadInfo(); } 异步方法获取数据 Task<string> GetDataAsync() { var beginInfo = GetBeginThreadInfo(); using (HttpClient http = new HttpClient()) { );//注意:这里是异步等待 } return beginInfo + GetEndThreadInfo(); } public string GetBeginThreadInfo() { int t1, t2, t3; ThreadPool.GetAvailableThreads(out t1, out t3); ThreadPool.GetMaxThreads(out t2, out t3); , DateTime.Now, Thread.CurrentThread.ManagedThreadId, t2 - t1); } public string GetEndThreadInfo() { int t1, t2, t3; ThreadPool.GetAvailableThreads(out t1, out t3); ThreadPool.GetMaxThreads(out t2, out t3); , DateTime.Now, Thread.CurrentThread.ManagedThreadId, t2 - t1); } }
3、新建一个web api控制器