所以我了解Node.js是如何工作的:它有一个侦听器线程,它接收一个事件,然后将它委托给一个工作池。工作线程在完成工作后通知侦听器,然后侦听器将响应返回给调用者。
我的问题是:如果我在Node.js中站起来一个HTTP服务器并在我的一个路由路径事件(例如“/ test / sleep”)上调用sleep,整个系统就会停止运行。甚至是单个监听器线程。但我的理解是这个代码发生在工作池上。
现在,相比之下,当我使用Mongoose与MongoDB通信时,DB读取是一种昂贵的I / O操作。 Node似乎能够将工作委托给一个线程并在完成时接收回调;从DB加载所花费的时间似乎并没有阻止系统。
Node.js如何决定使用线程池线程与侦听器线程?为什么我不能编写睡眠的事件代码并且只阻塞线程池线程?
答案 0 :(得分:217)
您对节点如何工作的理解并不正确......但这是一种常见的误解,因为情况的实际情况实际上相当复杂,并且通常归结为像#34这样简洁的小词。 ;节点是单线程的"过度简化的事情。
目前,我们将忽略通过cluster和webworker-threads的显式多处理/多线程,并且只讨论典型的非线程节点。
节点在单个事件循环中运行。它是单线程的,你只能得到那个线程。您编写的所有javascript都在此循环中执行,如果在该代码中发生阻塞操作,那么它将阻止整个循环,并且在完成之前不会发生任何其他操作。这是您经常听到的节点的典型单线程特性。但是,这不是全局。
某些通常用C / C ++编写的函数和模块支持异步I / O.当您调用这些函数和方法时,它们会在内部管理将调用传递给工作线程。例如,当您使用fs
模块请求文件时,fs
模块将该调用传递给工作线程,该工作程序等待其响应,然后将其呈现给事件在此期间没有它的循环一直在搅拌。所有这些都是从您(节点开发人员)那里抽象出来的,其中一些是通过使用libuv从模块开发人员中抽象出来的。
正如Denis Dollfus在评论中指出的那样(从this answer到类似问题),libuv用于实现异步I / O的策略并不总是一个线程池,特别是在{ {1}}模块此时似乎使用了不同的策略。对于我们这里的目的,注意如何实现异步上下文(通过使用libuv)以及由libuv维护的线程池是该库提供的实现异步性的多种策略之一,这一点非常重要。
在一个主要相关的切线上,有一个更深入的分析节点如何实现异步性,以及一些相关的潜在问题以及如何处理它们,in this excellent article。其中大部分都是基于我上面所写的内容,但另外它指出:
http
环境变量增加线程池的大小来缓解这种情况,只要您在需要创建线程池之前执行此操作:UV_THREADPOOL_SIZE
如果您想在节点中进行传统的多处理或多线程,您可以通过内置的process.env.UV_THREADPOOL_SIZE = 10;
模块或其他各种模块(例如前面提到的cluster
)来获取它,或者您可以伪造它通过实施某种方式来整理您的工作并手动使用webworker-threads
或setTimeout
或setImmediate
暂停您的工作并在以后的循环中继续它以让其他流程完成(但是&#39} ;不推荐)。
请注意,如果您在javascript中编写长时间运行/阻止代码,则可能会犯错误。其他语言的表现会更有效率。
答案 1 :(得分:17)
所以我了解Node.js是如何工作的:它有一个侦听器线程,它接收一个事件,然后将它委托给一个工作池。工作线程在完成工作后通知侦听器,然后侦听器将响应返回给调用者。
这不是很准确。 Node.js只有一个执行javascript的“worker”线程。节点中有线程处理IO处理,但将它们视为“工作者”是一种误解。实际上只有IO处理和节点内部实现的一些其他细节,但作为程序员,除了一些misc参数(如MAX_LISTENERS)之外,你不能影响它们的行为。
我的问题是:如果我在Node.js中站起来一个HTTP服务器并在我的一个路由路径事件(例如“/ test / sleep”)上调用sleep,整个系统就会停止运行。甚至是单个监听器线程。但我的理解是这个代码发生在工作池上。
JavaScript中没有睡眠机制。如果您发布了您认为“睡眠”的含义的代码片段,我们可以更具体地讨论这个问题。例如,没有这样的函数可以调用模拟python中的time.sleep(30)
之类的东西。有setTimeout
但从根本上说这不是睡觉。 setTimeout
和setInterval
显式释放,而不是阻止事件循环,以便其他代码位可以在主执行线程上执行。你可以做的唯一事情就是忙着用内存计算循环CPU,这实际上会使主执行线程挨饿并使你的程序无响应。
Node.js如何决定使用线程池线程与侦听器线程?为什么我不能编写睡眠的事件代码并且只阻塞线程池线程?
网络IO始终是异步的。故事结局。磁盘IO具有同步和异步API,因此没有“决定”。 node.js将根据您调用sync和normal async的API核心函数来运行。例如:fs.readFile
vs fs.readFileSync
。对于子进程,还有单独的child_process.exec
和child_process.execSync
API。
经验法则始终使用异步API。使用同步API的正当理由是在网络服务中监听连接之前的初始化代码,或者是不接受构建工具的网络请求的简单脚本等等。
答案 2 :(得分:3)
线程池如何使用以及何时使用:
首先,当我们在计算机上使用/安装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
答案 3 :(得分:0)
这种误解仅仅是先发制人的多任务处理和合作多任务处理之间的区别......
睡眠会关闭整个狂欢节,因为所有游乐设施都有一条线,你关上了门。把它想象成“一个JS解释器和其他一些东西”并忽略线程......对你来说,只有一个线程,......
...所以不要阻止它。