Node.js I / O事件是单线程还是多线程?
如果我有多个I / O进程,则节点将它们放在外部事件循环中。它们是按顺序处理(最快的第一个)还是处理事件循环以同时处理它们(......以及哪些限制)?
答案 0 :(得分:82)
事件循环
Node.js事件循环在单个线程下运行,这意味着您编写的应用程序代码在单个线程上进行评估。 Nodejs本身通过libuv使用下面的许多线程,但是在编写nodejs代码时你永远不必处理它们。
每次涉及I / O调用的呼叫都要求您注册回调。此调用也会立即返回,这允许您并行执行多个IO操作,而无需在应用程序代码中使用线程。一旦I / O操作完成,它的回调将在事件循环中被推送。它将在执行之前推送到事件循环的所有其他回调中立即执行。
有几种方法可以对回调如何添加到事件循环进行基本操作。 通常你不应该需要这些,但偶尔它们会很有用。
在任何时候都不会有两个真正的并行执行路径,因此所有操作本质上都是线程安全的。通常会有几个由事件循环管理的异步并发执行路径。
Read More about the event loop
<强>限制强>
由于事件循环,节点不必为每个传入的tcp连接启动新线程。这允许节点同时服务hundreds of thousands of requests,只要您不为每个请求计算前1000个素数。
这也意味着不进行CPU密集型操作很重要,因为这些操作会锁定事件循环并阻止其他异步执行路径继续执行。
不使用所有I / O方法的sync
变体也很重要,因为这些方法也会锁定事件循环。
如果你想做CPU繁重的事情,你应该将它委托给另一个可以更有效地执行CPU绑定操作的进程,或者你可以把它写成node native add on。
控制流程
为了管理编写许多回调,您可能希望使用控制流库。 我相信这是目前最流行的基于回调的库:
我使用了回调,他们几乎让我疯狂,我使用Promise有更好的经验,bluebird是一个非常受欢迎且快速的承诺库:
我发现这是节点社区中一个非常敏感的话题(回调与承诺),所以无论如何,请使用您认为最适合您个人的内容。一个好的控制流库也应该为您提供异步堆栈跟踪,这对于调试非常重要。
当事件循环中的最后一个回调完成它的执行路径并且没有注册任何其他回调时,Node.js进程将完成。
这不是一个完整的解释,我建议你查看以下帖子,它是最新的:
答案 1 :(得分:22)
来自Willem的回答:
Node.js事件循环在单个线程下运行。每次I / O调用都要求您注册回调。每个I / O调用也会立即返回,这允许您在不使用线程的情况下并行执行多个IO操作。
我想开始用上面的引言来解释,这是我在各处看到的节点js框架的常见误解之一。
Node.js不会只用一个线程神奇地处理所有那些异步调用,并且仍然保持该线程不被阻塞。它在内部使用google的V8引擎和一个名为libuv的库(用c ++编写),使它能够将一些潜在的异步工作委托给其他工作线程(有点像一个线程池,等待从主节点线程委派的任何工作) )。然后,当这些线程完成执行时,它们会调用它们的回调,这就是事件循环如何知道工作线程的执行完成的事实。
nodejs的主要优点是你永远不需要关心那些内部线程,他们会远离你的代码!通常在多线程环境中发生的所有令人讨厌的同步内容将由nodejs框架抽象出来,您可以在更加程序员友好的环境中愉快地处理单个线程(主节点线程)(同时受益于多个线程的所有性能增强)线程)。
如果有兴趣,以下是一篇好文章: When is the thread pool used?
答案 2 :(得分:0)
实际上,节点js核心实现使用两个组件:
v8 javascript运行时引擎
libuv用于手动进行非I / O阻止操作并为您处理线程和并发操作;
使用javascript,您实际上可以用一个线程编写代码,但这并不意味着您的代码可以在一个线程上执行,尽管您可以使用节点js中的集群在多个线程上执行
let fs = require('fs');
fs.stat('path',(err,stat)=>{
//do something with the stat;
console.log('second');
});
console.log('first');
答案 3 :(得分:0)
嗯,要了解事件中的nodejs I / O事件,您必须正确理解nodejs事件循环。
从名称事件循环中我们了解到,这是一个循环循环循环运行的循环,直到循环中没有事件保留或应用程序关闭为止。
事件循环是nodejs中最重要的功能之一,它是在nodejs中进行异步编程的原因。
程序启动时,我们在运行事件循环的单线程中处于节点进程中。现在,最重要的事情是我们需要知道,事件循环是回调函数内部所有应用程序代码的执行位置。
因此,基本上所有不是顶级代码的代码都将在事件循环中运行。某些部分(通常是繁重的工作)可能会转移到线程池中 (When is the thread pool used?),事件循环将处理那些繁重的工作,并将结果返回到事件循环的事件中。
这是节点体系结构的核心,nodejs围绕回调函数构建。因此,由于节点使用事件触发的体系结构,因此在将来某个时间完成某些工作后就会立即触发回调。
当应用程序在节点服务器上收到HTTP请求,计时器到期或读取文件完成时,一旦完成所有工作,就会发出事件,然后我们的事件循环将拾取这些事件并调用与每个事件关联的回调函数,通常是说事件循环执行业务流程,这仅意味着它接收事件,调用其回调函数并将更昂贵的任务卸载到线程池中。 现在,所有这些实际上如何在后台进行?这些回调以什么顺序执行?
好吧,当我们启动节点应用程序时,事件循环立即开始运行。事件循环具有多个阶段,每个阶段都有一个回调队列,其中四个最重要的阶段是1.过期的计时器回调,2.I / O轮询和回调3. setImmediate回调和4.关闭回调。 Node在内部使用其他阶段。
因此,第一阶段负责处理过期计时器的回调,例如从setTimeout()函数中进行回调。因此,如果计时器中有刚刚过期的回调函数,则它们是事件循环要处理的第一个函数。
**最重要的是,如果某个计时器在处理其他阶段之一的过程中稍后过期,那么该计时器的回调将仅在事件循环返回到此状态时被调用第一阶段。它在所有四个阶段都这样工作。**
因此,每个队列中的回调将被一个接一个地处理,直到队列中没有剩余的回调为止,然后事件循环才进入下一阶段。例如,假设有1000个setTimeOut回调计时器到期,并且事件循环处于第一阶段,那么所有这1000个setTimeOuts回调将一个接一个地执行,然后进入下一阶段(I / O池和回调)。>
接下来,我们有I / O池和I / O回调的执行。这里的I / O代表输入/输出,而轮询基本上意味着寻找准备好要处理的新I / O事件,并将主题放入回调队列中。
在Node应用程序的上下文中,I / O主要意味着诸如联网和文件访问之类的东西,因此在此阶段中,大概99%的通用应用程序代码都将被执行。
下一阶段是setImmediate回调,SetImmediate是一种特殊的计时器,如果我们想在I / O轮询和执行阶段之后立即处理回调,则可以使用它。
最后,第四个阶段是关闭回调,在此阶段,将处理所有关闭事件,例如,当服务器或WebSocket关闭时。
这是事件循环中的四个阶段,但是除了这四个回调队列之外,实际上还有另外两个队列, 1. nextTick()其他 2.微任务队列(主要用于已解决的诺言)
如果这两个队列之一中有任何要处理的回调,它们将在事件循环的当前阶段完成后立即执行,而不是等待整个循环/周期结束。
换句话说,在这四个阶段的每个阶段之后,如果这两个特殊队列中有任何回调,它们将立即执行。现在想象一下,当一个到期的计时器的回调正在运行时,一个promise会解析并从API调用返回一些数据。在这种情况下,该promise回调将在计时器结束后立即执行。
相同的逻辑也适用于nextTick()队列。当我们确实需要在当前事件循环阶段之后立即执行某个回调时,可以使用nextTick()函数。它与setImmediate有点相似,不同之处在于setImmediate仅在I / O回调阶段之后运行。
以上所有事情将在事件循环的一个周期内发生,与此同时,它们的新事件可能在特定阶段出现,或者旧事件可能过期,事件循环将使用另一个新事件处理这些事件循环。
因此,现在是时候确定循环是否应该继续到下一个刻度,或者是否应该退出程序。 Node只是简单地检查是否有计时器或I / O任务仍在后台运行,如果没有,则它将退出应用程序。但是,如果有任何待处理的计时器或I / O任务,则该节点将继续运行事件循环并开始下一个周期。
例如,在节点应用程序中,当我们侦听传入的HTTP请求时,我们基本上运行了一个无限I / O任务,该任务在事件循环中运行,因为Node.js可以继续运行并继续侦听新的HTTP请求进入而不仅仅是退出应用程序。
另外,当我们在后台写入或读取文件时,这也是一个I / O任务,并且在使用该文件时该应用程序不存在是有意义的吗?
现在实践中的事件循环:
const fs = require('fs');
setTimeout(()=>console.log('Timer 1 finished'), 0);
fs.readFile('test-file.txt', ()=>{
console.log('I/O finished');
});
setImmediate(()=>console.log('Immediate 1 finished'))
console.log('Hello from the top level code');
输出: 好吧,第一行是您好,来自顶层代码,是的,因为这是一个立即执行的代码,所以可以预料。然后,在我们获得三个输出之后,计时器1完成,因为我们之前说过的第一阶段,这行是预期的,但是在 I / O完成之后,应该打印此行,因为可以断定setImmediate在I / O回调阶段之后运行,但是此代码实际上不在I / O周期中,因此它不在事件循环内部运行,因为它不在任何回调函数内部运行。
现在让我们做另一个测试:
const fs = require('fs');
setTimeout(()=>console.log('Timer 1 finished'), 0);
setImmediate(()=>console.log('Immediate 1 finished'));
fs.readFile('test-file.txt', ()=>{
console.log('I/O finished');
setTimeout(()=>console.log('Timer 2 finished'), 0);
setImmediate(()=>console.log('Immediate 2 finished'));
setTimeout(()=>console.log('Timer 3 finished'), 0);
setImmediate(()=>console.log('Immediate 3 finished'));
});
console.log('Hello from the top level code')
输出符合预期,对吗?现在让我们添加一些延迟:
setTimeout(()=>console.log('Timer 1 finished'), 0);
setImmediate(()=>console.log('Immediate 1 finished'));
fs.readFile('test-file.txt', ()=>{
console.log('I/O finished');
setTimeout(()=>console.log('Timer 2 finished'), 3000);
setImmediate(()=>console.log('Immediate 2 finished'));
setTimeout(()=>console.log('Timer 3 finished'), 0);
setImmediate(()=>console.log('Immediate 3 finished'));
});
console.log('Hello from the top level code')
在I / O内部的第一个周期中,所有内容都执行了,但是由于第二个周期中的Dealy Timer-2在其代码中执行了
现在,让我们添加nextTick(),并查看nodejs的行为:
setTimeout(()=>console.log('Timer 1 finished'), 0);
setImmediate(()=>console.log('Immediate 1 finished'));
fs.readFile('test-file.txt', ()=>{
console.log('I/O finished');
setTimeout(()=>console.log('Timer 2 finished'), 3000);
setImmediate(()=>console.log('Immediate 2 finished'));
setTimeout(()=>console.log('Timer 3 finished'), 0);
setImmediate(()=>console.log('Immediate 3 finished'));
process.nextTick(()=>console.log('Process Next Tick'));
});
console.log('Hello from the top level code')
输出:
好吧,第一次回调是在process.NextTick()内部执行的,如预期的那样吗?由于nextTicks回调保留在微任务队列中,因此它们在每个阶段之后执行。
答案 4 :(得分:0)
如果您运行此简单的节点代码
console.log('starting')
setTimeout(()=>{
console.log('0sec')
}, 0)
setTimeout(()=>{
console.log('2sec')
}, 2000)
console.log('end')
您期望输出是什么? 如果是
starting
0sec
end
2sec
这是错误的猜测,我们会得到
starting
end
0sec
2sec
因为节点永远不会在退出main()之前在事件循环中打印代码
所以基本上,首先main()
将进入堆栈,然后console.log('starting ')
将首先打印出来,然后setTimeout(()=>{console.log('0sec')}, 0)
将进入堆栈,然后进入nodeAPI(节点使用多线程(用c ++编写的lib)执行setTimeout完成,即使上面的代码是单线程代码),时间到了,它会移到事件循环,除非堆栈不为空,否则节点无法打印它。因此,下一行,即setTimeout为2sec,将首先被压入堆栈,然后nodeAPI将等待2秒完成,然后进行偶数循环,这意味着将执行下一条代码行console.log('end' ),因此我们会在0秒之前看到结束消息,因为如果节点具有非阻塞性质。结束代码结束后,弹出main语句,并在第一个0秒之后执行将要执行的事件循环代码,之后将显示2毫秒msg。