四种主要的 IO 模型分别为 同步阻塞 IO、同步非阻塞 IO、IO 多路复用、异步 IO
阻塞和非阻塞
阻塞指的是调用线程一直在等待,而不能干别的事情。
非阻塞是指用户空间的程序不需要等待内核 IO 操作彻底完成,而是拿到内核返回的状态值就返回自己的空间,IO 操作能做就做,不能做就干其他事
Netty、Redis、Zookeeper高并发实战#^26174369-18-1201-1287
阻塞 IO 和非阻塞 IO
阻塞 IO 指的是线程在调用 IO 操作时需要等待内核缓冲区的数据准备好后才能返回用户空间执行用户操作,在这个期间内线程都是阻塞状态.
非阻塞 IO,指的是用户空间的程序不需要等待内核 IO 操作彻底完成,可以立即返回用户空间执行用户的操作,即处于非阻塞的状态,与此同时内核会立即返回给用户一个状态值
Netty、Redis、Zookeeper高并发实战#^26174369-18-701-796
Netty、Redis、Zookeeper高并发实战#^26174369-18-1087-1166
同步 IO 和异步 IO
同步 IO,是一种用户空间和内核空间 IO 发起方式,指的是用户空间的线程是主动发起 IO 调用的一方,内核是被动接受方。
异步 IO 则是反过来,内核是主动调用的一方,用户空间的线程是被调用的一方。
Netty、Redis、Zookeeper高并发实战#^26174369-18-867-971
同步阻塞 IO
我们已经介绍过了同步 IO 和阻塞 IO,我们可以联想一下同步阻塞 IO 是不是就是这俩组合起来呢?也就是由用户空间的线程发起主动 IO 调用,在内核缓存区数据没有准备好时这个线程将阻塞挂起,直到内核缓存区的操作彻底完成后这个线程才回到用户空间。
同步阻塞 IO 的优点
同步阻塞 IO 十分的简单易用,程序员不需要太多的处理,只需要调用对应 API 即可进行 IO 操作
同步阻塞 IO 的缺点
每一次 IO 调用都需要创建一个调用线程,而线程在等待内核执行操作时是阻塞的,虽然阻塞时线程是挂起的并不需要消耗很多 CPU 资源,但是在高并发场景下这样意味着短时间内会创建大量线程来维护网络连接,内存、线程切换的开销会非常巨大。
同步非阻塞 IO
同步非阻塞 IO 下应用的线程需要不断地进行 IO 系统的调用,轮询数据是否已经准备好,如果没有准备好久继续轮询,直到完成 IO 系统效用位置。在线程发起 IO 系统调用时,如果内核还在等待数据则线程立即返回,如果内核数据已经准备则线程会阻塞住等待内核缓冲区的操作彻底完成后返回用户空间。
同步非阻塞 IO 的优点
线程在内核层面做到了异步,内核在等待数据时线程会立即返回进行其他操作,减少了 IO 操作的阻塞时间
同步非阻塞 IO 的缺点
线程会不断轮询内核,占用大量的 CPU 时间,效率低下。在高并发场景中性能非常的低。
IO 多路复用
IO 多路复用是使用 select 或 epoll、poll 同时箭筒多个 IO 事件,通过将多个 IO 请求交给内核进行监听。和 同步非阻塞 Input-Output 类似多路复用 IO 也需要轮询。负责 selec 状态查询的线程需要不断的对 select 进行轮询,当查出 select 下有 IO 操作就绪时进行 IO 操作,将数据从内核缓冲区复制到用户缓冲区
IO 多路复用流程
- 选择器注册。先将需要 read 操作的目标文件描述符(socket 连接)提前注册在选择器中,在 about_Java 中式 Selector 类。然后开启 IO 多路复用的轮询流程
- 就绪状态的轮询。通过选择器查询所有注册过的文件描述符的 IO 就绪状态。通过查询的系统调用,内核会返回一个就绪的 socket 列表。任何一个 socket 准备好了就代表内核缓存区有数据了。
- 用户线程获得就绪的 socket 列表后,根据其中的 socket 连接发起 read 调用,用户线程阻塞。内核将数据从内核缓冲区中复制到用户缓冲区。
- 复制完成后,内核返回结果,用户线程才会解除阻塞的状态,用户线程读取到了数据,继续执行
IO 多路复用的优点
- 可以同时处理多个 IO 事件,从而提高程序的并发性
- 一个线程可以处理多个 IO 事件,减少了系统调用和线程切换和维护的开销,提高了系统性能
IO 多路复用的缺点
本质上 select、epoll 系统效用还是阻塞的,属于同步 IO。需要在读写事件就绪后由系统调用本身负责读写,也就是说读写过程依然是阻塞的。要彻底的解除线程的阻塞,就必须使用 异步 Input-Output
异步 IO
用户线程通过系统调用,向内核注册某个 IO 操作。内核在整个 IO 操作(包括数据准备、数据复制)完成后,通知用户程序,用户执行后续的业务操作。
Netty、Redis、Zookeeper高并发实战#^26174369-18-7286-7364
异步 IO 在内核等待数据和复制数据的两个阶段,用户线程都不是阻塞的。用户线程需要接收内核的 IO 操作完成事件,或者用户线程注册一个 IO 操作的回调函数,由内核完成操作后进行调用。而用户线程在执行 aio_read 调用后会立即返回,不会被阻塞。