理解Node.js的event loop
理解Node.js的event loop
翻译的一片文章,原文在这里(ps:这个博客我推荐看下,里面还有博主自己写的node.js电子书,质量很高),很基础的东西,个人感觉理解了这个,对以后node开发帮助很大,也不会去做用js手写异步函数的傻事了。但是直接读英文觉得没什么,翻成中文有些地方真觉得罗嗦。。。翻译有问题的地方,欢迎在下面或者我的博客里指出。
正文如下:
关于Node.js的第一个基本概念是I/O操作开销是巨大的:
所以,当前变成技术中最大的浪费来自于等待I/O操作的完成。有几种方法可以解决性能的影响:
- 同步方式:按次序一个一个的处理请求。利:简单;弊:任何一个请求都可以阻塞其他所有请求。
- 开启新进程:每个请求都开启一个新进程。利:简单;弊:大量的链接意味着大量的进程。
- 开启新线程:每个请求都开启一个新线程。利:简单,而且跟进程比,对系统内核更加友好,因为线程比进程轻的多;弊:不是所有的机器都支持线程,而且对于要处理共享资源的情况,多线程编程会很快变得太过于复杂。
第二个基本概念是每个连接都创建一个新线程是很消耗内存的(例如:你可以对比Nginx回想一下Apache内存耗尽的情景)。
Apache是多线程的:它为每个请求开启一个新的线程(或者是进程,这取决于你的配置),当并发连接增多时,你可以看看它是怎么一点一点耗尽内存的。Nginx和Node.js不是多线程的,因为线程的消耗太“重”了。它们两个是单线程、基于事件的,这就把处理众多连接所产生的线程/进程消耗给消除了。
Node.js中你的代码运行在单线程之中
确实只有一个线程:你不能并行执行任何代码,比如:下面的“sleep”将会阻塞sever1秒钟:
while(new Date().getTime() < now + 1000) {
// do nothing
}
当这段代码运行时,Node.js不会响应客户端任何请求,因为只有一个线程来运行你的代码,另外,如果你执行cpu密集的任务,比如重设图像的大小,它也会阻塞所有请求。
……然而,除了你的代码,其它的一切都是并行执行的
单线程没法让代码并行执行。但是所有I/O操作都是事件驱动、并行的,所以下面的代码不会阻塞server:
c.query(
'SELECT SLEEP(20);',
function (err, results, fields) {
if (err) {
throw err;
}
res.writeHead(200, {'Content-Type': 'text/html'});
res.end('<html><head><title>Hello</title></head><body><h1>Return from async DB query</h1></body></html>');
c.end();
}
);
如果你在一次请求中执行这些,当数据库sleep时,其他请求也会立即被处理。
为什么异步比较好?什么时候我们应该从同步转移到异步/并行执行呢?
同步执行也不错,因为它简便了我们敲代码。但在使用异步时,你不必关心后端是怎么处理的。而且,在I/O操作时不会阻止其他请求,同时无需承担每个请求所产生的线程/进程的成本。
I/O操作时使用异步处理很好,因为I/O操作的成本比单纯执行代码要高的多,我们应该在等待I/O时做其它更有意义的工作。
Event loop是指处理外部事件,并把外部事件转换为回调来进行调用的实体(晦涩难懂!!原文:an entity that handles and processes external events and converts them into callback invocations).所以,I/O调用的同时,server就可以去处理另一个请求。在一次I/O调用中,你的代码保存回调函数并把控制权交回到node.js运行时。当数据加载完毕可以访问时,就可以执行回调函数了。
当然,在后端有很多数据库接入和处理的进程。但是,这些都不需要通过你的代码直接实现,你也就不必了解后台I/O之间的相互作用关系。和Apache相比,省去了很多线程消耗,因为不是每个链接都需要新线程,仅那些需要并行运行的才需要新线程。
不只是I/O调用,Node.js期望所有的请求都能快速的响应,比如CPU密集的工作应该分离到其他进程,你可以使用事件和他交互。
内部实现
在内部,Node.js依赖libev来提供event loop,使用线程池来提供异步I/O。
理解Node.js的Event Loop
Node.js 是一个基于 Chrome V8 JavaScript 引擎的服务器端 JavaScript 运行环境,它的核心特性之一就是其非阻塞、事件驱动的特性。理解 Node.js 的 Event Loop 对于编写高效的 Node.js 应用程序至关重要。
基本概念
首先,我们要认识到 I/O 操作的开销是巨大的。在传统的编程模型中,大量的时间都花在了等待 I/O 操作完成上。这会导致应用程序的性能下降。为了应对这个问题,Node.js 采用了事件驱动、异步的方式来处理 I/O 操作。
同步 vs 异步
让我们先来看一下同步和异步的概念:
- 同步方式:这种方式按顺序处理请求。优点是简单,但缺点是任何一个请求都可以阻塞其他所有请求。
- 异步方式:这种方式允许同时处理多个请求。优点是高效,但缺点是代码编写稍微复杂一些。
单线程与 Event Loop
Node.js 中的代码运行在一个单线程中。这意味着,任何 CPU 密集型任务都会阻塞整个应用。例如:
// 阻塞服务器1秒
while (new Date().getTime() < new Date().getTime() + 1000) {
// do nothing
}
上述代码会阻塞服务器,导致其他请求无法及时响应。
然而,Node.js 的神奇之处在于,除了你的代码之外,所有 I/O 操作都是并行的。例如:
c.query(
'SELECT SLEEP(20);',
function (err, results, fields) {
if (err) {
throw err;
}
res.writeHead(200, {'Content-Type': 'text/html'});
res.end('<html><head><title>Hello</title></head><body><h1>Return from async DB query</h1></body></html>');
c.end();
}
);
在这个例子中,尽管数据库查询可能需要花费20秒的时间,但在这段时间内,服务器仍然可以处理其他请求。这是因为 Node.js 使用了一个称为 Event Loop 的机制来管理异步操作。
Event Loop 解析
Event Loop 是 Node.js 处理外部事件并将其转换为回调函数调用的核心机制。当你发起一个 I/O 请求时,Node.js 会保存回调函数并将控制权交还给运行时环境。一旦 I/O 操作完成,Node.js 会自动调用相应的回调函数。
内部实现
在内部,Node.js 依赖于 libev 来提供 event loop,使用线程池来处理异步 I/O 操作。这意味着,虽然 Node.js 本身是单线程的,但它可以有效地利用底层的多线程机制来提高性能。
总结
理解 Node.js 的 Event Loop 是掌握 Node.js 编程的关键。通过使用异步编程模式,Node.js 能够高效地处理大量并发连接,而不需要为每个连接创建新的线程或进程。这使得 Node.js 成为构建高性能网络应用的理想选择。
说的很好~
“Apache是多线程的:它为每个请求开启一个新的线程(或者是进程,这取决于你的配置),当并发连接增多时,你可以看看它是怎么一点一点耗尽内存的。Nginx和Node.js不是多线程的,因为线程的消耗太“重”了。”
请问这句话怎么理解,难道node是一个线程处理多个请求? 比如有三个人同时访问一个node站点,这三个请求是一个线程处理还是三个线程处理?
var responseHelper = { /** * [@param](/user/param) res restify response obj * [@param](/user/param) code http code * [@param](/user/param) message 消息 * [@constructor](/user/constructor) */ ManualError:function(res,code,message){ res.send(code, { code:code, message:message }); } };
三个请求的真正处理操作不在这个node线程中,这个线程运行的是event loop,把请求分发到worker 线程,worker线程应该是用c++实现的,完了后再通过callback把结果返回给客户端。
理解Node.js的event loop
Node.js 的核心概念之一就是事件循环(event loop),这是其非阻塞 I/O 模型的基础。事件循环使得 Node.js 能够高效地处理大量并发连接,而不需要为每个连接创建新的线程或进程。
单线程与事件驱动
尽管 Node.js 的用户代码是在一个单线程中运行,但这并不意味着它是阻塞的。实际上,除了你的用户代码外,Node.js 的其他部分都是并行执行的。具体来说,所有的 I/O 操作都是事件驱动的,这意味着在执行 I/O 操作时,Node.js 不会阻塞整个进程。
示例代码
让我们来看一个简单的例子:
const http = require('http');
const server = http.createServer((req, res) => {
// 模拟一个长时间的 I/O 操作,例如读取数据库
setTimeout(() => {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World\n');
}, 2000); // 延迟 2 秒
});
server.listen(3000, () => {
console.log('Server running at http://localhost:3000/');
});
在这个例子中,当服务器接收到请求时,它会启动一个定时器,模拟一个长时间的 I/O 操作。但即使这个定时器在运行,Node.js 仍然可以继续处理其他请求。因此,其他请求不会被阻塞。
为什么异步更好?
异步编程的好处在于,当你等待 I/O 操作完成时,Node.js 可以去做其他事情。这样,服务器可以更高效地利用资源,从而处理更多的并发请求。
事件循环的内部工作原理
Node.js 使用 libuv
库来管理事件循环和异步 I/O 操作。事件循环是一个无限循环,它不断地检查是否有新的事件需要处理。如果有,它会执行相应的回调函数。
在 Node.js 中,事件循环大致分为以下几个阶段:
- Timers: 处理
setTimeout
和setInterval
回调。 - I/O Callbacks: 处理几乎所有的回调,除了
close
事件和定时器回调。 - Idle, Prepare: 仅用于内部操作。
- Poll: 获取新的 I/O 事件,尽可能处理 I/O 事件的回调。
- Check: 处理
setImmediate()
的回调。 - Close Callbacks: 处理
socket.on('close', ...)
的回调。
结论
理解 Node.js 的事件循环是掌握其编程模型的关键。通过充分利用事件循环,开发者可以编写出高效、非阻塞的代码,从而更好地利用资源并处理大量并发连接。