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

时间:2014-08-02 21:37:57

标签: java asynchronous nonblocking

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

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

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

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

5 个答案:

答案 0 :(得分:80)

我认为这是一个老问题,但我认为这里错过了一些内容,@ 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。

答案 1 :(得分:49)

  

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

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

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


异步渠道论坛

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

答案 2 :(得分:4)

我想说有三种类型的io:

同步阻塞
同步无阻塞 异步

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

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

答案 3 :(得分:2)

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

了解IO是否完成的唯一方法是轮询其状态或阻止。可以将其视为Future。您启动IO操作,它会返回Future。您可以在其上调用isDone()来检查它是否完成,如果是,用它做你想做的事情,否则继续做其他事情直到你想要检查它是否完成。或者,如果您没有要做的事情,可以在其上调用get,这将阻止它完成。

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

这可能是阻止或非阻止。

阻止异步IO

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

非阻止异步IO

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

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

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

答案 4 :(得分:2)

同步与异步

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

阻止与非阻止

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

但是,从更狭义的意义上讲,“阻塞”和“非阻塞”可能是指不同的内核IO接口。在这里值得一提的是,这些天的所有IO操作都是由OS内核执行的,因为对OS诸如磁盘或网络接口卡等IO硬件设备的访问已被抽象化。这意味着您从用户空间代码请求的每个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运行时分别维护以进行垃圾回收和其他托管运行时任务。