nodejs中实现sleep功能实例
nodejs最让⼈不爽的就是其单线程特性,很多事情没法做,对CPU密集型的场景,性能也不够强劲。很长⼀段时间,我想在javascript语⾔框架下寻求⼀些解决⽅案,解决⽆法操作线程、性能差的问题。曾经最让我印象深刻的⽅案是,不过fibers也好,其他⽅案也好,在线程操作上还是很别扭,太过依赖辅助线程,本末倒置;就fiber⽽⾔,javascript固有的低性能问题并不能解决;最别扭的是在javascript语⾔框架下,线程间的消息传递常常很受限制,经常⽆法真正地共享对象。
nodejs的addon⽅式⽆疑是极好的,具有极强的灵活性、完备的功能和原⽣代码的性能。简单说就是让nodejs直接调⽤
c/c++模块,是⼀种javascript和native的混合开发模式。好东西呀,为什么不⽤呢?addon应该算是⼀个⼤话题,今天我也不想太深⼊说这个,我⾃⼰的实践也不是很多。那就实现⼀个sleep函数,就当是抛砖引⽟吧。
sleep
为什么javascript实现不了真正的sleep?sleep⽅法是通过向操作系统内核注册⼀个信号,指定时间后发送唤醒信号,⽽线程本⾝则挂起。本质上当线程 sleep(1000) 代表告诉操作系统:1000ms内不要给我分配C
PU时间。所以sleep能保证线程挂起时不再占⽤CPU资源。⽽javascript是单线程运⾏,本⾝取消了线程的概念,⾃然没有办法将主线程挂起中断。
也有⼈会尝试⽤javascript⽅法要实现sleep,例如这样:
复制代码代码如下:
function sleep(sleepTime) {
for(var start = +new Date; +new Date - start <= sleepTime; ) { }
}
这是采⽤空循环阻塞住主进程的运⾏来实现sleep,明显跟真正的sleep相去甚远。
那么如果实现⼀个真正的sleep呢?
环境准备
开发环境
之前我的⼀些博客已经说过,这⾥从略:node.js+npm、python 2.7、visual studio/ x-code。
编译⼯具
编译⼯具需要采⽤node-gyp,较新版本的nodejs⾃带此库,如果没有⾃带node-gyp,请执⾏:
复制代码代码如下:
npm install -g node-gyp
gyp特性我没有精⼒去研究,如果你⽐较熟悉gcc等其他编译器,不排除gyp会有不兼容之处,⽽且编译选项和开关也是不尽相同。建议针对nodejs重新编写c++代码,如果确实有模块需要复⽤,可以考虑先⽤熟悉的gcc编译成动态链接库,再编写少量代码来使⽤动态链接库,再把这部分代码⽤gyp编译出来供nodejs使⽤。
进⼊项⽬⽂件夹,执⾏ npm init 初始化项⽬。为了让nodejs知道我们想制作addon,我们需要在package.json中添加:
复制代码代码如下:
"gyp-file": true
如果使⽤过gcc,那么你⼀定记得makefile。类似的,gyp也是通过⼀个⽂件来描述编译配置,这个⽂件为p,它是⼀个我们⾮常熟悉的json⽂件。gyp不是我们探讨的重点,所以p也不会深⼊探究,我们只关注最重要的⼀些配置项。以下是⼀份简单但完整的p⽂件⽰例:
复制代码代码如下:
{
"targets": [
{
"target_name": "hello",
"sources": [ "" ],
"include_dirs": [
"<!(node -e \"require('nan')\")"
]
}
]
}
就看看这⾥⾯涉及的三个配置项:
1.target_name:表⽰输出出来的模块名。
2.sources:表⽰需要编译的源代码路径,这是⼀个数组。
3.include_dirs:表⽰编译过程中要⽤到的⽬录,这些⽬录中的头⽂件可以在预编译指令 #include 搜索到。在这⾥使⽤了⼀个⽐较特殊的写法,没有把路径⽤字符串常量给出,⽽是运⾏⼀个命令 node -e "require('nan')" ,nan库后⾯再说,先看看这个命令输出什么: node_modules\nan ,原来这句命令的意思是返回nan库的路径。
C++编码
OK,既然已经配置了源代码是,那就建⽴⼀个这样的⽂件。有⼀个问题需要提前提醒⼤家,我
们所写的c++模块最终是要被v8引擎使⽤,所以api、写法等受到v8引擎的制约。⽽不同版本的nodejs其实采⽤的v8引擎的版本也不尽相同,这也就意味着很难⽤⼀套c++代码满⾜不同版本的nodejs(指编译过程,编译完成后跨版本应该能够使⽤,没有验证过。github不能上传⼆进制类库,所以github上开源会有⿇烦。npm可以直接上传⼆进制类库,跳过编译步骤,所以问题相对较⼩)。
node 0.11及以上版本:
复制代码代码如下:
#include <node.h>
#include <v8.h>
using namespace v8;
void SleepFunc(const v8::FunctionCallbackInfo<Value>& args) {
Isolate* isolate = Isolate::GetCurrent();
HandleScope scope(isolate);
double arg0 = args[0] -> NumberValue();
Sleep(arg0);
}
void Init(Handle<Object> exports) {
Isolate* isolate = Isolate::GetCurrent();
exports->Set(String::NewFromUtf8(isolate, "sleep"),
FunctionTemplate::New(isolate, SleepFunc)->GetFunction());
}
NODE_MODULE(hello, Init);
node 0.10及以下版本:javascript免费教程
复制代码代码如下:
#include <node.h>
#include <v8.h>
using namespace v8;
Handle<Value> SleepFun(const Arguments& args) {
HandleScope scope;
double arg0 = args[0] -> NumberValue();
Sleep(arg0);
return scope.Close(Undefined());
}
void Init(Handle<Object> exports) {
exports->Set(String::NewSymbol("sleep"),
FunctionTemplate::New(SleepFun)->GetFunction());
}
NODE_MODULE(hello, Init);
可以看出,变化还是相当⼤的,如果能屏蔽这些差异就太好了,有办法了?我写这么多还不就是想告诉你有办法。是时候请出nan库了。
nan
还记得在p中,我们引⼊nan库的路径,就是要在这⾥⽤。nan库是⼲嘛的呢?它提供了⼀层抽象,屏蔽了nodejs 0.8、nodejs 0.10、nodejs 0.12、io.js之前addon的语法差异。赞!
先安装: npm install --save nan ,看看同样的功能,⽤了nan后如何实现:
复制代码代码如下:
#include <nan.h>
using namespace v8;
NAN_METHOD(Sleep){
NanScope();
double arg0=args[0]->NumberValue();
Sleep(arg0);
NanReturnUndefined();
}
void Init(Handle<Object> exports){
exports->Set(NanSymbol("sleep"), FunctionTemplate::New(Sleep)->GetFunction());
}
NODE_MODULE(hello, Init);
你需要了解的就是nan这套东西,⾄于v8的那⼀套就可以不⽤关注。
从下往上看:
复制代码代码如下:
NODE_MODULE(hello, Init);
这句定义addon的⼊⼝。注意第⼀个参数要与我们在p中target_name⼀项⼀致。第⼆个参数就是addon的⼊⼝函数。
复制代码代码如下:
void Init(Handle<Object> exports){
exports->Set(NanSymbol("sleep"), FunctionTemplate::New(Sleep)->GetFunction());
}
这段代码就是addon的⼊⼝⽅法。它接收两个参数,分别是exports和module。上⾯的⽰例省略了第⼆个参数。如果模块提供⼀个对象,可以像⽰例中那个,直接给exports指定要提供的key-value;如果特殊⼀点,仅提供⼀个数值,或⼀个函数,则需要⽤到第⼆个参数,类似于 NODE_SET_METHOD(module, "exports", foo); 。这个⽰例中是表⽰要输出这样⼀个模块:
复制代码代码如下:
{
"sleep": Sleep
}
Sleep是⼀个函数,下来就来看看Sleep的定义:
复制代码代码如下:
NAN_METHOD(Sleep){
NanScope();
double arg0=args[0]->NumberValue();
Sleep(arg0);
NanReturnUndefined();
}
其实就是读取javascript传⼊的参数,转成double型,再调⽤c++的sleep⽅法。
编译addon
下⾯就要开始编译这个模块了。⾸先执⾏ node-gyp configure 来进⾏构建前准备⼯作,它会⽣成⼀个build⽂件夹和⼀些⽂件。接下来运⾏ node-gyp build 就可以开始编译了。在这个⽰例中,最终会在/build/Release/⽬录下⽣成⼀个de⽂件,这就是最终能被javascript引⽤的addon模块了。
如果后续对c++代码有修改,就不⽤再运⾏ node-gyp configure ,直接运⾏ node-gyp build 就好。
nodejs使⽤
建⽴⼀个index.js,看看怎么⽤这个模块吧:
复制代码代码如下:
var sleep=require('./build/de').sleep;
console.log(new Date);
sleep(1000);
console.log(new Date);
// result
// Wed Mar 04 2015 14:55:18 GMT+0800 (中国标准时间)
// Wed Mar 04 2015 14:55:19 GMT+0800 (中国标准时间)
很容易吧,跟普通的javascript函数的使⽤⽅式⼀模⼀样。
⾄此本⽂想要分享的技术要点已经阐述完了。不过……究竟跟开篇提供的⽅法⽐起来有什么不⼀样?我不截图了,直接说明结果:
由于addon⽅式采⽤的⽅法是线程挂起,理论上不会有CPU占⽤和内存变化,结果也是验证了这⼀点。再看javascript循环模拟sleep的⽅式,因为⼀直在跑循环,内存增加⼀点可以理解,没什么⼤不了;再看CPU占⽤25%,似乎还算过得去。真的是这样吗?揭露真相的时候到了。我测试的笔记本电脑的CPU是双核四线程,再结合25%的CPU占⽤……难道双核四线程中有⼀个线程就被这个sleep给占⽤了?其实我发现这期间并没有⼀个线程被锁死,不过这不是javascript的功劳,⽽是intel超线程的功劳。因为说
是四线程,其实本质是两个处理核⼼只能是双线程,只是cpu做了⼀个时间⽚切割上的⼩把戏。例如核⼼
cpu01分成了t0和t2,假设在n tick(调度周期)后的⼀个tick内,任务会分到t0,那么在再后⾯⼀个tick,任务会分到t2。所以从⼀个⽐较长的时间尺度(相对于调度周期),⼀个任务在t0和t2上运⾏的时间基本是相当的。于是呈现出来的情景是nodejs 的进程没有占⽤t0或t2到100%,⽽是分别占⽤了50%上下。由于windows的进程调度相对⽐较复杂,所以CPU占⽤量上下浮动很⼤。可以这样预测,如果是双核双线程的CPU来处理这个脚本,CPU占⽤会上升到50%,并且⼀个核⼼卡死。如果是单核CPU来处理,CPU⼀下⼦会上升到100%。
好像CPU这段说得有点多,超线程那些也是猜测,各位看看就好。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论