第二章 为什么需要异步程序?

众所周知,异步编程既重要又有用,但是为什么它如此重要却要具体问题具体分析,因为这取决于你在编写什么样的程序。有些优点到处都被提及,但真正体现这些优点的场景可能你一辈子也碰不到,如果这和你情况相符,请确保读完这一章,因为背景知识会帮助你理解整个上下文。

带用户界面的桌面程序

桌面程序有一个主要的性能需求——需要对用户操作做出响应。人机交互(HCI)调研发现:程序慢一点用户可以忍,只要用户点击后界面有响应就行(比如有一个动画进度条之类的)。但是如果程序失去响应,用户就会感到抓狂。失去相应的主要原因就是程序在处理一些耗时操作,比如耗时的计算,或者进行IO处理,或者网络请求等。

在C#中你所使用的UI框架基本都只有一个UI线程,比如:

  • WinForm
  • WPF
  • Silverlight

UI线程是唯一可以控制特定窗口内容的线程,它也是唯一检查用户操作并给予响应的线程。如果UI线程很忙碌或者被阻塞超过几十毫秒,用户就会明显感知到程序的反应延迟。

异步代码意味着UI线程可以返回去处理它的主要工作:检查用户事件的消息队列,然后进行响应。它也可以显示进度条动画,在最近版本的Windows中,也可以显示鼠标悬停动画,这些都是很重要的可视化用户体验,用户看到这些后会知道程序一直在对他的行为进行响应。

所有常用的UI框架都只使用一个UI线程,其目的是为了简化同步工作。如果使用多个UI线程,那么可能会出现这种情况:一个UI线程去读按钮的宽度,另一个UI线程在处理空间布局。为了避免这两个UI线程产生冲突,就需要使用锁等机制进行线程间的同步,而这会影响程序的性能。

  

更容易理解的例子:咖啡馆

 下面我将举一个形象的例子来帮助理解上面的内容,如果你认为自己已经掌握了上面的内容,则可以跳过此章节。

想象有一个小咖啡馆,每天出售吐司面包作为早餐。这个店只有一个雇员——就是老板自己。他非常注重顾客对服务的感受,可惜他没学习过异步编程的技巧。

UI线程的模型与此非常类似,在计算机中工作必须通过一个线程来完成自己的任务,在咖啡店的例子中,雇员(也就是老板自己)只能在咖啡店中做自己的工作。在本例中,咖啡店只有一个雇员,就好比只有一个UI线程一样。

第一位顾客要了一片吐司面包,老板拿起面包放到烤面包机里,然后他就一直看着烤面包机工作。这时,顾客问他哪里有黄油,但是他置若罔闻——因为他已经被“观看烤面包机工作”这件事给阻塞住了。五分钟后,面包烤好了,老板把烤好的面包拿给顾客,可是顾客却对他刚才置之不理的行为感到很恼火——这个结果是大家都不想看到的。

现在让我们看看能不能教教这个老板如何异步地进行工作。

首先,他要确保烤面包机能够进行异步工作。当我们编写异步代码时,我们需要确保耗时操作在工作结束后能够回调。在咖啡馆的例子中,烤面包机必须有个计时器,并且能够在面包烤好后很大声地将面包弹出来,这样老板就会注意到面包已经好了。

另外,当开始烤面包后他就不需要在旁边盯着烤面包机看了,他应该回去为顾客提供其它服务。同样地,我们的异步代码必须在开始耗时操作后立即返回,这样UI线程就能够处理用户的操作了。这样做有两个原因:

  • 这样对于用户而言,响应性更好——顾客可以随时要黄油,再也不用担心被置之不理了
  • 用户可以同步地进行其它操作——下一位顾客也可以开始下单了

咖啡馆老板这时就可以同时给多个顾客提供服务了,唯一的限制就是烤面包机的个数,以及取面包、送面包的时间。

但是目前又有了新的问题:他发现很难记住哪一块面包应该给哪一位顾客。事实上,UI线程一旦返回,开始处理用户事件后,它也记不住它在等待哪个操作了。因此,我们需要在启动新任务时就附件一个回调方法,这个方法的作用就是当耗时操作结束后能够提醒我们接下来该做什么。对于咖啡馆老板来说,这个问题很容易解决——在面包上夹一个标签,标签上写明顾客的名字就好了。针对我们编程的来说,情况要稍复杂些,通常来讲我们需要针对任务结束后要做什么提供完整的指令。

当上面说的都准备就位后,咖啡馆老板就可以异步工作了,从而他的业务也会迅速扩张。顾客体验比之前好太多了,因为顾客很少需要等待。

我希望上面的例子能够帮助你理解为何异步编程对UI程序如此重要。

Web应用程序

ASP.NET web服务器不像UI程序那样只有一个UI线程。也就是说,针对web程序来说,使用异步也会有很多好处。因为在web程序中,运行时间较长的操作,尤其是远程数据库查询等,都是很常见的。 

安装的IIS的版本会决定可以处理web请求的总线程数,以及能够同时处理的请求的数量。如果web请求的大多数时间都花在等待数据库查询上,这时为了增加服务器的吞吐量而增加并行请求的数目,不失为一个好办法。

 当一个线程被阻塞等待时,它不会占用任何CPU时间。然而,这不等于说它不使用任何服务器资源。事实上,当线程被阻塞时它会产生两个严重的开销:

  • 内存:在Windows操作系统上,每一个托管线程都会预留1 Mb左右的虚拟内存。如果你只有几十个线程,这并没有问题,但是如果线程数量达到几百的数量级,就会很容易失控了。如果内存被交换到磁盘上,恢复线程将变得很慢。
  • 调度(Scheduler)开销:操作系统的调度程序负责选择在哪一个CPU上执行哪一个线程,以及什么时候执行。即使线程被阻塞了,调度程序也要考虑这些事情,它要知道线程是否已经不被阻塞了。这将使线程上下文切换变慢,从而降低系统整体的性能。

这些开销会增加服务器的负荷,从而降低系统的吞吐量、增大延迟。

请记住:异步代码的主要特征就是启动耗时操作的线程,在启动了耗时操作后就会去做其它事情。在ASP.NET程序中,线程来自于线程池,所以当启动了耗时操作后,线程又被放回到线程池中,然后它就可以处理其它请求了,因此在这种情况下,处理同样多的请求却需要相对较少的线程即可。

另外一个比喻:餐厅的厨房

 

posted @ 2021-03-05 09:34  一缕晨风拂过  阅读(200)  评论(0)    收藏  举报