作者 | 菜菜

责编 | 郭芮


Y妹:菜菜哥,老大说把所有的接口都改成异步操作......

菜菜:异步好呀,最少比同步能提高吞吐量。

Y妹:异步是怎么回事呢,能讲讲不?

菜菜:来,哥给你解释一番。




异步定义



关于异步的定义,网上有很多不同的形式,但是归根结底中心思想是不变的。无论是在HTTP请求调用的层面,还是在CPU内核态和用户态传输数据的层面,异步这个行为针对的是调用方:


一个可以无需等待被调用方的返回值就让操作继续进行的方法。


在多数程序员的概念中一般是指线程处理的层面:


异步是计算机多线程的异步处理。与同步处理相对,异步处理不用阻塞当前线程来等待处理完成,而是允许后续操作,直至其它线程将处理完成,并回调通知此线程。


程序员修神之路:问世间异步为何物?_多线程


可以这样通俗的理解,异步主要解决的问题是不阻塞调用方,用方这里可以是HTTP请求的发起者,也可以是一个线程。


但此处需要明确的是:异步与多线程与并行不是同一个概念。




CPU密集型操作



我听有的同学说,异步解决的是IO密集型的操作,我觉得是不准确的。异步同样可以解决CPU密集型操作,只不过场景有限而已。


有一个前提:利用异步解决CPU密集型操作要求当前运行环境支持多线程才行,比如JavaScript这个语言,本质上它的运行环境是单线程的,所以对于CPU密集型操作,JavaScript会显得力不从心。


异步解决CPU密集操作一般情况下发生在同进程中,为什么这么说呢,如果发生在不同机器或者不同进程在很多情况下已经属于IO密集型的范围了。这里顺便提醒一下:IO操作可不单单是指磁盘的操作,所有有输入/输出(Input/Output)操作的都可以泛称为IO。


举个栗子吧:


在一个带有UI的软件上点击一个按钮,UI线程会发生操作行为,假如UI线程在执行过程中有一个计算比较耗时的操作(你可以想象成计算1--999999999的和),UI线程在同步操作的情况下会一直等待计算结果,在计算完毕之后才会继续执行剩余操作。在等待的这个过程中,呈现给用户的情况就是UI卡住了,俗称假死了,带给用户的体验是非常不好的。这种情况下,我们可以新启动一个线程去执行这个耗时的操作,当执行完毕,利用某种通知机制来通知原来线程,以便原来线程继续自己的操作。


启动新线程执行CPU密集型操作利用的其实就是多线程的优势,如果是单核CPU,其实这种优势并不明显。




IO密集型操作



异步的优势在IO密集型操作中表现的淋漓尽致,无论是读取一个文件还是发起一个网络请求,我的建议是尽量使用异步。


这里首先普及一个小知识:其实每个外设设备都有自己的处理器,比如磁盘,所以每个外设设备都可以处理自己相应的请求操作。但是处理外设设备信息的速度和CPU的执行速度来比较有着天壤之别。


程序员修神之路:问世间异步为何物?_响应时间_02


上图展示了不同的 IO 操作所占用的 CPU 时钟周期。


在计算机中,CPU的运算速度最快,以其的运算速度为基准,时钟周期为1。其次是一级缓存、二级缓存和内存,硬盘和网络最慢,它们所花费的时钟周期和内存所花费的时钟周期差距在五位数以上,更不用提跟CPU和一级缓存、二级缓存的差距了。


由于速度的差距,所以几乎所有的IO操作都推荐使用异步。比如当读取磁盘一个文件的时候,同步状态下当前线程在等待读取的结果,这个线程闲置的时间几乎可以用蛋疼来形容。所以现代的几乎所有的知名第三方的操作都是异步操作,尤其以Redis、Nodejs 为代表的单线程运行环境令人刮目相看。


现在是微服务盛行的时代,UI往往一个简单的按钮操作,其实在后台程序可能调用了几个甚至更多的微服务接口(关于微服务这里不展开),如果程序是同步操作的话,那响应时间是这些服务接口响应时间的和,但是如果采用的是异步操作,调用方可以在瞬间把调用服务接口的操作发送出去,线程可以继续执行下边代码或者等待所有的服务接口返回值也可以。


最差的情况下,接口的响应时间为最慢的那个服务接口响应时间,这有点类似于木桶效应。




异步的回调



通过以上介绍,我们一定要记住一个知识点:异步需要回调机制。


异步操作之所以能在执行结果完成之后继续执行下面程序完全归功于回调,这也是所有异步场景的核心所在,前到JS的异步回调,后到CPU内核空间copy数据到用户空间完成通知等等异步场景,回调无处不在。


说道回调大部分语言都是注册一个回调函数,比如JS会把回调的方法注册到执行的队列,C#会把回调注册到IOCP。这里延伸一下,在很多系统里,很多IO网络模型其实是属于同步范畴的,比如多路复用技术,真正异步非阻塞的推荐Windows下的IOCP。


现在很多现代语言都支持更优秀的回调方式,比如JS和C# 现在都支持async 和await方式来进行异步操作。


据说Windows下的IOCP才是真正的异步非阻塞模型,求留言区验证!


程序员修神之路:问世间异步为何物?_多线程_03




异步的特点



优势:



  • 异步操作无须额外的线程负担,使用回调的方式进行后续处理,在设计良好的情况下,处理函数可以不必使用共享变量(即使无法完全不用,最起码可以减少 共享变量的数量),减少了死锁的可能。
  • 线程数量的减少,减少了线程上下文在CPU切换的开销。
  • 微服务环境(调用多个服务接口的情况下)加快了上层接口的响应时间,意味着增加了上层接口的吞吐量。


劣势:



  • 异步操作传统的做法都是通过回调函数来实现,与同步的思维有些差异,而且难以调试。
  • 如果当前环境有操作顺序的要求,异步操作为了保证执行的顺序需要做额外的工作。
  • 由于多数情况下异步的回调过程中的执行线程并非原来的线程,所以在捕获异常,上下文传递等方面需要做特殊处理,特别是不同线程共享代码或共享数据时容易出问题。




写在最后




  • 在并发量较小的情况下,阻塞式 IO和异步IO的差距可能不是那么明显,但随着并发量的增加,异步IO的优势将会越来越大,吞吐率和性能上的差距也会越来越明显。
  • 在压力比较小的情况下,一般异步请求的响应时间大于同步请求的响应时间,因为异步的回调也是需要时间的。
  • 在大并发的情况下,采用异步调用的程序所用线程数要远远小于同步调用程序所用的线程数,CPU使用率也一样(因为避免了太多线程上下文切换的成本)。


为了系统性能,不要让任何设备停下来休息。



作者:菜菜,一个奔走在通往互联网更高之路的工程师,热衷于互联网技术。目前就职于某互联网教育公司,应用服务端主要负责人。拥有10年+互联网开发经验,热衷于高性能、高并发、分布式技术领域的研究,主要工作语言为C#和Golang 。

声明:本文为作者投稿,版权归其个人所有。