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

aka*_*lou 62 java asynchronous nonblocking

试着总结一下这两个概念之间的区别(因为当我看到人们在一个句子中使用它们时,我真的很困惑,比如"非阻塞异步IO",我试图找出它是什么意思).

因此,在我的理解中,非阻塞IO主要是处理IO的OS机制,如果有任何数据就绪,否则只返回错误/什么都不做.

在异步IO中,您只需提供回调,并在数据可用时通知您的应用程序.

那么实际上什么是"非阻塞异步IO"?以及它们如何用Java实现(标准JDK,没有外部库,我知道有java.nio.channels.{Channels, Selector, SelectorKey}java.nio.channels.{AsynchronousSocketChannel}):非阻塞IO,异步IO和非阻塞异步IO(如果有这样的东西)?

Aar*_*onM 90

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

现在,异步就是混乱开始的地方.异步的一般概念仅意味着在执行后台操作时该过程继续,发生这种情况的机制不是特定的.该术语含糊不清,因为非阻塞IO和线程阻塞IO都可以被认为是异步的.两者都允许并发操作,但资源要求不同,并且代码实质上不同.因为你问了一个问题"什么是非阻塞异步IO",我将对异步使用更严格的定义,一个执行IO的线程系统可能是也可能不是非阻塞的.

一般定义

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

更严格的定义

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

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

阻止IO - 标准单线程IO,其中应用程序在继续之前等待所有IO操作完成.对于需要多个IO操作的应用程序而言,易于编码,无并发性且速度慢.在等待IO中断发生时,进程或线程将休眠.

异步IO - 线程IO,其中应用程序使用执行线程同时执行阻塞IO操作.需要线程安全代码,但通常比替代方法更容易读写.获得多个线程的开销,但具有明确的执行路径.可能需要使用同步方法和容器.

非阻塞IO - 单线程IO,其中应用程序使用select来确定哪些IO操作已准备就绪,允许在OS处理并发IO时执行其他代码或其他IO操作.等待IO中断时,进程不会休眠,但负责检查文件句柄上的IO标志.由于需要使用select检查IO标志,所以代码要复杂得多,但不需要线程安全代码或同步方法和容器.以代码复杂性为代价的低执行开销.执行路径是错综复杂的.

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

  • 像AKKA和vert.x这样的框架支持非阻塞功能.人们经常将它们混淆为非阻塞IO框架.这些框架做了很多事情,但不是非阻塞IO.它们仅支持如上所述的异步IO. (4认同)
  • 这是最准确的答案 (2认同)
  • 仍然没有关注您,我解释了您要提出的观点。与异步IO是单线程还是多线程有关。为了使IO在单线程执行中异步,您必须使用非阻塞IO。为了使IO在线程执行中被宽松地认为是异步的,可以使用阻塞的线程,也可以将非阻塞的IO与未阻塞的线程一起使用。因此,非阻塞io(单线程异步)是非常复杂的非阻塞io,具有称为非阻塞异步IO的线程。那您怎么称呼中间的那个是阻塞线程的异步IO呢? (2认同)

kol*_*sus 51

那么实际上什么是"非阻塞异步IO"?

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

主要有两种I/O. 同步异步.同步阻止当前执行线程直到处理完成,而异步不阻塞当前执行线程,而是将控制权交给OS内核进行进一步处理.然后,内核在提交的任务完成时建议异步线程


异步通道组

Java中的异步通道的概念由异步通道组支持.异步通道组基本上汇集了许多通道以供重用.async api的消费者从组中检索一个通道(默认情况下JVM创建一个通道),并且通道在完成其读/写操作后自动将自身重新置于组中.最终,异步通道组受到惊喜,线程池的支持.此外,异步通道是线程安全的.

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

java.nio.channels.DefaultThreadPool.initialSize
Run Code Online (Sandbox Code Playgroud)

在给定整数值的情况下,将设置该大小的线程池,以支持通道组.否则,将为开发人员透明地创建和维护通道组.


以及如何用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());
Run Code Online (Sandbox Code Playgroud)

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

  • 异步I/O通常是异步*因为*I/O机制阻塞.在这种情况下,异步只是意味着它在另一个线程中完成. (7认同)
  • 有***种类:阻塞,非阻塞和异步.您已经错过了关于如何使用外部库在Java中实现它们的问题. (5认同)

nic*_*kdu 5

我会说有三种类型的io:

同步阻塞
同步非阻塞
异步

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

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


Did*_* A. 5

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

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

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

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

阻塞异步 IO

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

非阻塞异步 IO

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

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

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


Ale*_*sha 5

同步与异步

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

阻塞与非阻塞

从更广泛的意义上来说,“阻塞”和“非阻塞”可以大致对应地表示“同步”和“异步”。您经常会遇到“阻塞”与“同步”互换使用以及“非阻塞”与“异步”互换使用的情况。从这个意义上说,“非阻塞异步”就像上面其他人提到的那样是多余的。

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

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

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

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

结论

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

后记

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

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