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

Posted

技术标签:

【中文标题】非阻塞 IO 与异步 IO 以及 Java 中的实现【英文标题】:non-blocking IO vs async IO and implementation in Java 【发布时间】:2014-09-25 19:16:12 【问题描述】:

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

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

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

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

【问题讨论】:

'非阻塞异步 I/O' 只是毫无意义的双重谈话。我不明白您为什么认为需要外部库。它们最终都只是操作系统设施的包装器。 您正确理解了这些条款。如前所述,“非阻塞异步 IO”将是多余的。如果底层 I/O 机制是非阻塞的,则它不需要是异步的,反之亦然。也许用这种方式描述它的人意味着它是非阻塞的,因为它是异步的。 (例如:android-async-http 库是同步套接字 I/O 的异步包装器。) @KevinKrumwiede 你能提供一个 async-io 实际阻塞的例子(我唯一能想象的回调和主进程共享同一个线程,并且有一个 wait/future.get () 在回调或类似的)。 【参考方案1】:

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

有四种与本次讨论相关的 IO:

阻塞 IO

非阻塞 IO

异步 IO

异步非阻塞 IO

我认为这种混淆是因为定义不明确。所以让我试着澄清一下。

首先让我们谈谈IO。当我们的 IO 很慢时,这是最明显的,但 IO 操作可以是阻塞的,也可以是非阻塞的。这与线程无关,它与操作系统的接口有关。当我向操作系统请求 IO 操作时,我可以选择等待所有数据准备好(阻塞),或者获取现在可用的数据并继续前进(非阻塞)。默认为阻塞 IO。使用阻塞 IO 编写代码要容易得多,因为路径更加清晰。但是,您的代码必须停止并等待 IO 完成。非阻塞 IO 需要与较低级别的 IO 库接口,使用 select 和 read/write 而不是提供方便操作的较高级别库。非阻塞 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。

【讨论】:

像 AKKA & vert.x 这样的框架,支持非阻塞特性。人们经常将它们混淆为非阻塞 IO 框架。这些框架做了很多事情,但不是非阻塞 IO。它们只支持如上所述的异步 IO。 这是最准确的答案 谢谢。这可能是一个非常令人困惑的主题,特别是因为许多术语可以互换使用,并且根据项目和平台的不同而不同。这显然是 OP 的困惑,我希望为他和其他人解决这个问题。 仍然没有关注你,我解释了你想要表达的观点。异步 IO 是单线程还是多线程是相关的。要使 IO 在单线程执行中异步,您必须使用非阻塞 IO。要在线程执行中松散地认为 IO 是异步的,您可以使用阻塞线程,或者您可以将非阻塞 IO 与未阻塞线程一起使用。因此非阻塞 io(单线程异步),非常复杂的带有线程的非阻塞 io 称为非阻塞异步 IO。那你把中间那个叫阻塞线程的异步 IO 叫什么呢? 我选择并明确说明了为什么将其区分为“异步 IO”。它只是代数。 A = B + C 其中 A = "非阻塞异步 IO", B = "非阻塞 IO" 因此求解 C 我们有 "异步 IO"。【参考方案2】:

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

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

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


异步渠道组

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

支持异步通道组的线程池大小由以下 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

【讨论】:

三种种:阻塞、非阻塞和异步。您错过了关于如何使用外部库在 Java 中实现它们的问题。 @EJP - 在没有外部库的情况下,Java 中有对异步 I/O 的内部支持,我的回答中有这个。关于阻塞/非阻塞,是否有阻塞异步 I/O?如果您有样品,我很乐意更新我的答案 异步 I/O 通常是异步的因为 I/O 机制是阻塞的。在这种情况下,异步只是意味着它在另一个线程中完成。 所以,@KevinKrumwiede 根据您的定义,这是否意味着每个 I/O 都在阻塞,问题是我们在什么时间点/线程阻塞,对吗?我们应该只讨论同步/异步 io 而不要提及阻塞/非阻塞,因为它一直阻塞(可能不是立即阻塞,比如 future.get() - 等待结果),或者来自异步线程(我们在某个时候阻止异步线程执行)。 我想所有的 I/O 在某种程度上都是阻塞的,如果不是在软件中,在硬件中。是否将其称为阻塞取决于提供给您的 API,即它是否阻塞 您的 线程。如果 I/O 在 API 之外 是非阻塞的,那是因为它在 API 内部 API 的某个级别上是异步的。这就是为什么说“非阻塞异步 I/O”是多余的。非阻塞和异步相互暗示。【参考方案3】:

我会说有三种类型的io:

同步阻塞 同步非阻塞 异步

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

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

【讨论】:

【参考方案4】:

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

知道 IO 是否完成的唯一方法是轮询其状态或阻塞。将其视为Future。您启动一个 IO 操作,它会返回一个 Future。您可以在其上调用isDone() 以检查它是否已完成,如果已完成,请使用它执行您想要的操作,否则请继续执行其他操作,直到您下次要检查它是否已完成。或者,如果您无事可做,您可以调用get,它会一直阻塞直到完成。

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

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

阻塞异步 IO

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

非阻塞异步 IO

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

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

我更彻底地解释了区别here.

【讨论】:

回调既不是阻塞也不是非阻塞。我从未见过一个框架/语言/系统,其中线程将停止挂起对回调的调用,然后在启动回调的地方重新开始。也许这样的系统确实存在,但那将是相当奇怪的。正如您所说,通常会注册回调,并且执行会独立于回调继续执行。当问题不可知或以 Java 为中心时,这个答案感觉非常以 javascript 为中心。 看看我对底层 IO 是如何发生的澄清。我想它会帮助你解决这个问题。 @AaronM 我编辑了我的答案,以摆脱我认为给你的印象,我很困惑。你的回答很好,但我觉得它在技术细节上有点太详细了。我也有点不同意你的一些语义,但只是轻微的。我的示例是基于 Java 的,我的答案中没有任何 JavaScript。我觉得它普遍适用于所有语言和操作系统。您现在是否仍然看到任何令人困惑或不同意的内容? 有道理,我现在更喜欢它了。我唯一的问题是异步非阻塞。从开发者层看似乎是准确的,但从系统层看却不是。如果 IO 是非阻塞的,那么必须检查 IO 是否/何时完成。内核不会自动调用堆栈中的函数。但正如您所提到的,这需要一个框架,而该框架将为开发人员管理这种复杂性。感谢上帝。 关于 JavaScript 评论,我应该说的是它感觉倾向于事件/函数式编程环境,我仍然觉得它是。这在 Java 中并不常见,在 JavaScript 中很常见,因此我确实做了评论。但是所有这些类型的 IO 也用于非事件驱动代码,传统的过程代码也是如此。在这种情况下,异步会变得更加复杂,但是很有可能在不使用回调(或承诺或未来)的情况下进行非阻塞异步 io。回调和其他替代方法确实使代码更容易理解。【参考方案5】:

同步与异步

异步是一个相对术语,适用于所有类型的计算,而不仅仅是 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 运行时(或者准确地说是libuv)维护了一个专用的线程池。每当请求异步磁盘 IO 操作时,运行时都会将工作分配给该池中的一个线程。该线程将执行标准的阻塞磁盘 IO,而主(调用)线程将异步进行。更不用说大量线程,它们由 V8 运行时单独维护,用于垃圾收集和其他托管运行时任务。

【讨论】:

以上是关于非阻塞 IO 与异步 IO 以及 Java 中的实现的主要内容,如果未能解决你的问题,请参考以下文章

linux同步与异步阻塞与非阻塞概念以及五种IO模型

简述linux同步与异步阻塞与非阻塞概念以及五种IO模型

Dart 中的多线程 与 Future

Java中的非阻塞异步IO

Linux IO模型与Java NIO

IO模型介绍 以及同步异步阻塞非阻塞的区别