AIO 是真正意义上的异步非阻塞 I/O 模型。在 NIO 的实现中,是需要用户线程一直调用 select() 轮询检查 IO 缓冲区数据是否就绪的,这个轮询的过程还是阻塞的,并非真正解放当前线程,因为它还是需要去查询哪些
IO 就绪。而真正理想的异步非阻塞 IO 应该让内核系统完成,进程读取数据时只负责发送跟接收指令,数据的准备工作则完全由操作系统来处理。

我们针对之前讨论的 IO 模型举个简单的例子:

  • 第一种阻塞 I/O 就是你去了书店,告诉老板你想要某本书,然后你就一直在那里等着,直到书店老板翻箱倒柜找到你想要的书。


  • 第二种非阻塞 I/O 类似于你去了书店,问老板有没有一本书,老板告诉你没有,你就离开了。一周以后,你又来这个书店,再问这个老板,老板一查,有了,于是你买了这本书。


  • 第三种基于非阻塞的 I/O 多路复用,你来到书店告诉老板:老板,到货给我打电话,我再来付钱取书。


  • 第四种异步 I/O 就是你连去书店取书的过程也想省了,你留下地址,付了书费,让老板到货时寄给你,你直接在家里拿到就可以看了。

这里放置了一张表格,总结了以上几种 I/O 模型。
image.png

Linux 对 aio 的支持

Linux 操作系统准备了 aio_readaio_write 函数实现真实的异步,当用户发起 aio_read 请求后就会自动返回,内核会自动将数据从内核缓冲区拷贝到用户进程空间,应用进程啥都不用管。
image.png
aio 系列函数是由 POSIX 定义的异步操作接口,可惜的是,Linux 下的 aio 操作,不是真正的操作系统级别支持的,它只是由 GNU libc 库函数在用户空间借由 pthread 方式实现的,而且仅仅针对磁盘类 I/O,套接字 I/O 不支持。

也有很多 Linux 的开发者尝试在操作系统内核中直接支持 aio,例如一个叫做 Ben LaHaise 的人,就将 aio 实现成功 merge 到 2.5.32 中,这部分能力是作为 patch 存在的,但是,它依旧不支持套接字。Solaris 倒是有真正的系统系别的 aio,不过还不是很确定它在套接字上的性能表现,特别是和磁盘 I/O 相比效果如何。

综合以上结论就是,Linux 下对异步操作的支持非常有限,这也是为什么使用 epoll 等多路分发技术加上非阻塞 I/O 来解决 Linux 下高并发高性能网络 I/O 问题的根本原因。

Java AIO

在 Java 7 引入的 NIO 2 中,增添了对 AIO 模式的支持,利用事件和回调处理 Accept、Read 等操作。示例代码如下:

  1. AsynchronousServerSocketChannel serverSock = AsynchronousServerSocketChannel.open().bind(sockAddr);
  2. serverSock.accept(serverSock, new CompletionHandler<>() {
  3. //为异步操作指定CompletionHandler回调函数
  4. @Override
  5. public void completed(AsynchronousSocketChannel sockChannel, AsynchronousServerSocketChannel serverSock) {
  6. serverSock.accept(serverSock, this);
  7. // 另外一个 write(sock,CompletionHandler{})
  8. sayHelloWorld(sockChannel, Charset.defaultCharset().encode
  9. ("Hello World!"));
  10. }
  11. // 省略其他路径处理方法...
  12. });

业务逻辑的关键在于,通过指定 CompletionHandler 回调接口,在 accept/read/write 等关键节点,通过事件机制调用,这是非常不同的一种编程思路。