您当前的位置:首页 > 文章 > 迄今为止把同步/异步/阻塞/非阻塞/BI

迄今为止把同步/异步/阻塞/非阻塞/BIO/NIO/AIO讲的最清楚的文章

作者:架构师成长历程 时间:2024-01-09 阅读数:118 人阅读
常规的误区

假设有一个展示用户详情的需求,分两步,先调用一个HTTP接口拿到详情数据,然后使用适合的视图展示详情数据。

如果网速很慢,代码发起一个HTTP请求后,就卡住不动了,直到十几秒后才拿到HTTP响应,然后继续往下执行。

这个时候你问别人,刚刚代码发起的这个请求是不是一个同步请求,对方一定回答是。这是对的,它确实是。

但你要问它为什么是呢?对方一定是这样回答的,“因为发起请求后,代码就卡住不动了,直到拿到响应后才可以继续往下执行”。

我相信很多人也都是这样认为的,其实这是不对的,是把因果关系搞反了: 不是因为代码卡住不动了才叫同步请求,而是因为它是同步请求所以代码才卡住不动了。 至于为什么能卡住不动,这是由操作系统和CPU决定的: 因为内核空间里的对应函数会卡住不动,造成用户空间发起的系统调用卡住不动,继而使程序里的用户代码卡住不动了。 因此卡住不动了只是同步请求的一个副作用,并不能用它来定义同步请求,那该如何定义呢?


同步和异步

所谓同步,指的是协同步调。既然叫协同,所以至少要有2个以上的事物存在。协同的结果就是: 多个事物不能同时进行,必须一个一个的来,上一个事物结束后,下一个事物才开始。 那当一个事物正在进行时,其它事物都在干嘛呢?

严格来讲这个并没有要求,但一般都是处于一种“等待”的状态,因为通常后面事物的正常进行都需要依赖前面事物的结果或前面事物正在使用的资源。 因此,可以认为,同步更希望关注的是从宏观整体来看,多个事物是一种逐个逐个的串行化关系,绝对不会出现交叉的情况。 所以,自然也不太会去关注某个瞬间某个具体事物是处于一个什么状态。

把这个理论应用的出神入化的非“排队”莫属。凡是在资源少需求多的场景下都会用到排队。

比如排队买火车票这件事: 其实售票大厅更在意的是旅客一个一个的到窗口去买票,因为一次只能买一张票。

即使大家一窝蜂的都围上去,还是一次只能卖一张票,何必呢?挤在一起又不安全。

只是有些人素质太差,非要往上挤,售票大厅迫不得已,采用排队这种形式来达到自己的目的,即一个一个的买票。

至于每个旅客排队时的状态,是看手机呀还是说话呀,根本不用去在意。

除了这种由于资源导致的同步外,还存在一种由于逻辑上的先后顺序导致的同步。 比如,先更新代码,然后再编译,接着再打包。这些操作由于后一步要使用上一步的结果,所以只能按照这种顺序一个一个的执行。

关于同步还需知道两个小的点: 一是范围,并不需要在全局范围内都去同步,只需要在某些关键的点执行同步即可。 比如食堂只有一个卖饭窗口,肯定是同步的,一个人买完,下一个人再买。但吃饭的时候也是一个人吃完,下一个人才开始吃吗?当然不是啦。 二是粒度,并不是只有大粒度的事物才有同步,小粒度的事物也有同步。

只不过小粒度的事物同步通常是天然支持的,而大粒度的事物同步往往需要手工处理。 比如两个线程的同步就需要手工处理,但一个线程里的两个语句天然就是同步的。

所谓异步,就是步调各异。既然是各异,那就是都不相同。所以结果就是: 多个事物可以你进行你的、我进行我的,谁都不用管谁,所有的事物都在同时进行中。

一言以蔽之,同步就是多个事物不能同时开工,异步就是多个事物可以同时开工。

注:一定要去体会“多个事物”,多个线程是多个事物,多个方法是多个事物,多个语句是多个事物,多个CPU指令是多个事物。等等等等。


阻塞和非阻塞

所谓阻塞,指的是阻碍堵塞。它的本意可以理解为由于遇到了障碍而造成的动弹不得。

所谓非阻塞,自然是和阻塞相对,可以理解为由于没有遇到障碍而继续畅通无阻。 对这两个词最好的诠释就是,当今中国一大交通难题,堵车: 汽车可以正常通行时,就是非阻塞。一旦堵上了,全部趴窝,一动不动,就是阻塞。

因此阻塞关注的是不能动,非阻塞关注的是可以动。

不能动的结果就是只能等待,可以动的结果就是继续前行。 因此和阻塞搭配的词一定是等待,和非阻塞搭配的词一定是进行。

回到程序里,阻塞同样意味着停下来等待,非阻塞表明可以继续向下执行。

阻塞和等待

等待只是阻塞的一个副作用而已,表明随着时间的流逝,没有任何有意义的事物发生或进行。

阻塞的真正含义是你关心的事物由于某些原因无法继续进行,因此让你等待。但没必要干等,你可以做一些其它无关的事物,因为这并不影响你对相关事物的等待。

在堵车时,你可以干等。也可以玩手机、和别人聊天,或者打牌、甚至先去吃饭都行。因为这些事物并不影响你对堵车的等待。不过你的车必须呆在原地。

在计算机里,是没有人这么灵活的,一般在阻塞时,选在干等,因为这最容易实现,只需要挂起线程,让出CPU即可。在条件满足时,会重新调度该线程。

两两组合

所谓同步/异步,关注的是能不能同时开工。

所谓阻塞/非阻塞,关注的是能不能动。

通过推理进行组合: 同步阻塞,不能同时开工,也不能动。只有一条小道,一次只能过一辆车,可悲的是还的堵上了。

同步非阻塞,不能同时开工,但可以动。只有一条小道,一次只能过一辆车,幸运的是可以正常通行。

异步阻塞,可以同时开工,但不可以动。有多条路,每条路都可以跑车,可气的是全都的堵上了。

异步非阻塞,可以工时开工,也可以动。有多条路,每条路都可以跑车,很爽的是全都可以正常通行。 是不是很容易理解啊。其实它们的关注点是不同的,只要搞明白了这点,组合起来也不是事儿。

回到程序里,把它们和线程关联起来: 同步阻塞,相当于一个线程在等待。

同步非阻塞,相当于一个线程在正常运行。

异步阻塞,相当于多个线程都在等待。

异步非阻塞,相当于多个线程都在正常运行。

I/O

IO指的就是读入/写出数据的过程,和等待读入/写出数据的过程。一旦拿到数据后就变成了数据操作了,就不是IO了。 拿网络IO来说,等待的过程就是数据从网络到网卡再到内核空间。读写的过程就是内核空间和用户空间的相互拷贝。

所以IO就包括两个过程,一个是等待数据的过程,一个是读写(拷贝)数据的过程。而且还要明白,一定能包括操作数据的过程。


阻塞IO和非阻塞IO

应用程序都是运行在用户空间的,所以它们能操作的数据也都在用户空间。按照这样子来理解,只要数据没有到达用户空间,用户线程就操作不了。

如果此时用户线程已经参与,那它一定会被阻塞在IO上。这就是常说的阻塞IO。用户线程被阻塞在等待数据上或拷贝数据上。

非阻塞IO就是用户线程不参与以上两个过程,即数据已经拷贝到用户空间后,才去通知用户线程,一上来就可以直接操作数据了。

用户线程没有因为IO的事情出现阻塞,这就是常说的非阻塞IO。

同步IO和同步阻塞IO

按照上文中对同步的理解,同步IO是指发起IO请求后,必须拿到IO的数据才可以继续执行。 按照程序的表现形式又分为两种: 在等待数据的过程中,和拷贝数据的过程中,线程都在阻塞,这就是同步阻塞IO。

在等待数据的过程中,线程采用死循环式轮询,在拷贝数据的过程中,线程在阻塞,这其实还是同步阻塞IO。

网上很多文章把第二种归为同步非阻塞IO,这肯定是错误的,它一定是阻塞IO,因为拷贝数据的过程,线程是阻塞的。 严格来讲,在IO的概念上,同步和非阻塞是不可能搭配的,因为它们是一对相悖的概念。 同步IO意味着必须拿到IO的数据,才可以继续执行。因为后续操作依赖IO数据,所以它必须是阻塞的。

非阻塞IO意味着发起IO请求后,可以继续往下执行。说明后续执行不依赖于IO数据,所以它肯定不是同步的。 因此,在IO上,同步和非阻塞是互斥的,所以不存在同步非阻塞IO。但同步非阻塞是存在的,那不叫IO,叫操作数据了。

所以,同步IO一定是阻塞IO,同步IO也就是同步阻塞IO。


异步IO和异步阻塞/非阻塞IO

按照上文中对异步的理解,异步IO是指发起IO请求后,不用拿到IO的数据就可以继续执行。

用户线程的继续执行,和操作系统准备IO数据的过程是同时进行的,因此才叫做异步IO。 按照IO数据的两个过程,又可以分为两种: 在等待数据的过程中,用户线程继续执行,在拷贝数据的过程中,线程在阻塞,这就是异步阻塞IO。

在等待数据的过程中,和拷贝数据的过程中,用户线程都在继续执行,这就是异步非阻塞IO。

第一种情况是,用户线程没有参与数据等待的过程,所以它是异步的。但用户线程参与了数据拷贝的过程,所以它又是阻塞的。合起来就是异步阻塞IO。

第二种情况是,用户线程既没有参与等待过程也没有参与拷贝过程,所以它是异步的。当它接到通知时,数据已经准备好了,它没有因为IO数据而阻塞过,所以它又是非阻塞的。合起来就是异步非阻塞IO。

IO模型

网络IO的本质是Socket的读取,Socket在Linux系统被抽象为流,IO可以理解为对流的操作。Linux标准文件访问方式如下:


当发起一个read操作的时候,会经历2个阶段:

  1. 等待数据准备;
  2. 将数据从内核拷贝到进程中;

对于socket流也会经历两个阶段:

  1. 将磁盘或者其他设备到达以后的信息,拷贝到内核的缓存区中;
  2. 将内核的缓存区的数据复制到应用进程缓存中;

网络应用需要处理的无非就是两大类问题,网络IO,数据计算。相对于后者,网络IO的延迟,给应用带来的性能瓶颈大于后者,接下来我们介绍下IO模型:

同步阻塞IO(blocking IO)

同步阻塞 IO 模型是最常用的一个模型,也是最简单的模型。在Linux中,默认情况下所有的socket都是blocking。阻塞就是进程休息, CPU处理其它进程去了。

用户空间的应用程序执行一个系统调用(recvform),这会导致应用程序阻塞,直到数据准备好,并且将数据从内核复制到用户进程,最后进程再处理数据,在等待数据到处理数据的两个阶段,整个进程都被阻塞。不能处理别的网络IO。调用应用程序处于一种不再消费 CPU 而只是简单等待响应的状态,因此从处理的角度来看,这是非常有效的。在调用recv()/recvfrom()函数时,发生在内核中等待数据和复制数据的过程,大致如下图:


  1. 应用进程向内核发起recfrom读取数据;
  2. 准备数据包(应用进程阻塞);
  3. 将数据从内核负责到应用空间;
  4. 复制完成后,返回成功提示;
特点

同步阻塞 IO 整个过程都是阻塞的,对于用户可以及时返回数据,无延迟,对于开发者来说简单省事,对于系统来说无法应对高并发访问,以及用户在等待期间也无法进行其他任何操作。

同步非阻塞IO(nonblocking IO)

同步非阻塞就是采用轮询的方式,定时去查看数据是否准备完成。在这种模型中,进程是以非阻塞的形式打开的。IO 操作不会立即完成,如果该缓冲区没有数据的话,就会直接返回一个EWOULDBLOCK错误,不会让应用一直等待中。

非阻塞IO也会进行recvform系统调用,检查数据是否准备好,与阻塞IO不一样,非阻塞将大的整片时间的阻塞分成N多的小的阻塞, 所以进程不断地有机会被CPU访问。也就是说非阻塞的recvform系统调用调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,此时会返回一个error。


  1. 应用进程向内核发起recvfrom读取数据;
  2. 没有数据报准备好,即刻返回EWOULDBLOCK错误码;
  3. 应用进程向内核发起recvfrom读取数据;
  4. 已有数据包准备好就进行一下步骤,否则还是返回错误码;
  5. 将数据从内核拷贝到用户空间;
  6. 完成后,返回成功提示;
特点

同步非阻塞方式相比同步阻塞方式,在等待任务期间进程可以处理其他事情,缺点的话就是因为采用定时轮询的方式,导致系统整体的吞吐量降低。

IO多路复用( IO multiplexing)

同步非阻塞方式需要不断主动轮询,轮询占据了很大一部分过程,轮询会消耗大量的CPU时间,当并发情况下服务器很可能一瞬间会收到几十上百万的请求,这种情况下同步非阻塞IO需要创建几十上百万的线程去读取数据,同时又因为应用线程是不知道什么时候会有数据读取,为了保证消息能及时读取到,那么这些线程自己必须不断的向内核发送recvfrom 请求来读取数据。这么多的线程不断调用recvfrom 请求数据,明细是对线程资源的浪费。

于是有人就想到了由一个线程循环查询多个任务的完成状态(fd文件描述符),只要有任何一个任务完成,就去处理它。这样就可以只需要一个或几个线程就可以完成数据状态询问的操作,当有数据准备就绪之后再分配对应的线程去读取数据,这么做就可以节省出大量的线程资源出来,这个就是IO多路复用。


  1. 应用进程向内核发起select调用;
  2. kernel会监听所有select负责的socket;
  3. 任何一个socket中的数据准备好了,select就会返回;
  4. 应用进程再调用recvfrom操作,将数据从内核拷贝到用户空间;
  5. 完成后,返回成功提示;
特点

IO多路复用与同步非阻塞相比,应用线程通过调select/poll之后,阻塞住,进入到内核态后由内核线程来轮询这个应用线程所关注的所有文件描述符对应的缓冲区是否有数据准备就绪,只要有一个缓冲区数据准备就绪,就可以进行数据拷贝然后返回给用户线程,这种方式就减少了用户线程的不断轮询以及避免在每次轮询时所产生的两次上下文切换过程。

此外就是IO多路复用模型可以同时阻塞多个I/O操作。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时(这里并不是全部数据可读或可写),才真正调用I/O操作函数。

此外还需要注意的是,IO多路复用既然可以处理多个IO,也就带来了新的问题,多个IO之间的顺序变得不确定了。

信号驱动IO(signal-driven IO)

IO多路复用解决了一个线程或者多个线程可以监控多个文件描述符的问题,但是select是采用轮询的方式来监控多个文件描述符的,通过不断的轮询文件描述符的可读状态来知道是否有可读的数据,这样无脑的轮询就显得有点浪费,因为大部分情况下的轮询都是无效的,于是乎有人就想,能不能不要总是去轮询数据是否准备就绪,能不能发出请求后,等数据准备好了在通知我,所以这样就出现了信号驱动IO。

信号驱动IO不是用循环请求询问的方式去监控数据就绪状态,而是在调用sigaction时候建立一个SIGIO的信号联系,当内核数据准备好之后再通过SIGIO信号,通知线程数据准备好后的可读状态,当线程收到可读状态的信号后,此时再向内核发起recvfrom读取数据的请求。因为信号驱动IO的模型下,应用线程在发出信号监控后即可返回,不会阻塞,所以这样的方式下,一个应用线程也可以同时监控多个文件描述符。


  1. 应用进程开启套接口信号驱动IO功能,通过系统调用sigaction执行一个信号处理函数,请求即刻返回;
  2. 当数据准备就绪时,就生成对应进程的SIGIO信号,通过信号回调通知应用进程;
  3. 应用进程再调用recvfrom操作,将数据从内核拷贝到用户空间;
  4. 完成后,返回成功提示;
特点

信号驱动IO相比于IO多路复用,在通过这种建立信号关联的方式,实现了发出请求后只需要等待数据就绪的通知即可,这样就可以避免大量无效的数据状态轮询操作。

异步非阻塞 IO(asynchronous IO)

不管是IO多路复用还是信号驱动,我们要读取数据的时候,总是要发起两阶段的请求,第一次发送select请求,询问数据状态是否准备好,第二次发送recevform请求读取数据。这个时候我们会有一个疑问,为什么在读数据之前总要有个数据就绪的状态,可不可以应用进程只需要向内核发送一个read 请求,告诉内核要读取数据后,就立即返回。当内核数据准备就绪,内核会主动把数据从内核复制到用户空间,等所有操作都完成之后,内核会发起一个通知告诉应用,所以这样就出现了异步非阻塞 IO模型。

异步非阻塞IO模型应用进程发起aio_read操作之后,立刻就可以开始去做其它的事。后续的操作有内核接管,当内核收到一个asynchronous read之后,它会立刻返回,不会对用户进程产生任何block。然后,内核会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成以后,内核会给用户进程发送一个signal或执行一个基于线程的回调函数来完成这次 IO 处理过程。


  1. 应用进程发起aio_read操作,立即返回;
  2. 内核等待数据准备完成,然后将数据拷贝到用户内存;
  3. 内核会给用户进程发送一个signal信号;
  4. 收到信号,返回成功提示;
特点

异步非阻塞 IO相比于信号驱动IO,信号驱动IO模型只是由内核通知我们可以开始下一个IO操作,而异步非阻塞 IO模型是由内核通知我们操作什么时候完成。

五种IO模型总结
阻塞IO和非阻塞IO区别

调用阻塞IO会一直阻塞住对应的进程直到操作完成,而非阻塞IO在内核还准备数据的情况下会立刻返回。

同步IO和异步IO区别

两者的区别就在于同步IO做IO操作的时候会将进程阻塞,也就是应用进程调用recvfrom操作,recvfrom会将数据从内核拷贝到用户内存中,在这段时间内,进程是被阻塞的。


举个例子

小王去买火车票,三天后买到一张退票。参演人员(老李,黄牛,售票员,快递员),往返车站耗费1小时。

同步阻塞 IO

小王去火车站买票,排队三天买到一张退票。整个三天小王无法做其他事情,只能做买票的一件事情。

同步非阻塞 IO

小王去火车站买票,隔一天去火车站问有没有退票,三天后买到一张票。整个过程中小王需要往返3次,往返消耗3小时,这个期间小王可以做其他事情。

IO多路复用
select/poll

小王去火车站买票,委托黄牛购买,然后每隔12小时打电话询问黄牛,黄牛三天买到票,然后小王去火车站交钱领票。整个小王需要往返2次,往返消耗2小时,黄牛需要手续费100,打电话6次,这里的黄牛就是select/poll,多路指的就是一个黄牛可以服务多个人。

epoll

小王去火车站买票,委托黄牛购买,黄牛买到后即通知小王去领,然后小王去火车站交钱领票。整个过程小王需要往返2次,往返消耗2小时,黄牛需要手续费100,无需打电话。

信号驱动IO

小王去火车站买票,售票员留下电话,有票后,售票员电话通知小王,然后小王去火车站交钱领票。整个过程小王需要往返2次,往返消耗2小时,无手续费,无需打电话。

异步非阻塞 IO

小王去火车站买票,给售票员留下电话,有票后,售票员电话通知小王并快递送票上门。整个过程小王需要往返1次,往返消耗1小时,无手续费,无需打电话。


本站大部分文章、数据、图片均来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了您的权益请来信告知我们删除。邮箱:1451803763@qq.com