1.1 同步、异步、阻塞、非阻塞
同步 VS 异步
同步与异步与(被调用者)消息的通知机制有关。
同步
同步处理是指被调用方得到最终结果之后才返回给调用方。比如,调用readfrom系统调用时,必须等待IO操作完成才返回给调用方。
异步
异步处理是指没得到最终结果时被调用方先返回应答,计算完最终结果后再通知并返回给调用方。比如:调用aio_read系统调用时,不必等IO操作完成就直接返回,调用结果通过信号来通知调用者。
阻塞 VS 非阻塞
阻塞与非阻塞与(调用者)等待消息通知时的状态有关。
阻塞
阻塞调用是指调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果之后才会返回。
非阻塞
非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该调用不会阻塞当前线程。
两者的最大区别在于被调用方在收到请求到返回结果之前的这段时间内,调用方是否一直在等待。
阻塞是指调用方一直在等待而且别的事情什么都不做;非阻塞是指调用方先去忙别的事情。
举例
以小明下载文件为例,对上述概念做一梳理:
1)同步阻塞
同步阻塞:小明一直盯着下载进度条,到 100% 的时候就完成。
同步:等待下载完成通知;
阻塞:等待下载完成通知过程中,不能做其他任务处理;
2)同步非阻塞
同步非阻塞:小明提交下载任务后就去干别的,每过一段时间就去瞄一眼进度条,看到 100% 就完成。
同步:等待下载完成通知;
非阻塞:等待下载完成通知过程中,去干别的任务了,只是时不时会瞄一眼进度条;【小明必须要在两个任务间切换,关注下载进度】
3)异步阻塞
异步阻塞:小明换了个有下载完成通知功能的软件,下载完成就“叮”一声。不过小明仍然一直等待“叮”的声音。
异步:下载完成“叮”一声通知;
阻塞:等待下载完成“叮”一声通知过程中,不能做其他任务处理;
4)异步非阻塞
异步非阻塞:仍然是那个会“叮”一声的下载软件,小明提交下载任务后就去干别的,听到“叮”的一声就知道完成了。
异步:下载完成“叮”一声通知;
非阻塞:等待下载完成“叮”一声通知过程中,去干别的任务了,只需要接收“叮”声通知。
1.2 Linux IO模型
IO执行的两个阶段
一个输入操作时,数据并不会直接拷贝到程序的程序缓冲区,通常包括两个不同的阶段:
-
1)等待数据准备好;(内核准备数据)
-
2)从内核向进程复制数据 (从内核复制数据到用户进程)
实际应用程序在系统调用完成上面的 2 步操作时,根据
(调用方)调用方式的阻塞、非阻塞,
(被调用方)操作系统在处理应用程序请求时,处理方式的同步、异步处理的不同,
可以分为 5 种 I/O 模型
Linux的5种IO模型
概念说明 |
优缺点 | |||
---|---|---|---|---|
阻塞I/O |
|
在linux中,默认情况下,所有套接字都是阻塞的。 进程调用一个recvfrom请求,但是它不能立刻收到回复,直到数据返回,然后将数据从内核空间复制到程序空间。 |
阻塞IO: 在IO执行的两个阶段中,进程都处于blocked(阻塞)状态,在等待数据返回的过程中不能做其他的工作,只能阻塞的等在那里。 |
优点:程序简单,在阻塞等待数据期间进程/线程挂起,基本不会占用 CPU 资源。 |
非阻塞I/O |
|
在非阻塞式 I/O 模型中,应用程序把一个套接字设置为非阻塞时,就是告诉内核,当所请求的 I/O 操作无法完成时,不要将进程睡眠,而是返回一个错误。应用程序基于 I/O 操作函数将不断的轮询数据是否已经准备好,如果没有准备好,继续轮询,直到数据准备好为止。 |
非阻塞IO: 在非阻塞状态下,IO执行的等待阶段并不是完全的阻塞的,但是第二个阶段依然处于一个阻塞状态。 |
优点:不会阻塞在内核的等待数据过程,每次发起的 I/O 请求可以立即返回,不用阻塞等待,实时性较好。 |
I/O多路复用 (select、poll、 epoll) |
|
在 I/O 复用模型中,会用到 select 或 poll 函数或 epoll 函数(Linux 2.6 以后的内核开始支持),这两个函数也会使进程阻塞,但是和阻塞 I/O 有所不同。 |
IO多路复用: 单个进程/线程可以同时处理多个网络连接的IO。 它的基本原理就是不再由应用程序自己监视连接,取而代之由内核替应用程序监视文件描述符。 |
优点:可以基于一个阻塞对象,同时在多个描述符上等待就绪,而不是使用多个线程(每个文件描述符一个线程),这样可以大大节省系统资源。 |
信号驱动式I/O |
在信号驱动式 I/O 模型中,应用程序使用套接口进行信号驱动 I/O,并安装一个信号处理函数,进程继续运行并不阻塞。 |
优点:线程并没有在等待数据时被阻塞,可以提高资源的利用率。 |
||
异步I/O模型 |
|
应用程序告知内核启动某个操作,并让内核在整个操作(包括将数据从内核拷贝到应用程序的缓冲区)完成后通知应用程序。 |
优点:异步 I/O 能够充分利用 DMA 特性,让 I/O 操作与计算重叠。 |
五种IO模型比较
从上图中我们可以看出,越往后,阻塞越少,理论上效率也是最优。
前四种I/O模型都是同步I/O操作,他们的区别在于第一阶段,而他们的第二阶段是一样的:在数据从内核复制到应用缓冲区期间(用户空间),进程阻塞于recvfrom调用。
相反,异步I/O模型在这等待数据和接收数据的这两个阶段里面都是非阻塞的,可以处理其他的逻辑用户进程将整个IO操作交由内核完成,内核完成后会发送通知。在此期间,用户进程不需要去检查IO操作的状态,也不需要主动的去拷贝数据。
1.3 JDK IO发展(BIO--->NIO--->AIO)
时间点 |
概念 |
同步/异步?阻塞/非阻塞? |
代码示例 |
client数:I/O线程数 |
API使用难度 |
吞吐量 |
总结下 | |
---|---|---|---|---|---|---|---|---|
BIO |
jdk1.4之前,源码在java.net包下面 |
Blocking IO 阻塞IO |
同步、阻塞 |
见下面 |
1:1 或M:N (线程池) |
简单 ServerSocket |
低 |
在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。 |
NIO |
jdk1.4引入(2002年),源码在java.nio包下面 |
Non-Blocking IO 非阻塞IO |
同步、非阻塞 |
见下面 |
M:1 |
复杂 ServerSocketChannerl Selector SelectionKey ByteBuffer |
高 |
对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。 |
AIO |
jdk1.7引入(2011年) |
Asynchronous IO 异步IO |
异步、非阻塞 |
无 |
M:0 |
复杂 |
高 |
BIO代码示例(未使用Netty的阻塞网络编程)
public class PlainOioServer { public void serve(int port) throws IOException { final ServerSocket socket = new ServerSocket(port); try { for(;;) { final Socket clientSocket = socket.accept(); System.out.println("Accepted connection from " + clientSocket); //每次和一个client建立连接,都要创建一个线程。 //client数:线程数= 1:1 new Thread(new Runnable() { @Override public void run() { OutputStream out; try { out = clientSocket.getOutputStream(); out.write("Hi! ".getBytes(Charset.forName("UTF-8"))); out.flush(); clientSocket.close(); } catch (IOException e) { e.printStackTrace(); } finally { try { clientSocket.close(); } catch (IOException ex) { // ignore on close } } } }).start(); } } catch (IOException e) { e.printStackTrace(); } } }
NIO代码示例(未使用Netty的非阻塞网络编程)
public class PlainNioServer { public void serve(int port) throws IOException { ServerSocketChannel serverChannel = ServerSocketChannel.open(); serverChannel.configureBlocking(false); ServerSocket ss = serverChannel.socket(); InetSocketAddress address = new InetSocketAddress(port); ss.bind(address); Selector selector = Selector.open(); serverChannel.register(selector, SelectionKey.OP_ACCEPT); final ByteBuffer msg = ByteBuffer.wrap("Hi! ".getBytes()); for (;;){ try { selector.select(); } catch (IOException ex) { ex.printStackTrace(); //handle exception break; } Set<SelectionKey> readyKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = readyKeys.iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); iterator.remove(); try { if (key.isAcceptable()) { ServerSocketChannel server = (ServerSocketChannel) key.channel(); SocketChannel client = server.accept(); client.configureBlocking(false); client.register(selector, SelectionKey.OP_WRITE | SelectionKey.OP_READ, msg.duplicate()); System.out.println( "Accepted connection from " + client); } if (key.isWritable()) { SocketChannel client = (SocketChannel) key.channel(); ByteBuffer buffer = (ByteBuffer) key.attachment(); while (buffer.hasRemaining()) { if (client.write(buffer) == 0) { break; } } client.close(); } } catch (IOException ex) { key.cancel(); try { key.channel().close(); } catch (IOException cex) { // ignore on close } } } } } }
AIO代码示例(略)
1.5 Netty与NIO
1.5.1 Netty对3种I/O模式的支持
问题1: 为什么Netty仅支持NIO了?
为什么不建议使用BIO(阻塞IO)?
连接数高的情况下:阻塞---->耗资源、效率低
为什么删除掉已经做好的AIO支持?
Netty5支持AIO,但是被废弃。
-
Windows上AIO实现成熟,但是Windows很少用来做服务器
-
Linux常用来做服务器,但是AIO实现不够成熟(Linux内核2.6才引入AIO)
-
Linux下AIO相比较NIO性能提升不明显
问题2:为什么Netty有多种NIO实现?
不管是jdk还是netty的版本,都是直接调用了linux的epoll来提供IO多路复用。
通用NIO实现(common)在Linux下也是使用epoll,为什么要Netty单独实现epoll?
原因:自己实现的更好
-
netty暴露了更多的可控参数,例如
-
JDK的NIO默认实现中epoll是水平触发
-
Netty中epoll是边缘触发(默认)和水平触发可切换
-
-
Netty实现的垃圾回收更少、性能更好
1.5.2 代码:Netty使用BIO
public class NettyOioServer { public void server(int port) throws Exception { final ByteBuf buf = Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("Hi! ", Charset.forName("UTF-8"))); //使用OioEventLoopGroup,表示使用BIO EventLoopGroup group = new OioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(group) //使用OioServerSocketChannel,表示使用BIO .channel(OioServerSocketChannel.class) .localAddress(new InetSocketAddress(port)) .childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast( new ChannelInboundHandlerAdapter() { @Override public void channelActive( ChannelHandlerContext ctx) throws Exception { ctx.writeAndFlush(buf.duplicate()) .addListener( ChannelFutureListener.CLOSE); } }); } }); ChannelFuture f = b.bind().sync(); f.channel().closeFuture().sync(); } finally { group.shutdownGracefully().sync(); } } }
1.5.3 代码:Netty使用NIO
public class NettyNioServer { public void server(int port) throws Exception { final ByteBuf buf = Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("Hi! ", Charset.forName("UTF-8"))); //使用NioEventLoopGroup,表示使用NIO NioEventLoopGroup group = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(group) //使用NioServerSocketChannel,表示使用NIO .channel(NioServerSocketChannel.class) .localAddress(new InetSocketAddress(port)) .childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast( new ChannelInboundHandlerAdapter() { @Override public void channelActive( ChannelHandlerContext ctx) throws Exception { ctx.writeAndFlush(buf.duplicate()) .addListener( ChannelFutureListener.CLOSE); } }); } } ); ChannelFuture f = b.bind().sync(); f.channel().closeFuture().sync(); } finally { group.shutdownGracefully().sync(); } } }
参考资料
https://blog.csdn.net/z_ryan/article/details/80873449 linux5种IO模型
http://www.52im.net/thread-1935-1-1.html 高性能网络编程(五):一文读懂高性能网络编程中的I/O模型
https://juejin.im/post/5d46ce64f265da03e05af722 Netty中的epoll实现
https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/BIO-NIO-AIO.md BIO NIO AIO
iteye.com/blog/m635674608-2171397 Java NIO BIO AIO