繁体   English   中英

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

[英]non-blocking IO vs async IO and implementation in Java

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

因此,在我的理解中,非阻塞 IO 是主要的操作系统机制来处理 IO,如果有任何数据准备好,否则就返回错误/什么都不做。

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

那么究竟什么是“非阻塞异步 IO”? 以及它们如何在 Java 中实现(标准 JDK,没有外部库,我知道有java.nio.channels.{Channels, Selector, SelectorKey}java.nio.channels.{AsynchronousSocketChannel} ):非阻塞 IO ,异步IO,和非阻塞异步IO(如果有的话)?

我看到这是一个老问题,但我认为这里遗漏了一些东西,@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 操作具有可用字节。 在这种情况下,系统调用将再次安装信号处理程序,但不是休眠,而是将信号处理程序与文件句柄相关联,并立即返回。 该进程将负责定期检查已设置的中断标志的文件句柄。 这通常通过 select 调用完成。

现在异步是混乱开始的地方。 异步的一般概念仅意味着在执行后台操作时进程继续,发生这种情况的机制并不具体。 该术语含糊不清,因为非阻塞 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 和 Promises。

那么究竟什么是“非阻塞异步 IO”?

要回答这个问题,您必须首先了解没有阻塞异步 I/O之类的东西。 异步的概念要求没有等待,没有阻塞,没有延迟。 当您看到非阻塞异步 I/O 时非阻塞位仅用于进一步限定该术语中的异步形容词。 如此有效地,非阻塞异步 I/O可能有点冗余。

主要有两种I/O。 同步异步 Synchronous 阻塞当前执行线程直到处理完成,而Asynchronous 不会阻塞当前执行线程,而是将控制权传递给 OS Kernel 进行进一步处理。 当提交的任务完成时,内核然后通知异步线程


异步通道组

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

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

java.nio.channels.DefaultThreadPool.initialSize

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


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

嗯,我很高兴你问。 下面是一个AsynchronousSocketChannel的例子(用于打开一个非阻塞客户端Socket到一个监听服务器。)这个例子是Apress Pro Java NIO.2的摘录,由我评论:

//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());

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

我会说有三种类型的io:

同步阻塞
同步非阻塞
异步

同步非阻塞和异步都将被视为非阻塞,因为调用线程不等待 IO 完成。 因此,虽然非阻塞异步 io 可能是多余的,但它们并不相同。 当我打开一个文件时,我可以在非阻塞模式下打开它。 这是什么意思? 这意味着当我发出 read() 它不会阻塞。 它要么返回可用字节,要么指示没有可用字节。 如果我没有启用非阻塞 io,则 read() 将阻塞,直到数据可用。 如果我想要一个线程处理多个 io 请求,我可能想要启用非阻塞 io。 例如,我可以使用 select() 找出哪些文件描述符或套接字具有可供读取的数据。 然后我对这些文件描述符进行同步读取。 这些读取都不应该阻塞,因为我已经知道数据可用,而且我已经在非阻塞模式下打开了文件描述符。

异步 io 是您发出 io 请求的地方。 该请求已排队,因此不会阻塞发出线程。 当请求失败或成功完成时,您会收到通知。

非阻塞 IO是指执行 IO 的调用立即返回,并且不会阻塞您的线程。

知道 IO 是否完成的唯一方法是轮询其状态或阻止。 把它想象成一个Future 你开始一个 IO 操作,它会返回一个Future 你可以调用isDone()来检查它是否完成,如果是,就用它做你想做的事情,否则继续做其他事情,直到下次你想检查它是否完成。 或者,如果你无事可做,你可以调用get on it,它会阻塞直到它完成。

异步 IO是指执行 IO 的调用通知您它已通过事件完成,而不是通过其返回值。

这可以是阻塞的或非阻塞的。

阻塞异步 IO

阻塞异步 IO 的意思是执行 IO 的调用是一个正常的阻塞调用,但是您调用的东西将该调用包装在一个线程中,该线程将阻塞直到 IO 完成,然后委托处理 IO 的结果到您的回调。 也就是说,仍然有一个线程在 IO 上阻塞的堆栈下方,但您的线程不是。

非阻塞异步 IO

这实际上是更常见的一种,这意味着非阻塞 IO 不需要像标准非阻塞 IO 那样轮询其状态,而是在完成后调用您的回调。 与阻塞异步 IO 不同,这个线程在堆栈的任何地方都没有阻塞,因此它更快并且使用更少的资源,因为异步行为是在不阻塞线程的情况下进行管理的。

您可以将其视为CompletableFuture 它要求您的程序具有某种形式的异步事件框架,该框架可以是多线程的,也可以不是。 因此,回调可能在另一个线程中执行,或者在当前任务完成后安排在现有线程上执行。

我在这里更彻底地解释了这种区别

同步与异步

异步是一个相对术语,适用于所有类型的计算,而不仅仅是 IO。 某些东西不能单独异步,而总是其他东西异步 通常,异步性意味着某些操作发生在与请求 IO 计算的线程不同的执行线程中,并且在请求线程和计算线程之间没有显式同步(等待)。 如果一个请求线程在计算线程执行其工作时等待(休眠、阻塞),我们称这种操作为同步操作。 也有混合情况。 有时,请求线程不会立即等待,而是在发出 IO 请求后异步执行一些固定数量的有用工作,但稍后会阻塞(同步)以等待 IO 结果(如果它们尚不可用)。

阻塞与非阻塞

从广义上讲,“阻塞”和“非阻塞”可以粗略地分别表示“同步”和“异步”。 您经常会遇到“阻塞”与“同步”和“非阻塞”与“异步”互换使用。 从这个意义上说,“非阻塞异步”就像上面提到的其他人一样是多余的。

然而,狭义上的“阻塞”和“非阻塞”可能指的是不同的内核 IO 接口。 这里值得一提的是,现在所有的 IO 操作都是由 OS 内核执行的,因为对 IO 硬件设备(例如磁盘或网络接口卡)的访问被 OS 抽象掉了。 这意味着您从用户空间代码请求的每个 IO 操作最终都将由内核通过阻塞或非阻塞接口执行。

当通过阻塞接口调用时,内核会假设您的线程想要同步获取结果,并将其置于睡眠状态(取消调度、阻塞),直到 IO 结果可用。 因此,当内核正在执行 IO 请求时,该线程将无法执行任何其他有用的工作。 例如,Linux 上的所有磁盘 IO 都是阻塞的。

非阻塞内核接口的工作方式不同。 你告诉内核你想要哪些 IO 操作。 内核不会阻塞(取消调度)您的线程并立即从 IO 调用返回。 然后您的线程可以继续并做一些有用的工作。 内核线程将异步完成 IO 请求。 然后您的代码需要偶尔检查内核是否已经完成其工作,之后您可以使用结果。 例如,Linux 为非阻塞 IO 提供了epoll接口。 出于相同目的,还有较旧的pollselect系统调用。 值得注意的是,非阻塞接口主要应用于网络。

请注意,某些更高级别的 IO API 在后台使用阻塞内核 IO 的事实并不意味着您的线程在调用该 API 时一定会阻塞。 这样的 API 可以实现一种机制来产生新的或使用不同的现有线程来执行该阻塞 IO。 稍后它将通过某种方式(回调、事件或让您的线程轮询)通知您的调用线程它已完成 IO 请求。 即,非阻塞 IO 语义可以由第三方库或运行时在用户空间中通过使用附加线程在阻塞 OS 内核接口之上实现。

结论

要了解每个特定的运行时或库如何实现 IO 异步性,您必须更深入地了解它是产生新线程还是依赖于异步内核接口。

后记

实际上,如今您几乎没有机会遇到真正的单线程系统。

例如,大多数人会将 Node.js 称为具有“单线程非阻塞”IO。 然而,这是一种简化。 在 Linux 上,真正的非阻塞 IO 只能通过epoll接口进行网络操作。 对于磁盘 IO,内核将始终阻塞调用线程。 为了实现磁盘 IO 的异步性(相对较慢),Node.js 运行时(或libuvlibuv )维护了一个专用线程池。 每当请求异步磁盘 IO 操作时,运行时都会将工作分配给该池中的线程之一。 该线程将执行标准的阻塞磁盘 IO,而主(调用)线程将异步进行。 更不用说许多线程,它们由 V8 运行时单独维护,用于垃圾收集和其他托管运行时任务。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM