同步和异步的关注点是用户线程和内核的交互方式。同步指的是用户线程发起I/O请求后需要等待或者轮询内核I/O操作完成后才能继续执行;异步是指用户线程发起I/O请求后仍然继续执行,当内核I/O操作完成后会通知用户线程,或者调用用户线程注册的回调函数。
阻塞和非阻塞的关注点是用户线程调用内核操作的方式。阻塞是指I/O操作需要彻底完成后才返回到用户空间,非阻塞是指I/O操作被调用后立即返回给用户一个状态值,无需等到I/O操作彻底完成。
同步阻塞I/O
同步阻塞I/O模型,最简单的I/O模型,用户线程在内核进行I/O操作时被阻塞。
同步阻塞I/O的操作为,用户线程通过系统调用read()函数,发起I/O读操作,由用户空间转到内核空间。内核等到数据包到达之后,然后将接收到的数据拷贝到用户空间,完成read()操作。整个过程中,用户线程需要等待read()函数将数据读取到用户空间缓冲区,才能够继续处理接收的数据。这将导致用户线程发起I/O请求时,不能做任何事情,对CPU的资源利用率不够。
同步非阻塞I/O
同步非阻塞I/O模型是建立在同步阻塞I/O模型的基础上的,用户线程发起I/O请求之后可以立即返回。
同步非阻塞I/O的操作,上面已经说了,用户线程发起I/O请求之后立即返回,但此时并未读取到任何数据,用户线程需要不断发起I/O请求,直到数据到达之后,才真正地读到数据,继续执行。整个过程中,用户需要不断地调用read(),尝试是否可以读取成功,读取成功才继续处理接收的数据。这样,虽然用户线程每次发起I/O请求后可以立即返回,但是这并没有什么意义,为了等到数据,仍然需要不断轮询、重复请求,消耗了大量的CPU资源,因此一般很少使用这种模型。
I/O多路复用
I/O多路复用模型是建立在内核提供的多路分离函数select基础之上的,使用select函数可以避免同步非阻塞I/O模型中轮询等待的问题。
I/O多路复用的操作位,用户线程首先将需要进行的I/O操作添加到select中,然后阻塞等待select系统调用返回。当数据到达时,I/O操作被激活,select函数返回,用户线程正式发起read()请求,读取数据并继续执行。
从流程上来看,使用select函数进行I/O请求和同步阻塞模型没有太大区别甚至还多了添加监视I/O,以及调用select函数的额外操作,效率更差。但是,使用select函数以后的最大优势就是可以在一个线程内同时处理多个I/O请求。用户可以注册多个I/O,然后不断地调用select读取被激活的I/O,即可达到在同一个线程内同时处理多个I/O请求的目的。而在同步阻塞模型中,用户必须通过多线程的方式才能达到这个目的地。
然而,使用select函数的优点并不仅限于此。虽然上述方式允许单线程内处理多个I/O请求,但是每个I/O请求的过程还是阻塞的(在select函数上阻塞),平均时间甚至比同步阻塞I/O模型还要长。如果用户线程只注册自己感兴趣的I/O请求,然后去做自己的事情,等到数据到来时再进行处理,那么则可以提高CPU的利用率。
I/O多路复用模型是最常使用的I/O模型,但是其异步程度还不够彻底,因为它使用了会阻塞线程的select系统调用。因此I/O多路复用模型只能称为异步阻塞I/O,而非真正的异步I/O。
Java NIO2:缓冲区