AIO 是真正意义上的异步非阻塞 I/O 模型。在 NIO 的实现中,是需要用户线程一直调用 select() 轮询检查 IO 缓冲区数据是否就绪的,这个轮询的过程还是阻塞的,并非真正解放当前线程,因为它还是需要去查询哪些
IO 就绪。而真正理想的异步非阻塞 IO 应该让内核系统完成,进程读取数据时只负责发送跟接收指令,数据的准备工作则完全由操作系统来处理。
我们针对之前讨论的 IO 模型举个简单的例子:
- 第一种阻塞 I/O 就是你去了书店,告诉老板你想要某本书,然后你就一直在那里等着,直到书店老板翻箱倒柜找到你想要的书。
- 第二种非阻塞 I/O 类似于你去了书店,问老板有没有一本书,老板告诉你没有,你就离开了。一周以后,你又来这个书店,再问这个老板,老板一查,有了,于是你买了这本书。
- 第三种基于非阻塞的 I/O 多路复用,你来到书店告诉老板:老板,到货给我打电话,我再来付钱取书。
- 第四种异步 I/O 就是你连去书店取书的过程也想省了,你留下地址,付了书费,让老板到货时寄给你,你直接在家里拿到就可以看了。
Linux 对 aio 的支持
Linux 操作系统准备了 aio_read 跟 aio_write 函数实现真实的异步,当用户发起 aio_read 请求后就会自动返回,内核会自动将数据从内核缓冲区拷贝到用户进程空间,应用进程啥都不用管。
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 等操作。示例代码如下:
AsynchronousServerSocketChannel serverSock = AsynchronousServerSocketChannel.open().bind(sockAddr);
serverSock.accept(serverSock, new CompletionHandler<>() {
//为异步操作指定CompletionHandler回调函数
@Override
public void completed(AsynchronousSocketChannel sockChannel, AsynchronousServerSocketChannel serverSock) {
serverSock.accept(serverSock, this);
// 另外一个 write(sock,CompletionHandler{})
sayHelloWorld(sockChannel, Charset.defaultCharset().encode
("Hello World!"));
}
// 省略其他路径处理方法...
});
业务逻辑的关键在于,通过指定 CompletionHandler 回调接口,在 accept/read/write 等关键节点,通过事件机制调用,这是非常不同的一种编程思路。