根据AsynchronousFileChannel和AsynchronousChannelGroup的文档,异步NIO正在使用专用线程池来处理“IO事件”。在这种情况下,我找不到任何明确的声明“处理”的含义,但是根据this,我很确定在一天结束时,这些专用线程上会发生阻塞。为了缩小范围,我使用Linux并基于Alex Yursha's回答,上面没有非阻塞IO之类的东西,只有Windows在一定程度上支持它。
我的问题是:使用异步NIO相对于在我自己创建的专用线程池上运行的同步IO有什么好处?考虑到引入的复杂性,在什么情况下仍然值得实现?
最佳答案
这主要与手动滚动缓冲区大小有关。这样,您可以节省大量内存,但前提是您要尝试处理大量(数千个)同时连接。
首先是一些简化和警告:
同步模型
在同步模型中,生活相对简单:我们将创建2001个线程:
每个单独的运动件都易于编程。在战术上使用单个
java.util.concurrent
数据类型,甚至使用一些基本的synchronized()
块,都可以确保我们不会遇到任何竞争状况。我预想每页可能会有一页代码。但是,我们确实有2001个线程。每个线程都有一个堆栈。在JVM中,每个线程都具有相同大小的堆栈(您无法创建线程,但堆栈大小不同),然后使用
-Xss
参数配置它的大小。您可以将它们设置为小至128k,但即使对于堆栈,这仍然是128k * 2001
=〜256MB ,我们没有覆盖任何堆(人们来回发送的所有字符串,都卡住了)在发送队列中),应用本身或JVM基础知识。在后台,拥有16个内核的CPU将要发生的事情是有2001个线程,并且每个线程都有自己的条件集,这将导致它唤醒。对于接收者来说,数据是通过管道进入的,对于发送者来说,它的网卡指示它已经准备好发送另一个数据包(以防它正在等待将数据向下推送),或者等待
obj.wait()
调用以得到通知(从用户接收文本的线程会将该字符串添加到1000个发件人的所有队列中,然后将它们全部通知)。这需要进行很多上下文切换:线程唤醒,在缓冲区中看到
Joe: Hello, everybody, good morning!
,将其转换为数据包,将其blit到网卡的内存缓冲区中(这非常快,这只是CPU和内存的交互),以及例如,将重新休眠。然后,CPU核心将继续运行,并找到另一个准备好执行某些工作的线程。CPU内核具有内核上的高速缓存;实际上,有一个层次结构。有一个主要的RAM,然后是L3高速缓存,L2高速缓存,内核高速缓存-在现代体系结构中,CPU不能再真正在RAM上运行了,它们需要芯片周围的基础架构来认识到它需要读取或写入内存。在不属于这些高速缓存之一的页面上,那么CPU将冻结一会儿,直到基础设施可以将RAM的该页面复制到其中一个高速缓存中为止。
每次内核切换时,很有可能需要加载一个新页面,并且可能要花费数百个周期才能使CPU摇动手指。调度程序写得不好将导致超出预期的范围。如果您了解NIO的优势,通常“那些上下文切换很昂贵!”出现-这或多或少是他们在谈论的内容(但是,扰流板警报:异步模型也因此受到影响!)
异步模型
在同步模型中,找出1000个已连接用户中的哪个已准备好进行事情的工作是“卡在”等待事件的线程中。操作系统正在处理这1000个线程,并且在有工作要做时会唤醒线程。
在异步模型中,我们进行了切换:我们仍然有线程,但线程要少得多(每个内核一到两个是一个好主意)。这比连接的用户少得多的线程:每个线程负责所有连接,而不是仅负责1个连接。这意味着每个线程将执行检查哪个连接的用户有工作要做的工作(他们的网络管道中有要读取的数据,或者准备好让我们将更多数据向下推给他们)。
区别在于线程询问操作系统的内容:
两种模型都没有固有的速度或设计优势-我们只是在App和OS之间转移工作。
NIO经常被吹捧的一个优势是您不需要担心竞争条件,同步,并发安全的数据结构。这是一个经常重复出现的虚假事实:CPU具有许多内核,因此,如果您的非阻塞应用程序仅创建一个线程,那么您的绝大部分CPU只会闲着无所事事,这是非常低效的。
这里最大的好处是:嘿,只有16个线程。那是
128k * 16
= 2MB的堆栈空间。与同步模型占用的256MB相比,与形成了鲜明的对比!但是,现在发生了另一件事:在同步模型中,有关连接的许多状态信息被“卡在”该堆栈中。例如,如果我这样写:假设协议(protocol)是:客户端发送1个int,它是消息中的字节数,然后是那么多字节,即消息,以UTF-8编码。
// 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));
运行此线程时,线程很可能最终会阻塞对输入流的read
调用,因为这将涉及与网卡通信并将一些字节从其内存缓冲区移到该进程的缓冲区中,以完成工作。在冻结的同时,指向该字节数组的指针,size
变量,r
等等都在堆栈中。在异步模型中,它不是那样工作的。在异步模型中,您将获得数据,然后再获得其中的任何内容,然后必须处理此问题,因为如果不这样做,则该数据将消失。
因此,在异步模型中,您可以获得
Hello everybody, good morning!
消息的一半。您得到了代表Hello eve
的字节,仅此而已。为此,您已经获得了此消息的总字节长度,需要记住这一点,以及到目前为止收到的一半。您需要明确做一个对象并将其存储在某处。这是关键点:在同步模型中,很多状态信息都在堆栈中。在异步模型中,您使数据结构自己来存储此状态。
而且因为您可以自己制作这些文件,所以它们可以动态调整大小,并且通常要小得多:您只需要约4个字节来存储大小,另外8个左右即可存储指向字节数组的指针,少数几个可以存储用户名指针,仅此而已。这比堆栈用来存储这些东西的
128k
小几个数量级。现在,另一个理论上的好处是您无需进行上下文切换-当read()调用没有剩余数据可提供给您,因为网卡正在等待数据时,CPU和OS不必切换到另一个线程,现在是线程的工作了:好的,没问题-我将继续讨论另一个上下文对象。
但这是一个红色的鲱鱼-操作系统是否在处理1000个上下文概念(1000个线程),或者您的应用程序在处理1000个上下文概念(这些“跟踪器”对象)都没有关系。它仍然是1000个连接,每个人都在聊天,因此,每当您的线程继续检查另一个上下文对象并用更多数据填充其字节数组时,很可能它仍然是高速缓存未命中,并且CPU仍然会花费数百倍的精力循环,而硬件基础结构将适当的页面从主RAM提取到缓存中。因此,尽管上下文对象较小的事实将在某种程度上减少高速缓存未命中,但该部分的相关性并不高。
这使我们回到:主要好处是您可以手动滚动这些缓冲区,这样做既可以使它们更小,又可以动态调整它们的大小。
异步的缺点
我们使用垃圾收集语言是有原因的。我们没有在汇编器中编写所有代码是有原因的。手动管理所有这些挑剔的细节通常是不值得的。就是这样:通常,这种 yield 是不值得的。但是,就像GFX驱动程序和内核内核具有大量的机器代码,并且驱动程序往往是在手动管理的内存环境中编写的一样,在某些情况下,对这些缓冲区进行仔细的管理非常值得。
成本很高。
想象一下一种具有以下特性的理论编程语言:
这看起来像是语言的彻底的灾难,不是吗?但这就是您编写异步代码时所生活的世界!
问题是:在异步代码中,您无法调用阻塞函数,因为如果它阻塞了,嘿,那是现在被阻塞的16个线程之一,这立即意味着您的CPU现在什么也不做。如果所有16个线程最终都在该阻塞部分中,则CPU实际上根本不执行任何操作,并且所有内容都被冻结。你就是做不到。
有很多东西会阻塞:打开文件,甚至碰到一个从未接触过的类(该类都需要从磁盘上的jar中加载,验证和链接),就像查看数据库,进行快速网络连接一样检查,有时要求在当前时间执行此操作。即使在调试级别记录日志也可以做到这一点(如果最终将其写入磁盘,瞧-阻止操作)。
您是否知道有任何日志框架会 promise 启动一个单独的线程来将日志处理到磁盘上,或者会以其无法阻止的方式进行记录呢?我也不知道
因此,阻塞的方法为红色,异步处理程序为蓝色。 Tada-这就是为什么异步很难如此正确地实现的原因。
执行摘要
由于有色函数的问题,编写好异步代码确实是一件很痛苦的事情。它的表面也不快-实际上,它通常更慢。如果您想同时运行成千上万个操作,并且跟踪每个单独操作的相关状态数据所需的存储量很小,那么异步就可以胜出,因为您可以手动分配该缓冲区,而不是被迫每个缓冲区依赖一个堆栈线。
如果您有剩余的钱,那么,开发人员的薪水会为您买很多RAM的棍子,因此通常正确的选择是使用线程,如果要处理,则选择带有大量RAM的盒子许多同时连接。
请注意,诸如youtube,facebook等网站有效地采用了“在RAM上花钱”的解决方案-他们分摊产品,以便许多简单廉价的计算机共同为网站服务。不要敲它。
我在此答案中描述的聊天应用程序是异步真正发光的示例。另一个是,例如,您收到一条短消息,而您所要做的就是对它进行哈希处理,加密哈希并进行响应(要进行哈希处理,您无需记住所有流入的字节,您只需将每个字节都扔掉到具有恒定内存负载的哈希器中,当所有字节发送完毕后,瞧,您便拥有了哈希值)。相对于提供数据的速度,您正在寻找每个操作很少的状态,并且没有太多的CPU能力。
一些不好的例子:在一个系统中,您需要执行一堆数据库查询(您需要一种异步方式来与您的数据库进行对话,并且一般而言,DB很难同时运行1000个查询),这是一个比特币挖掘操作(比特币挖矿是瓶颈,试图在一台机器上同时处理数千个连接毫无意义)。
关于java - Java中的异步文件NIO有什么好处?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62842128/