本文转发自技术世界,原文链接 http://www.jasongj.com/java/nio_reactor/
同步 vs. 异步
同步I/O
每个请求必须逐个地被处理,一个请求的处理会导致整个流程的暂时等待,
这些事件无法并发地执行。用户线程发起I/O请求后需要等待或者轮询内核I/O操作完成后才能继续执行。
同步意味着一个任务的某个处理过程会对多个线程在用串行化处理
异步I/O
多个请求可以并发地执行,一个请求或者任务的执行不会导致整个流程的暂时等待。
用户线程发起I/O请求后仍然继续执行,当内核I/O操作完成后会通知用户线程,或者调用用户线程注册的回调函数。
异步则意味着某个处理过程可以允许多个线程同时处理。
阻塞 vs. 非阻塞
阻塞
某个请求发出后,由于该请求操作需要的条件不满足,请求操作一直阻塞,不会返回,直到条件满足。
非阻塞
请求发出后,若该请求需要的条件不满足,则立即返回一个标志信息告知条件不满足,而不会一直等待。
一般需要通过循环判断请求条件是否满足来获取请求结果。
需要注意的是,阻塞并不等价于同步,而非阻塞并非等价于异步。
事实上这两组概念描述的是I/O模型中的两个不同维度。
同步和异步着重点在于多个任务执行过程中,后发起的任务是否必须等先发起的任务完成之后再进行。
而不管先发起的任务请求是阻塞等待完成,还是立即返回通过循环等待请求成功。
而阻塞和非阻塞重点在于请求的方法是否立即返回(或者说是否在条件不满足时被阻塞)。
Java中四种I/O模型
Java最早提供的blocking I/O即是阻塞I/O,
如上文所述,阻塞I/O下请求无法立即完成,则保持阻塞。
阻塞I/O分为如下两个阶段:
阶段1:等待数据就绪。网络 I/O 的情况就是等待远端数据陆续抵达;磁盘I/O的情况就是等待磁盘数据从磁盘上读取到内核态内存中。
阶段2:数据拷贝。出于系统安全,用户态的程序没有权限直接读取内核态内存,因此内核负责把内核态内存中的数据拷贝一份到用户态内存中。
而NIO即是非阻塞I/O,
非阻塞I/O请求包含如下三个阶段 1)socket设置为 NONBLOCK(非阻塞)就是告诉内核,
当所请求的I/O操作无法完成时,不要将线程睡眠,而是返回一个错误码(EWOULDBLOCK) ,这样请求就不会阻塞。
2)I/O操作函数将不断的测试数据是否已经准备好,如果没有准备好,继续测试,直到数据准备好为止。
整个I/O 请求的过程中,虽然用户线程每次发起I/O请求后可以立即返回,但是为了等到数据,仍需要不断地轮询、重复请求,消耗了大量的 CPU 的资源。
3)数据准备好了,从内核拷贝到用户空间。
一般很少直接使用这种模型,而是在其他I/O模型中使用非阻塞I/O 这一特性。这种方式对单个I/O 请求意义不大,但给I/O多路复用提供了条件。
同时通过NIO实现的Reactor模式即是 I/O复用模型的实现,
I/O多路复用(异步阻塞 I/O)
I/O多路复用会用到select或者poll函数,这两个函数也会使线程阻塞,
但是和阻塞I/O所不同的是,这两个函数可以同时阻塞多个I/O操作。
而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数。 从流程上来看,使用select函数进行I/O请求和同步阻塞模型没有太大的区别,甚至还多了添加监视Channel,以及调用select函数的额外操作,增加了额外工作。
但是,使用 select以后最大的优势是用户可以在一个线程内同时处理多个Channel的I/O请求。
用户可以注册多个Channel,然后不断地调用select读取被激活的Channel,即可达到在同一个线程内同时处理多个I/O请求的目的。
而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。 调用select/poll该方法由一个用户态线程负责轮询多个Channel,直到某个阶段1的数据就绪,再通知实际的用户线程执行阶段2的拷贝。
通过一个专职的用户态线程执行非阻塞I/O轮询,模拟实现了阶段一的异步化。
通过AIO实现的Proactor模式即是异步I/O模型的实现。
调用AIO_read 函数,告诉内核描述字,缓冲区指针,缓冲区大小,文件偏移以及通知的方式,然后立即返回。
当内核将数据拷贝到缓冲区后,再通知应用程序。所以异步I/O模式下,阶段1和阶段2全部由内核完成,完成不需要用户线程的参与。