背景
学习IT其实大部分也是学习概念,今天我在一个技术分享群里看到内部框架由阻塞是改成非阻塞式(基于netty的),那我就不由想问非阻塞式好在哪里,业界是怎么做的。
面对这些问题肯定得要写一篇文章来梳理了。
BIO 、NIO、AIO(NIO.2)
a).客户端阻塞
如果客户端只有一个线程,这个线程发起读取文件的操作必须等待IO流返回,线程(客户端)才能做其他的事。
ServerSocket ss = createServerSocket();
ss.bind(new InetSocketAddress(8080));
for (;;) {
Socket socket = ss.accept();
handle(socket);//只有一个线程
}
b).线程级别阻塞 BIO
客户端一个线程情况下,一个线程导致整个客户端阻塞。那么我们可以使用多线程,一部分线程在等待IO操作返回其他线程可以继续做其他的事。此时从客户端角度来说,客户端没有闲着。但是从单个线程角度来说,等待IO返回的线程依然是阻塞的。
在这种情况下是一个连接一个线程。
其伪代码如下:
ServerSocket ss = createServerSocket();
ss.bind(new InetSocketAddress(8080));
for (;;) {
Socket socket = ss.accept();
new Handler(socket).start();//创建多个线程
}
c).I/O多路复用 (操作系统级别) NIO
使用BIO在大量的IO并发操作中也会有一些瓶颈,比如每个连接创建一个线程,创建线程需要内存和cpu的资源,同时操作系统有一个最大线程数的限制。如果客户端请求过多,服务端程序可能会因为不堪重负而拒绝客户端的请求。
所以我们没有必要每个连接都创建一个线程,当有请求(有数据到达)时候才创建一个线程来处理数据,这样同一个线程还可以为多个请求服务。
多个请求共用一个线程是怎么实现的呢?当有连接过来的时候,这个连接会被注册到多路复用器上面。当我对多路复用器进行轮训发现有请求过来时才开启一个线程处理(我们称它为请求线程)。
请求线程过来后,可以会处理一些后端的资源比如jdbc等长连接,此时请求线程一直没有释放。在大量的并发时还是会有问题,从本质上来说NIO比BIO好在过滤掉了无用的连接,当很多有用连接(请求过来时),NIO和BIO都面对把资源耗费在长的IO处理上导致不能处理其他的操作。
此时要解决这样的问题我们就需要用到队列,即我把请求的数据和资源放入队列然后在全局的地方保持这种现场,这样请求线程把资源放入队列后可以继续去处理其他的事情(用空间来换时间),后端应用只要执行队列里的任务,执行完后在全局的地方得到线程通知前端。
d).AIO (缓冲区队列-空间换时间)
在AIO中API的read或write均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作的话,操作系统在将write方法写入完毕时操作系统主动通知应用程序。
AsynchronousSocketChannel
AsynchronousServerSocketChannel
AsynchronousFileChannel
AsynchronousDatagramChannel
其中的read/write方法,会返回一个带回调函数的对象,当执行完读取/写入操作后,直接调用回调函数。
多路复用是什么?
多路复用是指多个描述符的I/O操作都能在一个线程内并发交替地顺序完成,这里的复用是指复用同一个线程,操作系统的这个功能通过select/poll/epoll/kqueue之类的系统调用函数来使用,这些函数都可以同时监视多个描述符的读写就绪状况。
IO多路复用并不是使单个连接处理更快而是为了处理更多的连接,而AIO是为了使单个连接处理更快,不用等待。
select
1).注意Selector的select()方法为同步方法,如果当前没有I/O ready就会一直阻塞在那里.
2).一旦只要有一个ready,就会返回并执行下面的代码。相比于BIO,Handler线程永远只处理I/O ready的客户端请求,这样大幅度提高了吞吐量。
3).Mina/Netty和Node.js底层也是类似的实现。
Selector selector = Selector.open();
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
while(true) {
selector.select();
Iterator iterator = selector.selectedKeys().iterator();
while(iterator.hasNext()) {
SelectionKey key = iterator.next();
new Handler(key).start();
}
}
注意多路复用是操作系统的机制,不同平台底层对Selector/Channel的实现不同,不同的JDK对其也有不同的实现。比如Windows基于IOCP,Linux Kernel基于select/poll或epoll,FreeBSD基于kqueue等等。
当然这里涉及到用户态到内核态的切换,
多路复用的实现细节
I/O多路复用技术通过把多个I/O的阻塞复用到同一个select阻塞上,一个进程监视多个描述符,一旦某个描述符就位, 能够通知程序进行读写操作。
目前支持多路复用的系统调用有select, poll, epoll。
不管采用什么的系统调用,应用程序都要从用户态中访问,复用技术是操作系统底层操作,所以这里涉及到用户态和内核态的切换。
select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
select:
主动监视多个文件句柄的状态变化,程序会阻塞在select处等待,直到有文件描述符就绪或超时。
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)
poll:
poll的实现和select非常相似,只是描述fd集合的方式不同,poll使用pollfd结构而不是select的fd_set结构,其他的都差不多。
epoll:
而epoll提供了三个函数,epoll_create,epoll_ctl和epoll_wait,epoll_create是创建一个epoll句柄;epoll_ctl是注册要监听的事件类型;epoll_wait则是等待事件的产生。
select,poll,epoll具体详细步骤以后要专门写一篇博客,这里只是参考了(http://www.cnblogs.com/Anker/p/3265058.html)
java中的直接内存与NIO
java NIO中引入一种基于Channel和Buffer的IO方式,他可以使用native函数直接分配堆外内存这样,然后通过堆中的DirectoryByteBuffer对象作为这块内存的引用。BIO
写在后面的话
根绝场景分析从单个客户端单线程,多线程,用多路复用解决连接问题在后面处理开多线程,通过队列解决后台处理阻塞的问题。这就是从典型IO调用从前端往后端逐步优化的过程,我们就需要从整个context逐步分析和优化。这样才算是学活了。