首先我们要清楚同步异步和阻塞非阻塞是两种不同的概念,针对的是不同的层面:
同步:强调在发出一个请求或功能调用时,主动等待返回结果。在结果没得到前,不能返回或向下执行。
异步:强调在发出一个请求或功能调用时,不用等结果,继续向下执行,当有结果时,由被调用方通知主调方,主调方再拿结果
示例:
情景:你打电话到书店问老板有没有《操作系统》这本书。
同步情况:你等着老板去找书然后反馈给你结果。着重点在于你等待结果。
异步情况:你告诉老板,你先找,找到了回电话给我说结果。着重点是你不等待结果。
阻塞:就是程序执行到某一处条件未满足,程序则等待条件满足,满足后返回。
非阻塞:程序执行到某一处条件未满足则返回失败,不会使程序阻塞在这个地方。
还是以上述例子来讲:
阻塞情况:如果老板没有返回结果,你就把自己“挂起”,一直等着老板的结果。
非阻塞情况:你问老板有书没,老板没有准备好结果,那么则结果返回失败,你不会等在这里,而是去玩耍或是做其他的时期。但你需要每过一段时间再次询问一下老板有书没。
注意:阻塞不阻塞跟同步异步没有关系,只关注于程序等待调用时的状态。
关于同步异步与阻塞非阻塞的关系:同步异步均可以阻塞或非阻塞的方式实现,但实际上对于异步的阻塞非阻塞实现很少。下面看几个案例:
案例一:有进程A和进程B,进程A的功能是计算数据a + b + c + ......+ n,进程B的功能是产生数据b。那么进程A的运行需要进程B的运行结果。进程A要想执行,就必须等到进程B运行完成才行。在整个过程中进程A和进程B就是同步的过程,因为两个任务存在着时序的关系。
案例二:有进程A和进程B,进程A的一部分功能是计算数据a + b + c + ......+ n,计算完之后交由进程B打印。那么进程A的运行则不受进程B的影响。计算完之后将结果交由进程B即可,并且不影响后续执行。在整个过程中就是异步的关系。
案例三:以案例1为基础,进程A在进程B没有准备好数据b的情况下就一直等着,等进程B的结果然后再执行。这种情况就是同步阻塞。整体上进程A需要等待进程B的结果,属于同步,但在等待结果的过程中进程A把自己挂起等待进程B的结果,属于阻塞。即同步阻塞。
案例四:以案例1为基础,进程A在进程B没有准备好数据b的情况下,先去做别的事情,比如先计算a + b,但是在执行的过程中每隔一段时间询问进程B数据准备好了没有。这种情况就是同步非阻塞。整体上进程A需要等待进程B的结果,属于同步,但在等待结果的过程中进程B未准备好数据则直接返回失败,进程A没有阻塞在等待结果的状态,属于非阻塞。即同步非阻塞。
其中前4种都是同步I/O模型
1、阻塞式I/O
对于上述过程可分为两个阶段:
阻塞式I/O在这两个阶段都是被阻塞(等待)的,不去做其他事情。
2、非阻塞式I/O
非阻塞式I/O在应用进程发出recv请求时,如果内核数据并没有准备好,那么会直接返回一个error,说明数据未就绪,此时应用进程也不必死等在这里了,可以去做别的事情,但是需要定时的询问内核数据是否就绪。特点就是数据未就绪时不阻塞,但轮询的询问内核数据是否就绪。
3、I/O多路复用
由于非阻塞I/O需要主动的去轮询,并且后台中可能存在大量的任务进行,那么就会存在大量的用户态轮询,而I/O复用的思路就是将轮询的工作交给内核,并且同时监听多个任务数据是否就绪,当有一部分数据就绪时就会调用用户进程来处理。
I/O复用的函数有:select、poll、epoll(epoll效率最高)。这几个函数也会使进程阻塞,与阻塞式I/O不同的是,这些函数可以同时阻塞多个I/O操作。而且可以同时对多个操作(读写等)的I/O函数进行检测,直到有数据就绪时才真正调用I/O操作函数。
在少量i/o请求时使用i/o复用函数并不比阻塞i/o的效率高(延时降低),而且还会使降低(延时增加)。因为阻塞式i/o只在recv处阻塞,而多路复用时应用进程在recv处也处于阻塞状态,并且在select处也是阻塞状态。i/o多路复用的特点是处理多个连接时系统开销小。
4、信号式驱动i/o
首先我们允许Socket进行信号驱动IO,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。
5、异步i/o
相对于同步IO,异步IO不是顺序执行。用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到socket数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知。IO两个阶段,进程都是非阻塞的。