怎样理解阻塞非阻塞与同步异步的区别?
还是不同层次的问题……一个网络包从应用程序A发到另一台电脑上的应用程序B,需要经历:从A的业务代码到A的软件框架从A的软件框架到计算机的操作系统内核从A所在计算机的内核到网卡从网卡经过网线发到交换机等设备,层层转发,到达B所在计算机的网卡从B所在计算机的网卡到B所在计算机的内核从B所在计算机的内核到B的程序的用户空间从B的软件框架到B的业务代码这个层级关系就像是过程调用一样,前一级调用后一级的功能,后一级返回一个结果给前一级(比如:成功,或者失败)。只有在单独一级的调用上,可以说同步还是异步的问题。所谓同步,是指调用协议中结果在调用完成时返回,这样调用的过程中参与双方都处于一个状态同步的过程。而异步,是指调用方发出请求就立即返回,请求甚至可能还没到达接收方,比如说放到了某个缓冲区中,等待对方取走或者第三方转交;而结果,则通过接收方主动推送,或调用方轮询来得到。从这个定义中,我们看,首先1和7,这取决于软件框架的设计,如果软件框架可以beginXXX,然后立即返回,这就是一种异步调用,再比如javascript当中的异步HTTP调用,传入参数时提供一个回调函数,回调函数在完成时调用,再比如协程模型,调用接口后马上切换到其他协程继续执行,在完成时由框架切换回到协程中,这都是典型的异步接口设计。而2和6,其他答主已经说得很好了,其实都需要调用方自己把数据在内核和用户空间里搬来搬去,其实都是同步接口,除非是IOCP这样的专门的异步传输接口,所以这一级其实是同步的,阻塞与非阻塞的区别其实是影响调用接口的结果(在特定条件下是否提前返回结果),而不是调用方式。3和5,内核一般通过缓冲区,使用DMI来传输数据,所以这一步又是异步的。4,以太网是个同步时序逻辑,随信号传输时钟,必须两边设备同时就绪了才能开始传输数据,这又是同步的。总结来说,讨论究竟是异步还是同步,一定要严格说明说的是哪一部分。其他答主说非阻塞是同步而不是异步,这毫无疑问是正确的,然而说某个框架是异步IO的框架,这也是正确的,因为说的其实是框架提供给业务代码的接口是异步的,不管是回调还是协程,比如说我们可以说某个库是异步的HTTPClient,并没有什么问题,因为说的是给业务代码的接口。由于通常异步的框架都需要在2中使用非阻塞的接口,的确会有很多人把非阻塞和异步混为一谈。
`阻塞/非阻塞`和`同步/异步`不是一个概念。举几个简单的例子。当进程调用一个进行IO操作的API时(比如read函数),在数据没有到达前,read会挂起,进程会卡住。在数据读取完毕返回给进程时,read返回(返回值为读取到的字节数,数据从内核拷贝到用户空间),然后进程继续执行。那么这次read调用,是阻塞的。非阻塞就是read在数据没有读取完毕前,就返回了(返回值为-1,errno设置为EAGAIN)。此时进程没有拿到需要的数据。那怎么办?这时候有两种办法。一种是同步。因为进程没办法知道数据什么时候才真正读取完毕了,所以需要每隔一段时间就去轮询一下(就是重新调用read,看是不是数据真的已经读取完毕了)。大部分场景中都不会使用这种方式。但在某些特殊的情况下效率会特别高。还有一种是异步。早期的异步实现方式是内核给进程发信号(SIGIO或者SIGPOLL)。数据读写完毕后,内核发信号给进程,然后进程内的信号处理函数再调用read读取数据(这时可以确保数据真的已经读取完毕了)。但这种方式有一个小小的瑕疵,就是在进程进行多个fd读写的时候,信号来的时候没办法分清到底是哪个fd上的数据已经真正准备好了。所以进程还是要对所有持有的fd进行read调用。后来的异步实现,就有了更好的select/poll/epoll(I/Omultiplexing)。现在基本上像比较流行的Nginx/Redis都用epoll(在FreeBSD上是kqueue)。具体的实现可以看看代码,比较简单。如果有人已经看到这里的话,那么他可能会觉得不耐烦,又是非阻塞又是异步的,为啥要搞这么麻烦,能不能简单点?其实这一切都是因为IO太慢,网卡和磁盘离CPU太远。那怎么提升IO的性能呢?除了使用非阻塞/异步的方式外,还可以看看我司的产品。磁盘IO性能不够的话,可以使用SSD云主机和物理云主机。网络IO性能不够的话,可以使用网络增强型云主机。
对unix来讲:阻塞式I/O(默认),非阻塞式I/O(nonblock),I/O复用(select/poll/epoll)都属于同步I/O,因为它们在数据由内核空间复制回进程缓冲区时都是阻塞的(不能干别的事)。只有异步I/O模型(AIO)是符合异步I/O操作的含义的,即在1数据准备完成、2由内核空间拷贝回缓冲区后通知进程,在等待通知的这段时间里可以干别的事。
我认为同步、异步、阻塞、非阻塞,是分3个层次的:CPU层次;线程层次;程序员感知层次。这几个概念之所以容易混淆,是因为没有分清楚是在哪个层次进行讨论。CPU层次在CPU层次,或者说操作系统进行IO和任务调度的层次,现代操作系统通常使用异步非阻塞方式进行IO(有少部分IO可能会使用同步非阻塞轮询),即发出IO请求之后,并不等待IO操作完成,而是继续执行下面的指令(非阻塞),IO操作和CPU指令互不干扰(异步),最后通过中断的方式来通知IO操作完成结果。线程层次在线程层次,或者说操作系统调度单元的层次,操作系统为了减轻程序员的思考负担,将底层的异步非阻塞的IO方式进行封装,把相关系统调用(如read,write等)以同步的方式展现出来。然而,同步阻塞的IO会使线程挂起,同步非阻塞的IO会消耗CPU资源在轮询上。为了解决这一问题,就有3种思路:多线程(同步阻塞);IO多路复用(select,poll,epoll)(同步非阻塞,严格地来讲,是把阻塞点改变了位置);直接暴露出异步的IO接口,如kernel-aio和IOCP(异步非阻塞)。程序员感知层次在Linux中,上面提到的第2种思路用得比较广泛,也是比较理想的解决方案。然而,直接使用select之类的接口,依然比较复杂,所以各种库和框架百花齐放,都试图对IO多路复用进行封装。此时,库和框架提供的API又可以选择是以同步的方式还是异步的方式来展现。如python的asyncio库中,就通过协程,提供了同步阻塞式的API;如node.js中,就通过回调函数,提供了异步非阻塞式的API。总结因此,我们在讨论同步、异步、阻塞、非阻塞时,必须先明确是在哪个层次进行讨论。比如node.js,我们可以说她在程序员感知层次提供了异步非阻塞的API,也可以说在Linux下,她在线程层次以同步非阻塞的epoll来实现。
以linux下tcpsocket编程为例:阻塞就是recv/read的时候socket接收缓冲区要是有数据就读,没数据我就一直睡觉赖着不走,直到有数据来了读完我才走。send/write的时候,要是发送缓冲区满了,没有空间继续发送了我也一直睡觉赖着不走,直到发送缓冲区腾出足够的空间让我把数据全部塞到发送缓冲区里我才走。(当然如果你通过setsockopt设置了读写超时,超时时间到了还是会返回-1和EAGAIN,不再睡觉等待)非阻塞就是recv/read的时候,要是接收缓冲区有数据我就读完,没有数据我直接带着返回的-1和EGAIN走人,绝不睡觉等待耽误时间。write/send的时候,要是发送缓冲区有足够的空间,就立刻把数据塞到发送缓冲区去,然后走人,如果发送缓存区满了,空间不足,那直接带着返回的-1和EAGAIN走人。至于IO多路复用,首先要理解的是,操作系统为你提供了一个功能,当你的某个socket接收缓存区有数据可读,或者发送缓冲区有空间可写的时候,它可以给你一个通知。这样当配合非阻塞的socket使用时,只有当系统通知我哪个描述符可读了,我才去执行read操作,可以保证每次read都能读到有效数据而不做纯返回-1和EAGAIN的无用功。写操作类似。操作系统的这个功能通过select/poll/epoll之类的系统调用函数来使用,这些函数都可以同时监视多个描述符的读写就绪状况,这样,多个描述符的I/O操作都能在一个线程内完成,这就叫I/O多路复用,这里的“复用”指的是复用同一个线程。至于事件驱动,其实是I/O多路复用的一个另外的称呼。至于异步同步,我们常见的linux下的网络编程模型大部分都是同步io,以读操作为例,本质上都是需要用户调用read/recv去从内核缓冲区把数据读完再处理业务逻辑。异步io则是内核已经把数据读好了,用户直接处理逻辑。异步IO在linux下一般是用aio库。
回答请先登录