【问题标题】:Shouldn't asynchronous programming in Node.js cause a StackOverflow?Node.js 中的异步编程不应该导致 StackOverflow 吗?
【发布时间】:2012-01-15 10:10:47
【问题描述】:

我刚刚意识到(单线程)Node.js 存在问题:

  1. 服务器开始响应请求,并且请求一直运行直到它因为 I/O 而阻塞。

  2. 当请求处理器阻塞时,服务器会启动并返回到第 1 步,处理更多请求。

  3. 每当请求处理器阻塞 I/O 时,服务器都会检查是否有任何请求完成。它按 FIFO 顺序处理这些以响应客户端,然后像以前一样继续处理。

如果太多的请求开始相互阻塞并且没有一个完成,这是否意味着在#2 处应该存在堆栈溢出?为什么/为什么不?

【问题讨论】:

  • 为所有请求共享相同的“堆栈”是完全不切实际的——这怎么可能适用于单个线程和多个请求?我猜每个请求都有自己的堆分配(或等效)状态。
  • @Mat:QueueUserAPC 之类的东西很有可能——只是在某个点之后它会爆炸。所以你怀疑 JS 根本没有真正使用 CPU 堆栈来服务线程?
  • 您链接到的那个函数不会向线程的 CPU 堆栈添加任何内容,它会将内容添加到(单独分配的)队列中。 node.js 可以很好地使用这种技术,尽管它们可能会使用可移植的东西(而且很可能是自产的)。试着想出一种方法来在你上面描述的场景中使用实际的 CPU 堆栈,你会发现它不起作用。
  • @Mat: 不,它确实工作 -- 每当线程在警报状态下休眠时(例如使用WaitForSingleObjectEx),任何排队的 APC 都会在线程上调用,然后等待对WAIT_IO_COMPLETION 感到满意。效果很好。
  • 这不是重点。排队的对象不会放在线程的 CPU 堆栈上。线程(暂时)被转移到处理队列中存储的对象。它的 CPU 堆栈用于此目的,但这只是一个额外的帧。当一个排队的对象被处理时,堆栈返回到它的初始状态(并且该进程重新启动)。使用这种技术不存在使堆栈爆裂的风险(只要 1. 当进程被警告时堆栈不是“满的”并且 2. 数据处理本身不会爆裂它)。取决于排队的数据(项目的大小或 n°),线程堆栈不会增加。

标签: javascript node.js


【解决方案1】:

node.js 通过使用异步技术无处不在1来防止您描述的堆栈过度增长。

任何可能阻塞的东西都使用回调进行进一步处理,而不是阻塞调用。这完全避免了堆栈增长,并且可以轻松地重新进入事件循环(“驱动”底层真实 I/O 和请求调度)。

考虑这个伪代码:

fun() {
  string = net.read();
  processing(string);
}

线程在读取时被阻塞,只有在两次读取完成后才能释放堆栈,并且processing 已完成。

现在如果你所有的代码都是这样的:

fun() {
  net.read(onDone: processing(read_data));
}

如果你像这样实现read

net.read(callback) {
  iorequest = { read, callback };
  io.push_back(iorequest);
}

funread 可以将读取 I/O 与相关回调排队时立即完成。 fun 的堆栈在没有阻塞的情况下被回绕 - 它“立即”返回到事件循环,没有任何线程堆栈剩余。

即您可以继续下一个回调(重新进入事件循环),而无需在线程堆栈上保留任何每个请求的数据。

所以node.js 通过在“用户”代码中发生阻塞调用的任何地方使用异步回调来避免堆栈过度增长。

有关此内容的更多信息,请查看node.js 'about' 页面,以及最后链接的第一组幻灯片。

1好吧,差不多我猜


您在评论中提到了QueueUserAPC。通过这种类型的处理,队列中的 APC 被允许阻塞,队列中的下一个 APC在线程的堆栈上得到处理,使其成为“递归”调度。

假设我们有 3 个 APC 待处理(ABC)。我们得到:

初始状态:

Queue   ABC
Stack   xxxxxxxx

线程休眠,因此 APC 调度开始,进入 A 的处理:

Queue   BC
Stack   AAAAxxxxxxxx

A 阻塞,B 被调度在同一个堆栈上

Queue   C
Stack   BBBBBBAAAAxxxxxxxx

B 块,C 被调度:

Queue   
Stack   CCCCCCCBBBBBBAAAAxxxxxxxx

很明显,如果有足够多的阻塞 APC 处于挂起状态,堆栈最终会炸毁。

使用node.js,请求不允许阻塞。相反,这里是相同的三个请求会发生什么的模型:

Queue      ABC
Stack      xxxxxxxx

A开始处理:

Queue      BC
Stack      AAAAxxxxxxxx

现在 A 需要做一些阻止 - 在node.js 中,它实际上不能。它的作用是将另一个请求 (A') 排队(可能带有上下文 - 简单地说是包含所有变量的哈希):

I/O queue  A'
Queue      BC
Stack      AAAAxxxxxxxx

然后A 返回并回到:

I/O queue  A'
Queue      BC
Stack      xxxxxxxx

注意:不再有 A 堆栈帧。 I/O 挂起队列实际上由操作系统管理(使用epollkqueue 或其他)。主线程在事件循环中检查 OS I/O 就绪状态和挂起(需要 CPU)队列。

然后B得到一些CPU:

I/O queue  A'
Queue      C
Stack      BBBBBBBxxxxxxxx

同样的事情,B 想做 I/O。它排队一个新的回调并返回

I/O queue  A'B'
Queue      C
Stack      xxxxxxxx

如果 B 的 I/O 请求同时完成,下一个快照可能如下所示

I/O queue  A'
Queue      B'
Stack      CCCCCxxxxxxxx

处理线程上的回调堆栈帧绝不会超过一个。 API 不提供阻塞调用,该堆栈没有表现出 APC 模式的递归增长类型。

【讨论】:

  • 你可以“释放”一个线程的堆栈吗?如何?回调不会在同一个线程(和堆栈)上调用吗?
  • “发布”不是正确的术语。 fun 的堆栈帧非常短暂,fun 在请求排队后立即返回。 fun 本身可能是一个回调,关键是每个回调都返回到事件循环而不阻塞。这种方法的事件处理没有递归堆栈增长,下一个回调是从顶层事件循环开始的,而不是嵌套的堆栈帧。
【解决方案2】:

【讨论】:

    【解决方案3】:

    在 Node.js 中使用事件循环有几个关键方面与使用线程不同。

    在 Node.js 中,运行时不会在中间中断您的函数以开始执行另一个函数。相反,您必须 Node.js 并发启动之前从当前函数返回

    function readAndWriteItem(id) {
      var callback = function(item) {
        item.title = item.title + " " + item.title;
        writeItem(item);
      };
      readItem(id, callback);
    };
    

    在本例中创建回调闭包并调用readItem 的节点。据推测,readItem 会将查询的发出排队并设置自己的内部回调,以便在查询结果准备好时执行。所以这个示例函数readAndWriteItem 只是将要通过网络发送的消息排队,设置更多回调,并立即返回。一旦这个函数返回,Node.js 就可以发挥它的事件循环魔法了。

    由于函数已返回,并且由于在您使用 Node.js 时全面出现这种情况,因此没有堆栈过低。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-06-10
      • 2015-09-12
      • 2018-12-26
      • 2012-01-19
      • 1970-01-01
      相关资源
      最近更新 更多