Node.js非阻塞实践:从同步阻塞到子进程异步处理

作者:接口漫谈时间:2026-10-26 19:00:53浏览:114

Node.js基于单线程事件循环架构,JavaScript层的同步计算会阻塞后续请求处理。本文通过示例演示同步sleep导致的阻塞现象,分析formidable库利用事件驱动实现非阻塞的原理,并重点介绍使用child_process.fork创建独立子进程来执行耗时计算的方法,确保主线程事件轮询不被阻塞,实现高并发响应。

Node.js基础服务器构建与阻塞问题

首先,我们使用Node.js构建一个基础服务器。


index.js


代码如下:

 
var requestHandler = require("./requestHandler");
var server = require("./server");
var route = {
"/hello": requestHandler.hello,
"/upload": requestHandler.upload
}; 
server.start(route);

server.js


代码如下:

 

server.js


代码如下:

 
var http = require("http");
var url = require("url");
exports.start = function(route) {
var server = http.createServer(function(req, res) {
var pathName = url.parse(req.url).pathname;
var handler = route[pathName];
if (handler) {
console.log("Through path:" + pathName + ":" + new Date().getTime());
handler(res);
} else {
res.writeHead(404, {"Content-Type": "text/plain"});
res.end();
} 
}); 
server.listen(8088);
}; 

requestHandler.js


代码如下:

 
exports.hello = function(res) {
res.writeHead(200, {"Content-Type": "text/plain"});
res.write("say hello.");
res.end();
}; 
exports.upload = function(res) {
res.writeHead(200, {"Content-Type": "text/plain"});
res.write("upload");
res.end();
}; 

在命令行中,输入node index.js即可启动服务。


然而,上述代码是同步阻塞的。如果在createServer的回调函数中包含耗时较长的计算,将会阻塞Node.js的事件轮询机制。


Node.js的高效性,核心在于能够快速返回事件循环。


我们对requestHandler.js进行如下改造:在此示例中,由于事件循环持续被sleep函数占用,导致createServer的回调无法及时返回。


代码如下:

 
function sleep(milliSecond) {
var startTime = new Date().getTime();
console.log(startTime);
while(new Date().getTime() <= milliSecond + startTime) {
} 
console.log(new Date().getTime());
} 
exports.hello = function(res) {
sleep(20000);
res.writeHead(200, {"Content-Type": "text/plain"});
res.write("say hello.");
res.end();
}; 
exports.upload = function(res) {
res.writeHead(200, {"Content-Type": "text/plain"});
res.write("upload");
res.end();
}; 

此时,先访问http://localhost:8088/hello,再访问http://localhost:8088/upload。你会发现,尽管upload请求处理时间很短,却必须等待hello请求完成后才能响应。


基于事件驱动的非阻塞实现分析

我们尝试寻找异步调用的方法。例如formidable库中的文件上传功能,经测试是非阻塞的。查看formidable源码,发现关键代码如下:


代码如下:

 
IncomingForm.prototype.parse = function(req, cb) {
this.pause = function() {
try {
req.pause();
} catch (err) {
// the stream was destroyed
if (!this.ended) {
// before it was completed, crash & burn
this._error(err);
} 
return false;
} 
return true;
}; 
this.resume = function() {
try {
req.resume();
} catch (err) {
// the stream was destroyed
if (!this.ended) {
// before it was completed, crash & burn
this._error(err);
} 
return false;
} 
return true;
}; 
this.writeHeaders(req.headers);
var self = this;
req
.on('error', function(err) {
self._error(err);
}) 
.on('aborted', function() {
self.emit('aborted');
}) 
.on('data', function(buffer) {
self.write(buffer);
}) 
.on('end', function() {
if (self.error) {
return;
} 
var err = self._parser.end();
if (err) {
self._error(err);
} 
}); 
if (cb) {
var fields = {}, files = {};
this
.on('field', function(name, value) {
fields[name] = value;
}) 
.on('file', function(name, file) {
files[name] = file;
}) 
.on('error', function(err) {
cb(err, fields, files);
}) 
.on('end', function() {
cb(null, fields, files);
}); 
} 
return this;
}; 

在parse方法中,解析头部信息是同步阻塞的,但实际的文件上传操作在req.on('data')中通过事件驱动完成,这是非阻塞的。也就是说,其非阻塞模型依赖于整个Node.js的事件分发架构。


利用子进程处理耗时计算

那么,如果遇到像sleep那样消耗大量计算资源,且无法依赖Node.js事件分发架构的情况,该如何处理?


现在介绍一种类似HTML5 Web Worker的方法。


将requestHandler.js改造如下:


代码如下:

 
var childProcess = require("child_process");
exports.hello = function(res) {
var n = childProcess.fork(__dirname + "/subProcess.js");
n.on('message', function() {
res.writeHead(200, {"Content-Type": "text/plain"});
res.write("say hello.");
res.end();
}); 
n.send({});
}; 
exports.upload = function(res) {
res.writeHead(200, {"Content-Type": "text/plain"});
res.write("upload");
res.end();
}; 

并添加subProcess.js文件


代码如下:

 
function sleep(milliSecond) {
var startTime = new Date().getTime();
console.log(startTime);
while(new Date().getTime() <= milliSecond + startTime) {
} 
console.log(new Date().getTime());
} 
process.on('message', function() {
sleep(20000);
process.send({});
}); 

测试结果显示,当hello请求仍在等待时,upload请求已经返回。


Node.js异步架构深层解析

结语:


最近阅读博客园上的许多Node.js文章,普遍认为Node.js是异步的。但对于异步的具体程度,鲜有文章准确阐述。


实际上,Node.js采用双层架构:C++与JavaScript,且为单线程。这一点至关重要。Node.js通过C++调用V8引擎执行JS命令,为了维护调用顺序,维护了一个事件序列。因此,在一个JS函数内部,其调用绝对会阻塞其他函数的执行。所以,网上提到的process.nextTick和setTimeout等方法,并不能创建新线程以避免阻塞。它们实现的仅是事件序列中元素的顺序问题。相对于setTimeout,process.nextTick的实现更为简单,直接将回调加入事件序列的最顶层(某个特定事件)。而setTimeout则是增加了一个C++线程,在指定时间将回调加入事件序列。


以Node的文件IO为例,readFile等函数的第二个参数是一个回调。Node.js首先记录回调,然后调用底层C++,启动C++过程,这可以视为异步操作,因为此时已进入C++层面而非JS层面。因此,从exec到回调执行是异步的,从http.createServer到触发回调也是异步的。还有许多类似场景,例如MySQL调用方法,若查看源码,它是通过Socket发送命令,相信这个过程速度非常快。然后等待回调的过程由Node.js的C++部分隐藏处理,这也是异步的。


本文旨在说明,如果在JS端存在耗时运算,应使用前述方法,通过JS启动C++线程,即subprocess.js。该子进程不在Node的事件序列内部维护,而是独立线程,因此不会阻塞其他JS函数。

作者最新文章