非阻塞 IO 与异步 IO 以及 Java 中的实现

2022-08-31 11:48:49

试图为自己总结这两个概念之间的区别(因为当我看到人们在一句话中使用这两个概念时,我真的很困惑,比如“非阻塞异步IO”,我试图弄清楚它是什么意思)。

因此,在我的理解中,非阻塞IO是主要的操作系统机制,用于在准备好任何数据的情况下处理IO,否则只需返回错误/什么都不做。

在异步 IO 中,您只需提供一个回调,当数据可用时,您的应用程序将收到通知。

那么,什么是真正的“非阻塞异步 IO”呢?以及如何在Java中实现所有这些(标准JDK,没有外部库,我知道有和):非阻塞IO,异步IO和非阻塞异步IO(如果有这样的事情)?java.nio.channels.{Channels, Selector, SelectorKey}java.nio.channels.{AsynchronousSocketChannel}


答案 1

我看到这是一个古老的问题,但我认为这里遗漏了一些东西,@nickdu试图指出但还不是很清楚。

有四种类型的 IO 与此讨论相关:

阻止 IO

非阻塞 IO

异步 IO

异步非阻塞 IO

我认为这种混淆是因为定义含糊不清。因此,让我试着澄清这一点。

首先,我们来谈谈 IO。当我们有慢速IO时,这是最明显的,但IO操作可以是阻塞的,也可以是非阻塞的。这与线程无关,它与操作系统的接口有关。当我要求操作系统进行IO操作时,我可以选择等待所有数据准备就绪(阻止),或者获取现在可用的数据并继续前进(非阻塞)。默认值为阻止 IO。使用阻塞 IO 编写代码要容易得多,因为路径更清晰。但是,您的代码必须停止并等待 IO 完成。非阻塞 IO 需要与较低级别的 IO 库进行交互,使用选择和读/写,而不是使用提供方便操作的较高级别的库。非阻塞 IO 还意味着在操作系统执行 IO 时,您有一些需要处理的内容。这可能是对已完成的 IO 执行的多个 IO 操作或计算。

阻止 IO - 应用程序等待操作系统收集所有字节以完成操作或到达末尾,然后再继续。这是默认设置。为了更清楚地说明技术性,启动 IO 的系统调用将安装一个信号处理程序,等待 IO 操作进行时将发生的处理器中断。然后,系统调用将开始休眠,该休眠将当前进程的操作暂停一段时间,或直到进程中断发生。

非阻塞 IO - 应用程序告诉操作系统它只想要哪些字节现在可用,并在操作系统同时收集更多字节时继续前进。该代码使用 select 来确定哪些 IO 操作具有可用的字节。在这种情况下,系统调用将再次安装信号处理程序,但不会休眠,而是将信号处理程序与文件句柄相关联,并立即返回。该进程将负责定期检查已设置的中断标志的文件句柄。这通常通过选择呼叫来完成。

现在,异步是混乱开始的地方。异步的一般概念仅意味着在执行后台操作时该过程继续,发生这种情况的机制并不具体。该术语是模棱两可的,因为非阻塞 IO 和线程阻塞 IO 都可以被视为异步的。两者都允许并发操作,但是资源要求不同,并且代码也大不相同。因为您已经问了一个问题“什么是非阻塞异步 IO”,所以我将对异步使用更严格的定义,异步是执行 IO 的线程系统,它可能是非阻塞的,也可能不是非阻塞的。

一般定义

异步 IO - 允许发生多个并发 IO 操作的编程 IO。IO 操作同时发生,因此代码不会等待未准备好的数据。

更严格的定义

异步 IO - 使用线程或多处理来允许发生并发 IO 操作的编程 IO。

现在有了这些更清晰的定义,我们有以下种类型的IO范例。

阻塞 IO - 标准单线程 IO,其中应用程序等待所有 IO 操作完成,然后再继续。易于编码,没有并发性,对于需要多个 IO 操作的应用程序来说速度很慢。进程或线程将在等待 IO 中断发生时进入睡眠状态。

异步 IO - 线程化 IO,其中应用程序使用执行线程并发执行阻塞 IO 操作。需要线程安全代码,但通常比其他代码更易于读取和写入。获得多个线程的开销,但具有清晰的执行路径。可能需要使用同步的方法和容器。

非阻塞 IO - 单线程 IO,其中应用程序使用 select 来确定哪些 IO 操作已准备好推进,从而允许在操作系统处理并发 IO 时执行其他代码或其他 IO 操作。进程在等待 IO 中断时不会休眠,但负责检查文件句柄上的 IO 标志。由于需要使用select检查IO标志,因此代码要复杂得多,尽管不需要线程安全代码或同步方法和容器。低执行率,以牺牲代码复杂性为代价。执行路径很复杂。

异步非阻塞 IO - 一种混合的 IO 方法,旨在通过使用线程来降低复杂性,同时尽可能通过使用非阻塞 IO 操作来保持可伸缩性。这将是最复杂的 IO 类型,需要同步的方法和容器,以及复杂的执行路径。这不是人们应该考虑轻易编码的IO类型,并且通常仅在使用会掩盖复杂性的库时使用,例如Futures和Promise。


答案 2

那么,什么是真正的“非阻塞异步 IO”呢?

要回答这个问题,您必须首先了解没有阻止异步 I/O 这样的事情。异步性的概念本身就决定了没有等待,没有阻塞,没有延迟。当您看到非阻塞异步 I/O 时非阻塞位仅用于进一步限定该术语中的异步形容词。因此,实际上,非阻塞异步 I/O 可能有点冗余。

I/O 主要有两种:同步异步在处理完成之前,同步会阻止当前执行线程,而异步不会阻止当前的执行线程,而是将控制权传递给操作系统内核以进行进一步处理。然后,内核会在提交的任务完成时通知异步线程


异步通道组

Java 中的异步通道的概念由异步通道组提供支持。异步通道组基本上汇集了许多通道以供重用。异步 API 的使用者从组中检索通道(默认情况下,JVM 会创建一个通道),并且通道在完成读/写操作后自动将自己放回组中。最终,异步通道组得到了令人惊讶的线程池的支持。此外,异步通道是线程安全的。

支持异步通道组的线程池的大小由以下 JVM 属性配置

java.nio.channels.DefaultThreadPool.initialSize

给定一个整数值,它将设置该大小的线程池,以支持通道组。否则,频道组的创建和维护对开发人员是透明的。


以及如何在Java中实现所有这些

好吧,我很高兴你问。下面是一个示例(用于向侦听服务器打开非阻塞客户端。此示例摘自 Apress Pro Java NIO.2,由我评论:AsynchronousSocketChannelSocket

//Create an Asynchronous channel. No connection has actually been established yet
AsynchronousSocketChannel asynchronousSocketChannel = AsynchronousSocketChannel.open(); 

/**Connect to an actual server on the given port and address. 
   The operation returns a type of Future, the basis of the all 
   asynchronous operations in java. In this case, a Void is 
   returned because nothing is returned after a successful socket connection
  */
Void connect = asynchronousSocketChannel.connect(new InetSocketAddress("127.0.0.1", 5000)).get();


//Allocate data structures to use to communicate over the wire
ByteBuffer helloBuffer = ByteBuffer.wrap("Hello !".getBytes()); 

//Send the message

Future<Integer> successfullyWritten=  asynchronousSocketChannel.write(helloBuffer);

//Do some stuff here. The point here is that asynchronousSocketChannel.write() 
//returns almost immediately, not waiting to actually finish writing 
//the hello to the channel before returning control to the currently executing thread

doSomethingElse();

//now you can come back and check if it was all written (or not)

System.out.println("Bytes written "+successfullyWritten.get());

编辑:我应该提到对异步NIO的支持来自JDK 1.7


推荐