Node.js 何时会发生阻塞?

9

我已经使用Node.js有一段时间了,现在才意识到它可能会阻塞。我无法理解Node.js何时变成阻塞状态的条件。

  • 因此,Node.js是单线程的,因为(i)Javascript是单线程的,(ii)避免了所有多线程陷阱。
  • 尽管是单线程的,但为了同时执行许多任务,它实现了异步执行。因此,与数据库通信(总体上是I/O)是非阻塞的(因为它是异步的)。
  • 但是,所有要求做一些工作的传入请求(即与数据库通信)以及必须返回客户端的所有工作结果(即发送某些数据)都使用该单个线程。
  • Node.js在单个线程内使用“事件循环”来获取所有请求并将其分配给非阻塞I/O任务。

因此,由于异步回调,I/O任务是非阻塞的,但是单个线程可以阻塞,因为它是同步的,并且因为事件循环可能会被卡住,因为许多复杂的请求同时出现?

  1. 我理解得对吗?我猜不是,因为这里这里强调了“Node是单线程的,这意味着您的代码没有并行运行”。那实际上是什么意思,它如何使Node阻塞?
  2. 因此,事件循环会永远运行并始终搜索请求,还是在发现新请求后开始执行?
  3. Node的阻塞弱点是否使其对于大型项目无用,并最终适用于只有微型站点和小型项目?

非常感谢。


https://dev59.com/dGUq5IYBdhLWcg3wF8nw?rq=1 - Shanoor
3个回答

8
首先,明确一点:node.js整体上并不是单线程的。Node使用libuv线程池执行某些任务,这些任务在大多数平台上无法高效地从单个线程执行(例如文件I/O),或者它们本质上需要计算密集型操作(例如zlib)。值得注意的是,大多数crypto模块(也将是计算密集型)目前没有异步/非阻塞接口(除了crypto.randomBytes())。
v8还利用多线程进行垃圾回收、函数优化等操作。
然而,在node中几乎所有其他操作都发生在同一个单线程中。
现在具体回答您的问题:
  1. JavaScript代码在单线程中运行并不会导致Node.js阻塞。正如这个回答所解释的那样,Node.js主要是为了(I/O)并发而不是(代码)并行。例如,您可以使用内置的cluster模块在多核/CPU系统上并行运行Node.js代码,但是Node.js的主要目标是能够处理大量的I/O并发操作,而无需为每个套接字/服务器等专门分配一个线程。

  2. 这里有一篇好的详细介绍事件循环的文章,介绍了Node.js中事件循环的工作原理。

  3. 正如之前所描述的,Node.js的主要目标是很好地处理I/O,并适用于Web应用程序和任何类型的网络程序。

    如果您的脚本受到CPU限制(例如,您正在计算π或转码音频/视频),则最好将该任务委托给Node.js的子进程(例如,调用ffmpeg进行转码,而不是在JavaScript或同步地在Node.js主线程上的C++ Node.js插件中完成)。如果您没有同时进行其他任务(例如处理HTTP请求),也可以在进程内部执行这些阻塞操作。许多人将Node.js用于执行各种实用程序任务,其中I/O并发性不是很重要。例如,可以创建一个脚本来压缩、检查语法和/或打包JS和CSS文件,或者可以创建一个脚本来从大量图像中创建缩略图。

    但是,如果您的脚本创建了TCP或HTTP服务器,例如从数据库中获取信息,格式化它,并将其发送回用户,则Node.js将非常擅长执行该操作,因为在该过程中花费的大部分时间都是等待套接字/HTTP客户端发送(更多)数据,等待数据库从查询结果中回复。


好的,如果我将所有繁重的任务和占用CPU的脚本放在非阻塞的异步函数中,那么这些函数仍然需要很长时间,但它不会阻塞 Node,因为 CPU 可以空闲地去做其他事情,而不是等待响应。我说这话的基础是从 这里 ,部分Node.js keeps a single thread for your code.....however, everything runs in parallel except your code. - slevin
1
现在,我猜当Node被留下许多需要很长时间才能响应的非阻塞函数时,它将开始出现问题。例如,如果Node被留下200个I/O非阻塞函数,每个函数都需要1秒钟才能响应,即使从理论上讲它是非阻塞的,这也会导致Node出现堵塞。而且,它还会使事件循环陷入困境,因为添加到事件循环队列中的新回调必须等待执行所有先前的回调。这就是Grant在这里遇到的问题(https://medium.com/@theflapjack103/the-way-of-the-gopher-6693db15ae1f#.r5q4lm89w)。 - slevin
我是对的吗?我理解正确了吗?谢谢你的时间,我的朋友。 - slevin

4
让我们直接回答这个问题。
1. 是的,如果你这样看的话,Node.js是完全阻塞的。假设你从数据库读取了一个巨大的、占用一半G的CSV文件,然后尝试将其转换为json并发送到客户端(我知道这很天真,但请跟我走)。
JSON编码基本上就是字符串操作,这在很多语言中都可能会很慢,不仅仅是JavaScript。所以如果编码这样的json需要20秒钟,你会异步加载这个CSV文件,但是你会花费20秒钟来解析字符串。在此期间,没有任何其他回调、请求或文件系统可以运行-除了那个单一的“JSON.stringify()”函数外,您的所有实际编程运行都无法进行。
有方法可以解决这个特定的问题,但你应该意识到-如果你的单个函数或类似JSON.stringify的单个语句需要很长时间,它将会阻塞。你需要在编写应用程序时考虑到这一点。
2. 从本质上讲,事件循环会睡眠,直到其主队列上没有任务。还可以有其他队列,例如回调队列,其中您可以等待数十个数据库操作结果。但是只有当它们实际上通过数据库回复被回调时,它们才会加入到您的主事件循环中。
假设你正在解析上面的JSON,在此期间,你收到了5个新的请求,无论是针对它还是其他东西。这5个请求直接进入队列,一旦有请求完成,事件循环就会检查下一个需要处理的请求。如果没有请求,它就等待。
3. 阻塞并不会使Node失去用处。如果是这样的话,那些不是异步的单线程语言会在大规模情况下做什么呢?
Node已经被用于大型项目中,我相信你可以通过谷歌来找到很多。关键是要为适当的解决方案使用适当的工具-因此,在处理CPU密集型任务时,Node.js可能需要使用不同的策略,或者甚至可能不是最好的工具。

谢谢。但是等一下。如果我将“json-encoding-task”放在回调函数中并使其异步,为什么Node会阻塞?任务肯定需要20秒,但现在它在异步函数内部。它是非阻塞的,因此长时间任务发生在该异步函数内部,CPU可以做其他事情。所以现在CPU完成其他任务,当“json-encoding-task”完成时,它也会向客户端返回数据。 - slevin
该回调函数将在该线程上完全阻塞。 "单线程" 的核心是 一次只能运行一个用户定义的函数。Node 有这些维护线程,但您的程序只有一个线程,在单个节点进程上运行。如果您的程序可以在一秒钟内处理 100 个小请求,然后每个长回调需要 10 秒钟,那么总共需要 1 + 10*100 = 1001 秒,粗略地说-1 来服务所有这些初始请求和每个回调需要 10 秒钟。这就是为什么您要将长任务推入队列,并在另一个进程可以执行它们时执行它们的原因。 - Zlatko
你的主线程只处理那100个请求 - 而且可能可以处理更多,因为大部分时间它只是保持等待状态。 - Zlatko
所以根据这里的说法,一个长时间运行的任务将会阻塞自己的线程,而不是整个Node。如果同时发生了许多长时间运行的任务,Node将会被阻塞。它们都需要很长时间才能回答。添加到事件循环队列中的新长回调将必须等待所有先前的回调执行完毕。这就是Grant在这里遇到的问题。我说得对吗?谢谢。 - slevin
基本上,我相信。 - Zlatko

1

让我检查一下。

Node.js是单线程的,因此它的代码不能并行运行,但其I/O可以是并发的。我们使用异步JavaScript函数来实现这一点。所以这就是为什么I/O是非阻塞的。

Node.js为您的代码保留了一个单线程......但是,除了您的代码之外,所有内容都在并行运行。

例如,进行“休眠”将会使服务器阻塞一秒钟。- 单线程代码

所有I/O都是事件驱动和异步的,因此以下操作不会阻塞服务器:c.query('SELECT SLEEP(20);', .... - “休眠”位于异步函数内,查询 - 非阻塞I/O(来源:here

为了管理传入的请求,Node实现了“事件循环”。

事件循环是“处理和处理外部事件并将它们转换为回调调用的实体”。因此,I/O调用是Node.js可以从一个请求切换到另一个请求的点。在I/O调用中,您的代码保存回调并将控制返回给node.js运行时环境。当数据实际可用时,稍后将调用回调。 (来自这里
因此,I/O是非阻塞的,因为Node可以做其他事情而不是等待某些I/O完成。
如果请求需要太长时间才能回答,Node将为该请求分配一个线程池中的线程。
那个线程负责接收该请求,处理它,执行阻塞IO操作,准备响应并将其发送回事件循环。事件循环再将响应发送给相应的客户端。(来自这里
  1. 因此,如果有很多简单的请求,I/O 是非阻塞的,并且这些请求的所有回调都能够非常快速地响应。

(从这一点开始我不确定我是否理解正确)

2. 许多简单的请求和一个复杂的请求。 复杂的请求可能是一个繁重的任务、一个图像调整算法或任何需要时间的东西。 每个请求都在一个异步函数中,Node将为复杂的请求定义一个线程。 大多数简单的请求将立即响应。 复杂的请求将花费一些时间,在它自己的线程中,而简单的请求仍然在响应(因为非阻塞)。 但是事件循环中的回调按特定顺序排队(先进先出,对吗?)。
“在一个循环中,队列会轮询下一条消息(每次轮询称为“tick”),当遇到消息时,将执行该消息的回调函数。调用此回调函数作为调用堆栈中的初始帧,由于JavaScript是单线程的,因此在堆栈上所有调用返回之前,进一步的消息轮询和处理将停止。后续(同步)函数调用将向堆栈添加新的调用帧。”(来自这里的引用)
所以在复杂请求的回调之后进行的简单请求的回调需要一些时间来响应,因为复杂请求的回调需要很长时间。
3. 大量复杂请求,每个请求都在其自己的异步函数内部。如果每个请求需要1秒钟才能响应,而我们有10000个响应,则时间总和会增加。它们最终都会在使用事件循环的单线程节点中累加。在事件循环内部,每个需要很长时间响应的回调函数都排在另一个需要很长时间响应的回调函数后面。
我认为以上描述了Grant的问题here。那是我第一次读到有关node缺点的文章,至今仍不知道是否理解正确。因此,
我们的Node服务可能已经像冠军一样处理了传入请求,如果它所需的只是立即可用的数据。
但是,
Node是单线程的,这意味着你的代码不会并行运行。I/O可能不会阻塞服务器,但你的代码肯定会。如果我调用sleep 5秒钟,我的服务器在此期间将无响应。
Grant发现自己有很多请求需要花费时间,因为亚马逊服务很慢。
......正在等待大量嵌套回调,所有这些回调都依赖于来自S3的响应(有时可能非常慢)。
然后事件循环杀死了一切。
在循环中,队列轮询下一个消息(每个轮询称为“tick”),当遇到消息时,执行该消息的回调。调用此回调函数作为调用堆栈中的初始帧,并且由于JavaScript是单线程的,进一步的消息轮询和处理在返回堆栈上所有调用之前停止。随后(同步)函数调用向堆栈添加新的调用帧.....当任何请求超时发生时,事件及其关联的回调被放置在已经过载的消息队列上。虽然超时事件可能发生在1秒钟内,但在所有其他消息当前在队列上以及它们相应的回调代码完成执行之前,回调不会得到处理(可能几秒钟后)。
我不确定我是否理解正确。请随意指出我的错误并帮助我完全正确。谢谢。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接