一、开胃菜
最近在学习Python中的socketserver时,从网上搜索了一下相关介绍,感觉它是socket IO复用+多线程的实现,但是度娘上大部分说它是socket 异步+多线程的实现,于是又再次度娘看了看网上各种介绍阻塞非阻塞IO,同步异步IO的文章,感觉越看越乱,估计很多作者本身都没真正的去了解非阻塞与异步,经常将两个概念混淆,误导他人。
二、正餐
讲到网络编程的I/O模型,总会涉及到这几个概念。问了很多人,没几个能清晰地讲出他们之间的区别联系,甚至在网络上也有很多不同的观点,也不知是中国文字释义的博大精深,还是本来这几个概念就是绕人不倦。今天我也来给大家讲解一下我对这几个概念的理解。
既然网络上众说纷纭,不如找个权威参考一下,这个权威就是《UNIX网络编程:卷一》第六章——I/O复用。书中向我们提及了5种类UNIX下可用的I/O模型:
阻塞式I/O;
非阻塞式I/O;
I/O复用(select,poll,epoll...);
信号驱动式I/O(SIGIO);
异步I/O(POSIX的aio_系列函数);
阻塞式I/O模型:默认情况下,所有套接字都是阻塞的。怎么理解?先理解这么个流程,一个输入操作通常包括两个不同阶段:
(1)等待数据准备好;
(2)从内核向进程复制数据。
对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所有等待分组到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用程序缓冲区。好,下面我们以阻塞套接字的recvfrom的的调用图来说明阻塞:
标红的这部分过程就是阻塞,直到阻塞结束recvfrom才能返回。
非阻塞式I/O: 以下这句话很重要:进程把一个套接字设置成非阻塞是在通知内核,当所请求的I/O操作非得把本进程投入睡眠才能完成时,不要把进程投入睡眠,而是返回一个错误。看看非阻塞的套接字的recvfrom操作如何进行:
I/O多路复用:虽然I/O多路复用的函数也是阻塞的,但是其与以上两种还是有不同的,I/O多路复用是阻塞在select,epoll这样的系统调用之上,而没有阻塞在真正的I/O系统调用如recvfrom之上。如图:
我们阻塞与select调用,等待数据报套接字变为可读。当select返回套接字可读这一条件时,我们调用recvfrom把数据从内核缓冲区复制到应用程序缓冲区。
我们比较I/O复用与阻塞IO,I/O复用并不显得有什么优势,事实上由于使用select需要两个而不是单个系统调用,I/O复用还稍有劣势。但是,使用select的真正优势在于我们可以等待多个描述符就绪。
信号驱动式I/O:这种模型的优势在于等待数据报到达期间进程不被阻塞。 主循环可以继续执行,只要等待来自信号处理函数的通知。
异步I/O:这类函数的工作机制是告知内核启动某个操作,并让内核在整个操作(包括将数据从内核拷贝到用户空间)完成后通知我们。这种模型与信号驱动模型最大的区别在于:信号驱动式IO是由内核通知我们何时可以启动一个IO操作,而异步IO模型是由内核通知我们IO操作何时完成。如图:
三、加餐
1. 同步IO与异步IO的对比:
POSIX对这两个术语的定义:
同步I/O操作:导致请求进程阻塞,直到I/O操作完成;
异步I/O操作:不导致请求进程阻塞。
2. 自己的理解
阻塞,非阻塞:进程/线程要访问的数据是否就绪,进程/线程是否需要等待;
同步,异步:访问数据的方式,同步需要主动读写数据,在读写数据的过程中还是会阻塞;异步只需要I/O操作完成的通知,并不主动读写数据,由操作系统内核完成数据的读写
3. 留给你的问题
相信如果你认真看完了上面的内容,下面的几个概念你应该会明白了:
- 同步阻塞IO ?
- 同步非阻塞IO ?
- 异步IO(异步有没有阻塞和非阻塞IO呢?)
四、夜宵
同步、异步、阻塞、非阻塞这几个概念,上面已经写得很清楚了。
这里我结合自己的理解,简单地聊一下为什么这几个概念容易混淆。如果有错误之处,恳请批评指正。
我认为同步、异步、阻塞、非阻塞,是分3个层次的:CPU层次;线程层次;程序员感知层次。
这几个概念之所以容易混淆,是因为没有分清楚是在哪个层次进行讨论。
CPU层次
在CPU层次,或者说操作系统进行IO和任务调度的层次,现代操作系统通常使用异步非阻塞方式进行IO(也有少部分IO可能会使用同步非阻塞轮询(6-2的非阻塞IO模型)),即发出IO请求之后,并不等待IO操作完成,而是继续执行下面的指令(非阻塞),IO操作和CPU指令互不干扰(异步),最后通过中断的方式来通知IO操作完成结果。
线程层次
在线程层次,或者说操作系统调度单元的层次,操作系统为了减轻程序员的思考负担,将底层的异步非阻塞的IO方式进行封装,把相关系统调用(如read,write等)以同步的方式展现出来。然而,同步阻塞的IO会使线程挂起,同步非阻塞的IO会消耗CPU资源在轮询上。
为了解决这一问题,就有3种思路:
- 多线程(同步阻塞);
- IO多路复用(select,poll,epoll)(同步非阻塞,严格地来讲,是把阻塞点改变了位置,阻塞在select);
- 直接暴露出异步的IO接口,如kernel-aio和IOCP(异步非阻塞)。
程序员感知层次
在Linux中, IO多路复用
用得比较广泛,也是比较理想的解决方案。然而,直接使用select之类的接口,依然比较复杂,所以各种库和框架百花齐放,都试图对IO多路复用进行封装。
此时,库和框架提供的API又可以选择是以同步的方式还是异步的方式来展现。如python的asyncio库中,就通过协程,提供了同步阻塞式的API;如node.js中,就通过回调函数,提供了异步非阻塞式的API。
总结
因此,我们在讨论同步、异步、阻塞、非阻塞时,必须先明确是在哪个层次进行讨论。比如node.js,我们可以说她在程序员感知层次
提供了异步非阻塞的API,也可以说在Linux下,她在线程层次
以多路复用的epoll来实现。
广义上的异步:调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用
狭义上的异步:异步是一种访问数据的方式,异步只需要I/O操作完成的通知
,并不主动读写数据
,由操作系统内核完成数据的读写
广义上的同步:发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了
狭义上的同步:同步是一种访问数据的方式,同步需要主动读写数据
,在读写数据的过程中会阻塞
(例:我们需要自己调用recvfrom将数据从内核缓冲读入应用缓冲,即需要主动读写)
阻塞:进程/线程要访问的数据未就绪,当前进程/线程会挂起
非阻塞:进程/线程要访问的数据未就绪,当前进程/线程不会挂起