当前位置: 首页 > 知识库问答 >
问题:

Java中的异步文件NIO有什么好处?

袁河
2023-03-14

根据AsynchronousFileChannel和AsynchronousChannelGroup的文档,async NIO正在使用“IO事件处理”的专用线程池。在这个上下文中,我找不到任何明确的说明“处理”意味着什么,但根据这一点,我非常肯定,在一天结束时,阻塞会发生在那些专用线程上。为了缩小范围,我使用的是Linux,并且基于Alex Yursha的答案,它上没有非阻塞IO这样的东西,只有Windows在某些层面上支持它。

我的问题是:与在我自己创建的专用线程池上运行的同步IO相比,使用异步NIO有什么好处?考虑到引入的复杂性,当它仍然值得实现时,会是什么场景?

共有1个答案

宰父学
2023-03-14

这主要是关于如何处理缓冲区大小。这样,您可以节省大量内存,但前提是您要处理大量(数千个)同时连接。

首先是一些简化和警告:

>

  • 我将假设一个无脑的调度程序。有些操作系统在处理数千个线程方面做得很差。当一个用户进程启动1000个满线程时,操作系统就会崩溃,这并不是一个内在的原因,但有些操作系统还是会崩溃。NIO可以在这方面提供帮助,但这有点不公平--通常你应该升级你的OS。几乎所有linux,我相信win10绝对不会有这么多线程的问题,但是ARM上的一些旧linux端口,或者像Windows7之类的东西--可能会引起问题。

    我假设您正在使用NIO来处理传入的TCP/IP连接(例如web服务器或IRC服务器,诸如此类)。如果您试图同时读取1000个文件,同样的原则也适用,但请注意,您确实需要考虑瓶颈在哪里。例如,从一个磁盘上同时读取1000个文件是一个毫无意义的练习--这只会使事情变慢,因为您会使磁盘变得更加困难(如果它是一个旋转磁盘,这会加倍计算)。对于网络,特别是如果您使用的是快速管道,瓶颈不是管道或网卡,这使得“同时处理1000个连接”是一个很好的例子。事实上,我将使用一个聊天服务器作为例子,其中1000人都连接到一个巨大的聊天室。工作是接收任何有联系的人的短信,并将它们发送给每个人。

    在同步模型中,生活相对简单:我们将制作2001个线程:

    • 侦听套接字上新传入的TCP连接的1个线程。此线程将创建2个“处理程序”线程,并返回监听新连接。
    • 每个用户从套接字读取直到看到enter符号为止的线程。如果它看到这一点,它将接受到目前为止收到的所有文本,并用需要发送出去的新字符串通知所有1000个“发送者”线程。
    • 每个用户一个线程,它将在“Text messages to send out”的缓冲区中发送字符串。如果没有东西要发送,它将等待一个新的消息传递给它。

    但是,我们确实有2001个线程。每个线程都有一个堆栈。在JVM中,每个线程都得到相同大小的堆栈(您不能创建线程,但要使用不同大小的堆栈),您可以使用-xss参数配置它的大小。您可以使它们小到例如128K,但即使这样,对于堆栈来说,仍然是128K*2001=~256MB,我们还没有讨论任何堆(人们来回发送的、卡在发送队列中的所有字符串),或者应用程序本身,或者JVM基础知识。

    在引擎盖下,有16个核心的CPU会发生的事情是,有2001个线程,每个线程都有自己的一组条件,这些条件会导致它被唤醒。对于接收方来说,数据是通过管道进入的,对于发送方来说,要么是网卡指示它准备好发送另一个数据包(如果它正在等待将数据推送到线路上),要么是等待obj.wait()调用来获得通知(从用户接收文本的线程将把该字符串添加到1000个发送方中每个发送方的所有队列中,然后通知所有发送方)。

    这需要大量的上下文切换:线程醒来,在缓冲区中看到乔:大家好,早上好!,把它变成一个数据包,放到网卡的内存缓冲区中(这是非常快的,只是CPU和内存交互),然后重新进入Hibernate状态。然后CPU内核将继续前进,并找到另一个准备好执行某些工作的线程。

    CPU内核有内核缓存;事实上,这是有等级的。有主RAM,然后是L3高速缓存,L2高速缓存,内核高速缓存--在现代架构中,CPU不能真正在RAM上操作了,它们需要芯片周围的基础设施意识到它需要读或写到不在这些高速缓存中的页面上的内存,然后CPU会冻结一段时间,直到infra能够将该页面的RAM复制到其中一个高速缓存中。

    每次内核切换时,很有可能需要加载一个新页面,而这可能需要花费数百个周期,因为CPU正在玩弄它的拇指。一个编写错误的调度程序会导致比需要的更多的问题。如果你读到NIO的优点,经常会说“那些上下文开关很贵!”出现了--这或多或少是他们在谈论的(但是,剧透提醒:异步模型也受此影响!)

    在同步模型中,计算1000个连接用户中哪些用户准备好了事情发生的工作被“卡”在等待事件的线程中;操作系统正在处理这1000个线程,当有事情要做时会唤醒线程。

    在异步模型中,我们对它进行了切换:我们仍然有线程,但要少得多(每个核心一到两个线程是个好主意)。这比连接的用户要少得多:每个线程负责所有的连接,而不是只负责一个连接。这意味着每一个线程都要检查哪些连接的用户有事情要做(他们的网络管道有数据要读,或者我们准备好了把更多的数据推送到他们那里)。

    不同之处在于线程询问OS的内容:

    • [同步]好的,我要进入Hibernate状态,直到这个连接向我发送数据。
    • [async]好的,我要进入Hibernate状态,直到这一千个连接中的一个向我发送数据,或者我注册为正在等待网络缓冲区清除,因为我有更多数据要发送,并且网络已清除,或者socketlistener有新用户正在连接。

    这两种模式都没有固有的速度和设计优势--我们只是在应用程序和操作系统之间转移工作。

    // synchronous code
    int size = readInt();
    byte[] buffer = new byte[size];
    int pos = 0;
    while (pos < size) {
        int r = input.read(buffer, pos, size - pos);
        if (r == -1) throw new IOException("Client hung up");
        pos += r;
    }
    sendMessage(username + ": " + new String(buffer, StandardCharsets.UTF_8));
    

    运行时,线程很可能会阻塞对inputstream的read调用,因为这将涉及到与网卡对话,并将一些字节从其内存缓冲区移到该进程的缓冲区以完成作业。当它冻结时,指向该字节数组的指针、size变量、r等都在堆栈中。

    在异步模型中,它不是这样工作的。在异步模型中,您得到了给定的数据,并且得到了存在的任何数据,然后您必须处理这些数据,因为如果您不处理这些数据,这些数据就会消失。

    因此,在异步模型中,您得到的Hello everybody,good Morning!消息的一半。您得到表示hello eve的字节,就这样。因此,您已经获得了该消息的总字节长度,并且需要记住这一点,以及到目前为止收到的一半字节长度。您需要显式地创建一个对象并将这些东西存储在某个地方。

    关键是:在同步模型中,您的许多状态信息都是堆栈式的。在异步模型中,您自己创建数据结构来存储这种状态。

    因为您自己做这些,所以它们可以动态地调整大小,而且通常要小得多:您只需要4个字节来存储大小,另外8个左右用于指向字节数组的指针,少量用于用户名指针,仅此而已。这比堆栈用来存储这些东西的128k要小几个数量级。

    现在,另一个理论上的好处是,您不需要进行上下文切换,而不是当read()调用没有剩余的数据可以提供给您时,CPU和OS必须交换到另一个线程,因为网卡正在等待数据,现在是线程的工作:好的,没问题,我将继续到另一个上下文对象。

    我们有垃圾收集语言是有原因的。我们没有用汇编程序编写所有的代码是有原因的。用手工仔细管理所有这些挑剔的细节通常是不值得的。事实也是如此:这种好处往往是不值得的。但是,就像GFX驱动程序和内核有大量的机器代码,驱动程序往往是在手工管理的内存环境中编写的一样,在某些情况下,仔细管理这些缓冲区是非常值得的。

    不过,成本很高。

    设想一种具有以下属性的理论编程语言:

      null

    由于彩色函数的问题,编写好异步代码是一件非常麻烦的事情。它也不是在它的表面上更快--事实上,它通常更慢。如果您希望同时运行数千个操作,并且跟踪每个操作的相关状态数据所需的存储量很小,那么异步就会大获成功,因为您可以手摇该缓冲区,而不是被迫依赖于每个线程的一个堆栈。

    如果您有一些剩余的钱,那么,开发人员的工资为您购买了大量的RAM棒,所以通常正确的选择是使用线程,如果您想要处理许多同时的连接,则只选择一个具有大量RAM的盒子。

    请注意,像youtube、facebook等网站实际上是采用了“把钱扔在RAM上”的解决方案--它们将自己的产品分割开来,让许多简单而廉价的计算机协同工作,共同为一个网站服务。别敲门。

    async能真正发挥作用的例子是我在这个答案中描述的聊天应用程序。另一种方法是,比如说,你收到一条短消息,你所做的就是对它进行散列,加密散列,然后用它来响应(要进行散列,你不需要记住所有流入的字节,你只需要将每个字节扔进具有恒定内存负载的hasher中,当字节全部发送出去时,瞧,你就有了你的散列)。您要寻找的是每个操作的状态很少,相对于提供数据的速度来说,也没有太多的CPU功率。

    一些不好的例子:有一个系统,你需要做一堆DB查询(你需要一种异步的方式与你的DB对话,通常DBs不擅长尝试同时运行1000个查询),一个比特币挖掘操作(比特币挖掘是瓶颈,尝试在一台机器上同时处理数千个连接是没有意义的)。

  •  类似资料:
    • 所以我理解为什么从异步返回空洞通常没有意义,但我遇到了一种我认为完全有效的情况。请考虑以下人为的示例: 我意识到这是一个不寻常的例子,但我试图使其简单化和更普遍化。有人能向我解释为什么这是可怕的代码,以及我如何修改它以正确遵循约定吗? 谢谢你的任何帮助。

    • 问题内容: 到现在为止,因为它适用于服务的HTTP请求,我想条款- 和意味着同样的事情。但是显然,它们分别在Servlet 3.0和3.1中分别实现。我正在努力了解这里的区别… 请问有人可以进一步阐明这个话题吗?具体来说,我正在寻找一个示例,该示例说明服务器的Servlet 3.0实现如何异步而又在线程上阻塞?我想可能是,如果我理解了这一点,可能更容易理解Servlet 3.1中的非阻塞I / O

    • 问题内容: 我在看下面的例子 其中使用以下代码 这样做的好处是什么 我尝试过这两种方法,并且在一次附加到一个文件的任务上,它们的速度似乎相当。 问题答案: 在Javadoc中提供了关于这个问题的一个合理的讨论: 通常,Writer立即将其输出发送到基础字符或字节流。除非需要即时输出,否则建议将BufferedWriter包装在其write()操作可能会很昂贵的所有Writer周围,例如FileWr

    • 从本oracle java教程: OpenOptions参数 本节中的几个方法采用可选的OpenOptions参数。此参数是可选的,API会告诉您在未指定任何参数时方法的默认行为。 这些就是他们所指的API。 在没有指定任何的情况下,我无法找出文件创建的行为。默认的

    • 问题内容: 我查看了JDK和Apache压缩库随附的默认Zip库,由于以下三个原因,我对它们不满意: 它们太臃肿了,API设计也不好。我必须写50行锅炉板字节数组输出,压缩输入,文件输出流和关闭相关流,捕捉异常和移动字节缓冲区自己?为什么我不能有一个像)和这样的简单API呢? 似乎通过压缩解压缩会破坏文件元数据,并且密码处理会被破坏。 另外,与我从UNIX获得的命令行zip工具相比,我尝试的所有库

    • 问题内容: 经过一整天的研究和尝试,我终于放弃了 纯AJAX的 上传文件(ps:本文如何与JQuery异步上传文件?埋了我的最后希望) 我的问题可能没什么意义,但是我仍然想知道为什么ajax(或XMLHttpRequest)不能处理这个问题?为什么文件不能像真正的httprequest那样传输? 问题答案: 出于安全原因,JavaScript无法读取本地文件,因此我们无法使用AJAX发送数据。 但