这主要是关于手动滚动缓冲区大小。通过这种方式,您可以节省大量内存,但前提是您尝试处理大量(数千个)同时连接。
首先一些简化和警告:
-
我将假设一个非愚蠢的调度程序。有些操作系统只是在处理数千个线程方面做得非常糟糕。当用户进程启动 1000 个完整线程时,操作系统不会出现故障,这没有固有的原因,但有些操作系统无论如何都会崩溃。NIO可以提供帮助,但这是一个不公平的比较 - 通常你应该升级你的操作系统。几乎任何linux,我相信win10肯定没有这么多线程的问题,但是ARM黑客上的一些旧的linux端口,或者像Windows 7这样的东西 - 这可能会导致问题。
-
我假设您正在使用NIO来处理传入的TCP / IP连接(例如Web服务器或IRC服务器之类的)。如果您尝试同时读取 1000 个文件,则同样的原则也适用,但请注意,您确实需要考虑瓶颈所在。例如,从单个磁盘同时读取 1000 个文件是一项毫无意义的练习 - 这只会减慢速度,因为您正在使磁盘的生活更加困难(如果它是旋转磁盘,则计数加倍)。对于网络,特别是如果你在快速管道上,瓶颈不是管道或网卡,这使得“同时处理1000个连接”就是一个很好的例子。事实上,我将以聊天服务器为例,其中1000人都连接到一个巨大的聊天室。这项工作是接收来自任何连接的人的短信,并将其发送给每个人。
同步模型
在同步模型中,生活相对简单:我们将创建2001线程:
- 1 个线程,用于侦听套接字上的新传入 TCP 连接。此线程将创建 2 个“处理程序”线程,并返回侦听新连接。
- 每个用户从套接字读取直到看到回车符号的线程。如果它看到这种情况,它将采用到目前为止收到的所有文本,并使用需要发送的新字符串通知所有1000个“sender”线程。
- 每个用户一个线程,该线程将在“要发送的文本消息”缓冲区中发送字符串。如果没有要发送的内容,它将等到新消息传递给它。
每个单独的移动部件都易于编程。对单一数据类型的一些战术使用,甚至一些基本的块将确保我们不会遇到任何竞争条件。我设想每个部分可能有1页代码。java.util.concurrent
synchronized()
但是,我们确实有2001个线程。每个线程都有一个堆栈。在 JVM 中,每个线程获得相同大小的堆栈(您无法创建线程,但使用不同大小的堆栈),并使用参数配置它的大小。你可以把它们做得小到128k,但即使这样,对于堆栈来说,这仍然是~256MB,我们还没有覆盖任何堆(人们来回发送的所有字符串,卡在发送队列中),或者应用程序本身,或者JVM基础知识。-Xss
128k * 2001
在引擎盖下,有16个内核的CPU会发生什么,有2001个线程,每个线程都有自己的一组条件,这会导致它醒来。对于接收方,它是通过管道传入的数据,对于发送方,它的网卡指示它已准备好发送另一个数据包(以防它正在等待将数据推送到线路上),或者等待调用获得通知(从用户接收文本的线程会将该字符串添加到1000个发送方中每个发送方的所有队列中,然后通知他们所有人)。obj.wait()
这是大量的上下文切换:一个线程唤醒,在缓冲区中看到,将其转换为数据包,将其释放到网卡的内存缓冲区(这一切都非常快,它只是CPU和内存相互作用),并且会重新入睡,例如。然后,CPU内核将继续并找到另一个准备执行某些工作的线程。Joe: Hello, everybody, good morning!
CPU内核具有内核缓存;事实上,有一个层次结构。有主RAM,然后是L3缓存,L2缓存,核心缓存 - 在现代架构中CPU不能再真正在RAM上运行,他们需要芯片周围的基础设施意识到它需要读取或写入不在这些缓存中的页面上的内存,然后CPU将冻结一段时间,直到基础设施可以复制该RAM页面之一缓存。
每次核心切换时,它很可能都需要加载一个新页面,这可能需要数百个周期,其中CPU正在摆动其拇指。一个写得不好的调度程序会导致比需要的多得多的这种情况。如果您阅读NIO的优点,通常会出现“这些上下文切换很昂贵!”-这或多或少是他们正在谈论的(但是,剧透警告:异步模型也受到这种影响!
异步模型
在同步模型中,找出1000个连接用户中哪一个已经准备好发生的事情的工作被“卡”在等待事件的线程中;操作系统正在处理这1000个线程,并在有事情要做时唤醒线程。
在异步模型中,我们将其切换:我们仍然有线程,但要少得多(每个内核一到两个是一个好主意)。这比已连接的用户少得多:每个线程负责所有连接,而不仅仅是1个连接。这意味着每个线程将完成检查哪些连接的用户有事情要做的工作(他们的网络管道有数据要读取,或者准备让我们将更多数据推送到他们)。
不同之处在于线程向操作系统询问的内容:
- [同步]好吧,我想进入睡眠状态,直到这一个连接将数据发送给我。
- [异步]好吧,我想进入睡眠状态,直到这一千个连接中的一个向我发送数据,或者我注册我正在等待网络缓冲区清除,因为我有更多的数据要发送,并且网络是清除的,或者套接字列表器有一个新用户连接。
这两种模型都没有固有的速度或设计优势 - 我们只是在应用程序和操作系统之间转移工作。
NIO经常被吹捧的一个优点是,您不需要“担心”竞争条件,同步,并发安全的数据结构。这是一个经常重复的错误:CPU有许多内核,所以如果你的非阻塞应用程序只做一个线程,你的绝大多数CPU将坐在那里空闲无事,这是非常低效的。
这里最大的好处是:嘿,只有16个线程。这 = 2MB 的堆栈空间。这与同步模型占用的256MB形成鲜明对比!但是,现在发生了另一件事:在同步模型中,有关连接的大量状态信息“卡”在该堆栈中。例如,如果我写这个:128k * 16
假设协议是:客户端发送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
现在,另一个理论上的好处是你不会得到上下文切换 - 而不是CPU和操作系统必须交换到另一个线程,当read()调用没有数据给你,因为网卡正在等待数据,它现在是线程的工作去:好吧,没问题 - 我将转到另一个上下文对象。
但这是一个红鲱鱼 - 操作系统是否正在处理1000个上下文概念(1000个线程),或者您的应用程序是否正在处理1000个上下文概念(这些“跟踪器”对象),这并不重要。它仍然是1000个连接,每个人都在聊天,所以每次你的线程继续检查另一个上下文对象并用更多数据填充其字节数组时,它很可能仍然是一个缓存未命中,CPU仍然会旋转它的拇指数百个周期,而硬件基础设施将适当的页面从主RAM拉入缓存中。因此,该部分并不那么相关,尽管上下文对象较小的事实将在一定程度上减少缓存未命中。
这让我们回到了:主要的好处是你可以手动滚动这些缓冲区,这样做,你既可以使它们更小,又可以动态地调整它们的大小。
异步的缺点
我们有垃圾回收语言是有原因的。我们没有在汇编程序中编写所有代码是有原因的。手工仔细管理所有这些挑剔的细节通常是不值得的。所以它就在这里:通常这种好处是不值得的。但是,就像GFX驱动程序和内核有大量的机器代码,并且驱动程序倾向于在手动管理的内存环境中编写一样,在某些情况下,仔细管理这些缓冲区是非常值得的。
不过,成本很高。
想象一下具有以下属性的理论编程语言:
- 每个函数为红色或蓝色。
- 红色函数可以调用蓝色或红色函数,没问题。
- 蓝色函数也可以同时调用两者,但是如果蓝色函数调用红色函数,则会出现一个几乎不可能测试的错误,但会在实际负载下扼杀您的性能。Blue 只能通过不厌其烦地分别定义调用和对调用结果的响应并将此对注入队列来调用 red 函数。
- 函数往往不记录它们的颜色。
- 某些系统功能为红色。
- 您的函数必须是蓝色的。
这似乎是一种语言的彻底愚蠢的灾难,不是吗?但这正是您在编写异步代码时所生活的世界!
问题是:在异步代码中,你不能调用阻塞函数,因为如果它阻塞了,嘿,这是现在被阻塞的仅有的16个线程之一,这立即意味着你的CPU现在什么都不做1/16。如果所有16个线程最终都处于阻塞部分,则CPU实际上什么都不做,一切都被冻结了。你就是做不到。
有很多东西可以阻止:打开文件,甚至触摸以前从未接触过的类(该类需要从磁盘从jar中加载,验证和链接),就像查看数据库,进行快速网络检查,有时要求当前时间就可以了。即使在调试级别进行日志记录也可能这样做(如果最终写入磁盘,瞧 - 阻止操作)。
您是否知道任何日志记录框架要么承诺启动一个单独的线程来处理磁盘上的日志,要么不厌其烦地记录它是否阻塞?我也不知道。
因此,阻止的方法为红色,异步处理程序为蓝色。Tada - 这就是为什么异步如此难以真正正确的原因。
执行摘要
由于彩色函数问题,编写好异步代码是一个真正的痛苦。它也不是更快地从表面上看 - 事实上,它通常更慢。如果要同时运行数千个操作,并且跟踪每个操作的相关状态数据所需的存储量很小,则可以赢得很大的胜利,因为您可以手动滚动该缓冲区,而不是被迫依赖每个线程 1 个堆栈。
如果你还剩下一些钱,那么,开发人员的薪水会给你买很多RAM,所以通常正确的选择是使用线程,如果你想处理许多同时连接,就选择一个有很多RAM的盒子。
请注意,像youtube,facebook等网站有效地采取了“在RAM上折腾钱”的解决方案 - 他们分片他们的产品,以便许多简单而便宜的计算机协同工作来提供网站。不要敲它。
异步可以真正闪耀的例子是我在此答案中描述的聊天应用程序。另一个是,比方说,你收到一条短消息,你所做的就是对它进行哈希处理,加密哈希值,并用它进行响应(要散列,你不需要记住所有流入的字节,你可以把每个字节扔进具有恒定内存负载的hasher中,当字节全部发送时,瞧, 你有你的哈希)。您正在寻找每个操作的最小状态,并且相对于提供数据的速度而言,CPU 功率也不多。
一些不好的例子:是一个系统,你需要做一堆数据库查询(你需要一种异步的方式来与你的数据库通信,一般来说,数据库很难同时运行1000个查询),比特币挖矿操作(比特币挖矿是瓶颈,试图在一台机器上同时处理数千个连接是没有意义的)。