Nodejs探秘:深⼊理解单线程实现⾼并发原理
前⾔
从Node.js进⼊我们的视野时,我们所知道的它就由这些关键字组成 事件驱动、⾮阻塞I/O、⾼效、轻量,它在官⽹中也是这么描述⾃⼰的。
Node.js® is a JavaScript runtime built on Chrome’s V8 JavaScript engine. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient.
于是在我们刚接触Nodejs时,会有所疑问:
1、为什么在浏览器中运⾏的Javascript能与操作系统进⾏如此底层的交互?
2、nodejs 真的是单线程吗?
3、如果是单线程,他是如何处理⾼并发请求的?
4、nodejs 事件驱动是如何实现的?
等等。。。
看到这些问题,是否有点头⼤,别急,带着这些问题我们来慢慢看这篇⽂章。
架构⼀览
上⾯的问题,都挺底层的,所以我们从 Node.js 本⾝⼊⼿,先来看看 Node.js 的结构。
Node.js 标准库,这部分是由 Javascript编写的,即我们使⽤过程中直接能调⽤的 API。在源码中的 lib ⽬录下可以看到。
Node bindings,这⼀层是 Javascript与底层 C/C++ 能够沟通的关键,前者通过 bindings 调⽤后者,相互交换数据。实现在
这⼀层是⽀撑 Node.js 运⾏的关键,由 C/C++ 实现。
V8:Google 推出的 Javascript VM,也是 Node.js 为什么使⽤的是 Javascript的关键,它为 Javascript提供了在⾮浏览器端运⾏的环境,它的⾼效是 Node.js 之所以⾼效的原因之⼀。
Libuv:它为 Node.js 提供了,线程池,事件池,异步 I/O 等能⼒,是 Node.js 如此强⼤的关键。
C-ares:提供了异步处理 DNS 相关的能⼒。
http_parser、、zlib 等:提供包括 http 解析、SSL、数据压缩等其他的能⼒。
与操作系统交互
举个简单的例⼦,我们想要打开⼀个⽂件,并进⾏⼀些操作,可以写下⾯这样⼀段代码:
var fs = require('fs');
fs.open('./', "w", function(err, fd) { //..do something});
这段代码的调⽤过程⼤致可描述为:lib/fs.js → src/ → uv_fs
lib/fs.js
async function open(path, flags, mode) { mode = modeNum(mode, 0o666); path = getPathFromURL(path); validatePath(path);
validateUint32(mode, 'mode');
return new FileHandle(
await binding.NamespacedPath(path),
stringToFlags(flags), mode, kUsePromises));
}
src/
nodejs工作流引擎开源
static void Open(const FunctionCallbackInfo& args)
{ Environment* env = Environment::GetCurrent(args); const int argc = args.Length();
if (req_wrap_async != nullptr) { // open(path, flags, mode, req)
AsyncCall(env, req_wrap_async, args, "open", UTF8, AfterInteger,
uv_fs_open, *path, flags, mode);
} else { // open(path, flags, mode, undefined, ctx) CHECK_EQ(argc, 5);
FSReqWrapSync req_wrap_sync; FS_SYNC_TRACE_BEGIN(open);
int result = SyncCall(env, args[4], &req_wrap_sync, "open",
uv_fs_open, *path, flags, mode);
FS_SYNC_TRACE_END(open);
args.GetReturnValue().Set(result);
}
}
uv_fs
/* Open the destination file. */
dstfd = uv_fs_open(NULL, &fs_req,
req->new_path,
dst_flags,
statsbuf.st_mode, NULL);
uv_fs_req_cleanup(&fs_req);
Node.js 上的⼀幅图:
具体来说,当我们调⽤ fs.open 时,Node.js 通过 process.binding 调⽤ C/C++ 层⾯的 Open 函数,然后通过它调⽤ Libuv 中的具体⽅法 uv_fs_open,最后执⾏的结果通过回调的⽅式传回,完成流程。
我们在 Javascript中调⽤的⽅法,最终都会通过 process.binding 传递到 C/C++ 层⾯,最终由他们来执⾏真正的操作。Node.js 即这样与操作系统进⾏互动。
单线程
在传统web 服务模型中,⼤多都使⽤多线程来解决并发的问题,因为I/O 是阻塞的,单线程就意味着⽤户要等待,显然这是不合理的,所以创建多个线程来响应⽤户的请求。
Node.js 对http 服务的模型:
Node.js的单线程指的是主线程是“单线程”,由主要线程去按照编码顺序⼀步步执⾏程序代码,假如遇到同步代码阻塞,主线程被占⽤,后续的程序代码执⾏就会被卡住。实践⼀个测试代码:
var http = require('http');function sleep(time) {
var _exit = w() + time * 1000; while( w() < _exit ) {} return ;
}var server = ateServer(function(req, res){
sleep(10);
});
server.listen(8080);
下⾯为代码块的堆栈图:
先将index.js的代码改成这样,然后打开浏览器,你会发现浏览器在10秒之后才做出反应,打出Hello Node.js。
JavaScript是解析性语⾔,代码按照编码顺序⼀⾏⼀⾏被压进stack⾥⾯执⾏,执⾏完成后移除然后继续压下⼀⾏代码块进去执⾏。上⾯代码块的堆栈图,当主线程接受了request后,程序被压进同步执⾏的sleep执⾏块(我们假设这⾥就是程序的业务处理),如果在这10s内有第⼆个request进来就会被压进stack⾥⾯等待10s执⾏完成后再进⼀步处理下⼀个请求,后⾯的请求都会被挂起等待前⾯的同步执⾏完成后再执⾏。
那么我们会疑问:为什么⼀个单线程的效率可以这么⾼,同时处理数万级的并发⽽不会造成阻塞呢?就是我们下⾯所说的--------事件驱动。事件驱动/事件循环
Event Loop is a programming construct that waits for and dispatches events or messages in a program.
1、每个Node.js进程只有⼀个主线程在执⾏程序代码,形成⼀个执⾏栈(execution context stack)。
2、主线程之外,还维护了⼀个"事件队列"(Event queue)。当⽤户的⽹络请求或者其它的异步操作到来时,node都会把它放到Event Queue之中,此时并不会⽴即执⾏它,代码也不会被阻塞,继续往下⾛,直到主线程代码执⾏完毕。
3、主线程代码执⾏完毕完成后,然后通过Event Loop,也就是事件循环机制,开始到Event Queue的开头取出第⼀个事件,从线程池中分配⼀个线程去执⾏这个事件,接下来继续取出第⼆个事件,再从线程池中分配⼀个线程去执⾏,然后第三个,第四个。主线程不断的检查事件队列中是否有未执⾏的事件,直到事件队列中所有事件都执⾏完了,此后每当有新的事件加⼊到事件队列中,都会通知主线程按顺序取出交EventLoop处理。当有事件执⾏完毕后,会通知主线程,主线程执⾏回调,线程归还给线程池。
4、主线程不断重复上⾯的第三步。
总结:
我们所看到的node.js单线程只是⼀个js主线程,本质上的异步操作还是由线程池完成的,node将所有的阻塞操作都交给了内部的线程池去实现,本⾝只负责不断的往返调度,并没有进⾏真正的I/O操作,从⽽实现异步⾮阻塞I/O,这便是node单线程和事件驱动的精髓之处了。
Node.js 中的事件循环**的实现:**
Node.js采⽤V8作为js的解析引擎,⽽I/O处理⽅⾯使⽤了⾃⼰设计的libuv,libuv是⼀个基于事件驱动的跨平台抽象层,封装了不同操作系统⼀些底层特性,对外提供统⼀的API,事件循环机制也是它⾥⾯的实现。 在中:
Environment* CreateEnvironment(IsolateData* isolate_data,
Local context,
int argc, const char* const* argv,int exec_argc,const char* const* exec_argv)
{
Isolate* isolate = context->GetIsolate(); HandleScope handle_scope(isolate);
Context::Scope context_scope(context); auto env = new Environment(isolate_data,
context,
v8_platform.GetTracingAgent());
env->Start(argc, argv, exec_argc, exec_argv, v8_is_profiling); return env;
}
这段代码建⽴了⼀个node执⾏环境,可以看到第三⾏的uv_default_loop(),这是libuv库中的⼀个函数,它会初始化uv库本⾝以及其中的default_loop_struct,并返回⼀个指向它的指针default_loop_ptr。 之后,Node会载⼊执⾏环境并完成⼀些设置操作,然后启动event loop:
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论