什么时候使用线程池?


104

因此,我对Node.js的工作方式有所了解:它具有一个侦听器线程,该线程接收事件,然后将其委派给工作池。工作线程在完成工作后会通知侦听器,然后侦听器将响应返回给调用者。

我的问题是:如果我在Node.js中站起一个HTTP服务器,并在我的一个路由路径事件(例如“ / test / sleep”)中调用sleep,那么整个系统就会停顿下来。甚至是单个侦听器线程。但是我的理解是这段代码正在工作池中发生。

现在,相比之下,当我使用Mongoose与MongoDB交谈时,数据库读取是一项昂贵的I / O操作。Node似乎能够将工作委托给线程并在完成时接收回调。从数据库加载所需的时间似乎不会阻塞系统。

Node.js如何决定使用线程池线程还是侦听器线程?为什么我不能编写仅休眠并阻塞线程池线程的事件代码?


@Tobi-我已经看到了。它仍然没有回答我的问题。如果工作在另一个线程上,则睡眠只会影响该线程,而不会影响侦听器。
Haney

8
一个真正的问题,在这里您尝试自己理解一些东西,而当您找不到迷宫的出口时,您会寻求帮助。
拉斐尔·伊恩

Answers:


240

您对节点如何工作的理解是不正确的……但这是一个普遍的误解,因为这种情况的实际情况实际上相当复杂,并且通常归结为诸如“节点是单线程”之类的小词,过分简化了事情。

目前,我们将忽略通过集群webworker-threads进行的显式多处理/多线程,仅讨论典型的非线程节点。

节点在单个事件循环中运行。它是单线程的,您只能获得一个线程。您编写的所有JavaScript都会在此循环中执行,并且如果该代码中发生了阻塞操作,则它将阻塞整个循环,直到完成为止,否则将不会发生其他事情。这是您经常听到的节点的典型单线程性质。但是,这还不是全部。

通常使用C / C ++编写的某些功能和模块支持异步I / O。当您调用这些函数和方法时,它们在内部管理将调用传递给工作线程。例如,当您使用该fs模块请求文件时,该fs模块将该调用传递给工作线程,该工作线程等待其响应,然后将其呈现回事件循环,该循环在没有它的情况下一直在进行与此同时。所有这些都是从您(节点开发人员)那里抽象出来的,其中一些是通过使用libuv从模块开发商那里抽象出来的。

正如Denis Dollfus在评论中指出的(从此答案到类似的问题),libuv用于实现异步I / O的策略并不总是线程池,特别是在http模块的情况下,似乎采用了不同的策略。目前使用。就我们的目的而言,最重要的是要注意如何实现异步上下文(通过使用libuv),并且libuv维护的线程池是该库提供的实现异步的多种策略之一。


在一篇非常相关的切线上,这篇优秀的文章对节点如何实现异步性以及一些相关的潜在问题以及如何解决这些问题进行了更深入的分析。它的大部分内容是在我上面写的内容基础上扩展的,但是另外指出:

  • 您包含在项目中的任何使用本机C ++和libuv的外部模块都可能使用线程池(请考虑:数据库访问)
  • libuv的默认线程池大小为4,并使用队列来管理对该线程池的访问-结果是,如果您有5个长时间运行的数据库查询全部同时进行,则其中一个(以及任何其他异步查询)依赖于线程池的操作)将等待查询结束,甚至无法开始
  • 您可以通过UV_THREADPOOL_SIZE环境变量增加线程池的大小来缓解这种情况,只要您在需要并创建线程池之前就这样做即可:process.env.UV_THREADPOOL_SIZE = 10;

如果您希望在节点中使用传统的多处理或多线程,则可以通过内置cluster模块或上述其他各种模块来获取webworker-threads,也可以通过实现某种方式将工作分块并手动使用setTimeout或进行伪造。setImmediateprocess.nextTick暂停您的工作并在以后的循环中继续进行,以完成其他过程(但不建议这样做)。

请注意,如果您使用javascript编写长时间运行/阻止的代码,则可能是在犯错误。其他语言将更有效地执行。


1
天哪,这完全为我清除了。非常感谢@Jason!
Haney

5
没问题:)我发现自己不久以前就在哪里,很难找到一个明确的答案,因为一方面您有答案很明显的C / C ++开发人员,另一方面您有典型的答案。以前还没有深入研究这类问题的网络开发人员。当您降至C级时,我什至不确定我的答案在技术上是100%正确的,但从广义上讲是正确的。
2014年

3
使用线程池处理网络请求将浪费大量资源。根据这个问题, “它基于不同平台(例如epoll,kqueue和IOCP)中的异步I / O接口执行异步网络I / O,而没有线程池”,这很有意义。
Denis Dollfus 2014年

1
...也就是说,如果您直接在主要的JavaScript线程中进行了一些繁重的工作,或者您没有足够的资源或没有适当地管理它们来为线程池提供足够的空间,那么您可能会在较低的并发性上引入延迟阈值-结果是,对于相同的系统资源,与其他选项相比,使用node.js通常会遇到更高的吞吐量(尽管其他语言的其他基于事件的系统旨在挑战这一点-我没有,但基于最近的基准测试)-很明显,基于事件的模型的性能优于线程模型。
杰森

1
@Aabid侦听器线程不会执行数据库查询,因此所有10个查询都将花费大约6秒钟的时间(默认线程池大小为4)。如果您需要使用JavaScript进行任何不需要完成该数据库查询结果的工作,例如,有更多请求不需要线程池完成任何异步工作,则它将继续在主线程中工作事件循环。
杰森

20

因此,我对Node.js的工作方式有所了解:它具有一个侦听器线程,该线程接收事件,然后将其委派给工作池。工作线程在完成工作后会通知侦听器,然后侦听器将响应返回给调用者。

这不是很准确。Node.js只有一个执行Javascript的“工作者”线程。节点中有一些线程可以处理IO处理,但是将它们视为“工人”是一种误解。确实只有IO处理和节点内部实现的其他一些细节,但是作为程序员,除了一些杂项参数(例如MAX_LISTENERS)之外,您不能影响其行为。

我的问题是:如果我在Node.js中站起一个HTTP服务器,并在我的一个路由路径事件(例如“ / test / sleep”)中调用sleep,那么整个系统就会停顿下来。甚至是单个侦听器线程。但是我的理解是这段代码正在工作池中发生。

JavaScript中没有睡眠机制。如果您发布了您认为“睡眠”意味着什么的代码段,我们可以对此进行更具体的讨论。例如,没有此类函数可调用来模拟类似time.sleep(30)python的内容。有,setTimeout但是从根本上来说是不睡觉的。setTimeoutsetInterval显式释放(而不是阻塞)事件循环,以便其他代码位可以在主执行线程上执行。唯一可以做的就是利用内存中计算忙循环CPU,这实际上会使主执行线程饿死,并使程序无响应。

Node.js如何决定使用线程池线程还是侦听器线程?为什么我不能编写仅休眠并阻塞线程池线程的事件代码?

网络IO始终是异步的。故事结局。磁盘IO具有同步和异步API,因此没有“决定”。node.js将根据您称为同步与常规异步的API核心功能运行。例如:fs.readFilevs fs.readFileSync。对于子进程,也有单独的child_process.execchild_process.execSyncAPI。

经验法则始终使用异步API。使用同步API的有效原因是在网络服务中侦听连接之前的初始化代码,或者是在不接受网络对构建工具等要求的简单脚本中的初始化代码。


1
这些异步API来自何处?我明白您的意思,但是无论谁编写此API,都选择加入IOCP /异步。他们是如何选择这样做的?
Haney

3
他的问题是他将如何编写自己的时间密集型代码而不阻塞。
杰森

1
是。Node提供基本的UDP,TCP和HTTP网络。它仅提供异步“基于池”的API。世界上所有的node.js代码都无一例外地使用了这些基于池的异步API,因为只有所有可用的。文件系统和子进程是另一回事,但是网络始终是异步的。
Peter Lyons 2014年

4
彼得,小心点,免得你成为他水壶的锅子。他想知道网络API的编写者是如何做到的,而不是使用网络API的人们是如何做到的。我最终了解了节点如何表现非阻塞事件,因为我想编写自己的与网络或任何其他内置的异步API无关的非阻塞代码。很明显,大卫想做同样的事情。
杰森

2
Node不使用线程池进行IO,而是使用本机非阻塞IO,fs据我所知唯一的例外是
vkurchatkin 2014年

2

线程池如何使用以及何时使用:

首先,当我们在计算机上使用/安装Node时,它将在其他进程中启动一个进程,该进程在计算机中称为节点进程,并且一直运行直到您杀死它。这个运行过程就是我们所谓的单线程。

在此处输入图片说明

因此,单线程机制很容易阻止节点应用程序,但这是Node.js带到表中的独特功能之一。因此,再次运行节点应用程序,它将仅在单个线程中运行。无论您有1个或1百万个用户同时访问您的应用程序。

因此,让我们确切地了解启动节点应用程序时在nodejs单线程中会发生什么。首先,程序被初始化,然后执行所有顶级代码,这意味着所有不在任何回调函数内的代码(请记住,所有回调函数内的所有代码都将在事件循环下执行)。

之后,执行的所有模块代码然后注册所有回调,最后,为您的应用程序启动事件循环。

在此处输入图片说明

因此,正如我们之前讨论的那样,所有回调函数和这些函数中的代码将在事件循环下执行。在事件循环中,负载分布在不同的阶段。无论如何,我不会在这里讨论事件循环。

为了更好地理解线程池,我想请您想象一下,在事件循环中,一个回调函数内部的代码在完成另一个回调函数内部的代码执行之后才执行,现在如果有些任务实际上太繁琐了。然后,它们将阻塞我们的nodejs单线程。因此,这就是线程池进入的地方,就像事件循环一样,由libuv库提供给Node.js。

因此,线程池不是nodejs本身的一部分,它是libuv提供的,用于将繁重的工作分担给libuv,libuv将在其自己的线程中执行这些代码,并且libuv将在执行后将结果返回给事件循环。

在此处输入图片说明

线程池为我们提供了四个额外的线程,这些线程与主单线程完全分开。实际上,我们最多可以配置128个线程。

因此,所有这些线程一起形成了一个线程池。然后事件循环可以自动将繁重的任务卸载到线程池中。

有趣的是,所有这些都是在幕后自动发生的。决定什么去线程池,什么不去线程,不是我们的开发人员来决定的。

有很多任务转到线程池,例如

-> All operations dealing with files
->Everyting is related to cryptography, like caching passwords.
->All compression stuff
->DNS lookups

0

这种误解仅仅是抢先式多任务处理与协作式多任务处理之间的区别...

睡眠关闭了整个狂欢节,因为所有游乐设施实际上只有一条路线,而您关上了大门。将其视为“ JS解释器和其他一些东西”,而忽略线程...对于您来说,只有一个线程,...

...所以不要阻止它。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.