本人萌新一枚,实际工作中编写了一些WebApi介面,最近在学习一些非同步和WebApi的深层次的知识,发现很多博客或者说git中的项目出现了非同步WebApi,可能我见识短见,最近才发现, 类似于这样的

public async Task& GetUser(int id)

{ IUserService userService = new UserService(); User user = await userService.QueryByID(id);

return Ok(user);

},非同步编程也了解一些,就是不知道这样相比于同步的写法有哪些好处?是能够提高服务吞吐量吗?更直白点说就是我伺服器一秒钟只能处理1000个,现在能处理2000个?是这样吗?


这个代码中,userService获取值如果是直接从内存获取,那么它并不会给带来性能提升,还有可能带来性能损耗,如果它是从redis或资料库获取,那么会得到一定的提升。

非同步是一种很高效的操作,能够尽力榨干机器的性能,明白它的原理就很简单了。

比如现在有十个请求,每个请求分成三个阶段处理,初始化10ms,读取资料库10ms,运算10ms。

假如有一台伺服器,一次只能处理1个请求,那么同时来十个请求,排入队列,1号请求要等到回复要30毫秒,2号回复要等1号完成再进行,那就需要60ms,以此类推,需要300毫秒完成。

如果是非同步,第一个请求进入读取资料库时,CPU就空闲出来了,那么第二个请求的运算就可以开始了。

可以注意到当第一个请求第一阶段运算完成,读资料库时,CPU已经空闲下来了,但是同步的话就只能让它空著。

如果采用非同步,第一个请求30毫秒完成,第二个第40毫秒时就可以完成,第三个请求50ms…十个请求只需要120ms就可以完成。

可以看到,十个请求,120ms就处理ms了本来要300ms才能处理好的任务,时间节省了一半多。

nodejs单线程能够有极高的性能,也是通过非同步来实现的。

性能的提升意味著你可以用更加便宜的硬体节省成本以及应对各种业务需要。

所以,非同步的适用于充分利用资源空闲,反之,如果是不会出现资源空闲的任务,比如上述场景的十个请求都仅仅是读取资料库的话,是毫无意义的。

然后又会有疑问了,通常的web伺服器,它同时能处理多个请求,非同步是否还是需要的?

设想一个场景,同时来了一万个请求,而伺服器比如在Java中直接开同步线程,如果开上几千个就会不堪重负,并且会占用相当大的内存空间,并且显然这几千个线程仅仅是挤出来了计算空间,而对于共享资源比如读写资料库,线程需要进行互相加锁处理。

如果有每个进程,需要先读写10ms资料库,然后计算10ms,再读写资料库10ms,那么你将需要让它持有资料库资源高达30ms,其中有10ms只能眼睁睁看著,一万个请求将浪费10万ms用于等待。

如果能让线程们能够自己切换,它们就像帝国时代里的工人们,砍树的也能采石头,也能造房子,这个问题就能解决。

将任务进行分割,计算的任务,读资料库的任务,进行分离,然后一堆线程轮流负责专门读写资料库,一堆线程轮流专门负责计算,哪里有空闲时,闲的一堆线程过去负责其他工作。

听起来是不是很耳熟,这就是使用iocp或是epoll实现高性能处理的关键,写这些东西很繁琐,调度听起来也有点复杂,放到十多年前,会用这些的,光是跟人口沫横飞都可以吹上个大半天,而且大多数人即使知道了原理,也很难将它们运用好。

到现在,这些完全不用操心了,只需要简单的async与await,调调下线程池参数,必要时再用用自旋锁,遵循好规范,轻易就可以实现高性能方案。


先说结论,使用async(非同步)确实能提升webapi的处理能力。但是具体提升多少,或者使用不当反而会造成性能下降?这个就要好好了解async和sync的异同,然后根据自己的业务和代码逻辑进行选择。而不是盲目的全部都使用async。

通常,async的特性是允许任务在处理的过程中切换线程。这也就意味著,请求处理开始时候使用的一个线程,往往返回的时候是另外一个线程。而sync(同步)自始自终都会在保持在同一个线程上。

.net程序在运行时,会负责调度一个线程池,其中的线程数量,跟cpu、操作系统和.net版本不同数量不同,大概在cpu核心数~32768之间。例如使用的是32位系统的.net 4.0,最大线程数是1024个。也就是同时能够处理的任务数量是1024个,当并发的任务超过这个的时候,就会进入队列等待。在前台反馈给用户的就是一个HTTP 503错误。而且新开和保持一个线程都需要消耗硬体资源。

这时,使用async的好处就来了。一个web请求中的一些任务,特别是I/O任务,例如请求查询资料库,线程不会一直处于繁忙状态,会有空闲时间,当线程空闲的时候,会立即进入线程池,被分配到队列中等待的任务。这也就是所谓非同步的意思。

但是如果使用的sync的话,线程就会等待资料库查询返回结果后,再继续往后执行其它逻辑。一旦资料库遇到问题,线程就会一直处于被阻塞等待的状态。而不能进行其他任务的处理。这也是非同步能够提升一定的吞吐量的原因,但实际情况会比较复杂,并不能想答主那样简单粗暴的1000提升到2000那样,具体提升多少需要根据事情情况进行测算。

而且使用async也需要付出一定的代价的——线程的切换是需要消耗一定硬体资源,只是通常硬体的处理能力足够,我们会忽略这种消耗,真的不够用了, 也能够简单的扩容硬体提升。但如果滥用的话,甚至代码中出现了一些逻辑死循环之类的问题,这种消耗就会带来性能反而下降。比如,计算密集型的任务,async基本没有作用,而且会浪费更多资源。

了解了async和sync在线程处理上的区别之后,在实际的编码过程中就好选择了,一些简单的原则——

I/O密集型的任务可以使用async来处理,但是计算密集型的最好使用sync。

逻辑本身就非常简单的,使用sync可以使代码更简洁清晰,而且能减少一些开销。

如果你并不能很好的理解非同步、非阻塞和阻塞的概念,那还是老老实实使用sync,因为大多数项目的并发其实并不能让它产生瓶颈。


非同步定义

关于非同步的定义,网上有很多不同的形式,但是归根结底中心思想是不变的。无论是在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的执行速度来比较有著天壤之别。

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

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

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

非同步的回调

通过以上介绍,我们一定要记住一个知识点:非同步需要回调机制。非同步操作之所以能在执行结果完成之后继续执行下面程序完全归功于回调,这也是所有非同步场景的核心所在,前到js的非同步回调,后到cpu内核空间copy数据到用户空间完成通知 等等非同步场景,回调无处不在。说道回调大部分语言都是注册一个回调函数,比如js会把回调的方法注册到执行的队列,c#会把回调注册到IOCP。这里延伸一下,在很多系统里,很多IO网路模型其实是属于同步范畴的,比如多路复用技术,真正非同步非阻塞的推荐windows下的IOCP。

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

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

非同步的特点

优势

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

劣势

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

写在最后

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

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

具体的详细信息可以查看这个

程序员修神之路--问世间非同步为何物??

mp.weixin.qq.com图标

我简短的总结一下吧,webapi非同步介面就如同socket编程中使用IOCP非同步介面一样,微软帮你把很多东西都隐藏掉了,你不需要太了解如何做多线程开发也可以实现非同步,目的自然是为了提高网站吞吐量,提高线程使用率。

当然使用的时候很多细节还是要注意,否则用了async-await和没用没区别,如果调用栈中间既有同步调用,又有非同步调用,特别是同步方法调非同步的情况,可能会导致async链条断裂,导致整个调用以同步方式进行。


线程是一种稀缺的资源(相对于每秒万计的请求),尽量避免等待。

函数栈也是重资产,很多并行任务并不需要独立的函数栈。


非同步代码写起来简单。而且非同步代码可以专门的优化。。

没其它的了,就是代码简单。

在非同步代码出现之前,回调黑洞了解一下,then 链条只是在逻辑上更好理解一些,写起来仍然不好看。非同步标识才让这一切简化到不能再简化了。


io线程非同步,简单的讲就是能增加吞吐量,能增加工作线程的利用率,当然你webapi里得有io操作才有作用,不然反而有额外开销,不局限于网路io,只要是操作系统io线程负责的操作都有这种效果。


推荐阅读:
相关文章