旋转华尔兹,nodejs的背后

在使用nodejs的时候,会感觉写的很痛快,尤其是对于处理高并发的请求时,密集的IO操作,我们可以在代码中用回调链轻松的完成,只需要一个单进程,单线程就能完成频繁请求的分发调度。

js能做到这一步让我吃惊,于是决定打造一套属于我的node运行内核,完成对于回调链的驱动,也顺便感受一下nodejs的设计之美,代码的第一个版本在我的github https://github.com/linuxb/mynodeker 上,欢迎commit~

首先,既然说是单线程地处理高并发的请求,肯定是把这些耗时的IO任务给别的线程分发去了,表面上是一个进程一个线程,实际在底层可没这么老实,这种多任务的分发我可以考虑使用一个线程池实现,线程池能很好的做到线程的回收复用而且能优化线程的调度,对于这种消耗极大的回调链来说是比较实用的,来一个IO任务我就给他分配一个工作线程,完了再通过线程间的通信来通知主线程我完事啦,你可以处理我的回调方法啦,如果是promise,一般都是指js中then()链中的回调参数里的方法,这一点也类似于android系统中ipc的设计,在client与server的通信中,同样需要一个线程池进行维护,首先是获取service的代理,在通过线程池调度的线程完成系统内核的memory share读写。

除了线程池,还需要什么呢,当然是一个事件驱动了,事件驱动我采用libuv完成,这个威力强大的家伙提供了一套基于事件循环的IO啊,tcp啊,甚至很好的完成了ipc,线程间通信的问题,怪不得nodejs会选择它,libuv是一个C库,在我的C++程序中只需要调用它的函数即可,虽然C风格还是挺浓重的,这个库还不够,我还需要在这个库中做点什么,需要一个invokelooper,即一个循环执行队列,队列每一次循环前都会检查自己是否为空,如果里面有任务,一般都为wrappedcallback形式的经过我封装过的回调函数指针,就按照fifo执行,如果什么也没有,那就得主动寻找活来干了,哪寻找呢,当然是我们的IOTask,轮询一下被各个线程执行的他们有没有哪一位完事了,如果没有,那就sleep一会继续轮询咯,cpu也不能操劳过度啊,碰巧线程A中IOTask结束了,A在自己被池回收前敬业地给looper主线程发了一条message,looper马上找到对应的IOTask,并把它的cb(回调的函数指针)push到looper中,现在队列又不为空了,就可以乖乖的干活了。

要完成一种这样的操作,我们必须有一个结构来储存所有的IOtask,才能使looper接受到工作线程的消息是有地方可以查找,所以主要的类我们也就可以得出啦,下面看看UML的设计图:

这里写图片描述

可以看到ProcessState就是一个专门负责线程调度的类,里面聚合当前线程的一些信息,负责线程的创建,执行,与主线程的通信,以及最后对于池的回收,当然还要维护一套放着异步IOtask的结构,还有就是我们InvokeLooper类了,主要是维护一个执行队列,执行事件主循环,当然要跟js的代码发生交互,我还需要借助v8引擎提供的接口,这个东西通过js代码的解析,将js的对象都转化为C++对象,handler<>类型的智能指针(当然结构会有更多的字段),智能指针就有利于gc的回收,js中then链的函数对象当然也会转化为FunctionCallbackInfo<Value>& args 类型的C++引用,我们用这个引擎就可以捕捉到一个个从js层传递过来的task,在进行自己的封装成为可执行结构,可以称之为指令,这些指令就可在looper中顺序执行了,这有点像一个执行泵executePump,来看看我这些指令是怎么定义:

///node内核执行队列
typedef struct LinkNode
{
    //回调项
    void(*WrappedCallback)(void*);
    void* disc;
    void* argv;
    struct LinkNode* pNext;
} AscynInvokeQueueNode;

一个封装的回调函数指针,参数列表指针,就可以进行回调的指令执行了,当然还有一种复杂的情况没有讨论,这里只有IOtask列表传递的指令,js层传递过来的指令looper还不会处理,v8层接口对iotask进行封装后,由于looper处于循环中,所以processState还需要另外安排一个监听线程(watcher)进行来自V8事件的监听,为了归一化的处理,接收到监听的消息后,looper的执行循环需要进行一次操作节点的产生,看看这个操作节点的结构:

typedef struct node {

  oparg* arg1;
  oparg* arg2;
  op_handler_t* handler;
  OPCODE opcode;

} opnode;

跟zend引擎类似,这一个操作指令结构也由操作数以及操作码构成,目的是开放其他js操作的接口,建立操作函数表,通过操作码进行数组的索引,具体的机制可以参考我上一篇文章,这样来自js层的操作由这个结构进行统一的封装,具体什么操作呢,当然是将接受到的wrappedcallback push到事件轮询列表中,这个事件是有线程池触发的,在looper中的回调函数中执行,毕竟libuv提供了一整套线程通信的模型:

//notify to main thread
    uv_async_send(mpAsyncWatcher);

在两个需要通信的线程中注册同一个监听器mpAsyncWatcher 的实例就可以完成信息的交互,那么主线程接收到消息后,会如何啊,看看我的线程启动函数就知道了:

if(mpLoop == NULL)
        mpLoop = uv_default_loop();
    uv_async_init(mpLoop,mpAsyncWatcher,NotifyToV8);

uv_async_init函数就是完成接收消息回调触发的工作,NotifyToV8就是在looper上下文中的一个函数指针,当接收到watcher的消息后,回调马上执行,并且会插在所有来自IOtask的回调任务前面,拥有最高的优先级,它完了再按照原先的顺序执行队列中剩余的callback,其实也是考虑V8运行在另外一个进程时通过socket进行ipc这种方案,不过略麻烦,以后版本会考虑综合优化,其实就是watcher线程进行listen,接收到事件后也是同样的方式生成opnode,给looper执行。下面来看看数据结构:

这里写图片描述

基本的结构跟上面我说的一样,只是iotask的列表我使用了双向链表,主要是为了删除节点方便,删除还是一个比较频繁的操作,而且这个链表是动态变化的,在线程中的监听器(watcher,即uv_work_t结构中需要用一个字段来保存task的指针,或_cb字段保存回调函数指针(取决于需求,不过后来不用了)),再来看看threadpool方面,后来改用一个工作队列方式自动进行线程管理以及线程通信,提供线程结束后的回调操作,也是比较方便的,代码如下:

 inline void SpawnNewThread(void*msg = NULL)
    {
        //listener
        uv_work_t req = mp_evPump->plast->req;
        req.data = msg;
        uv_queue_work(mpLoop,&req,ProcessState::IOTaskWrapper,ProcessState::RetreatThreadToPool);
    }

RetreatThreadToPool就是线程即将被池回收时触发的回调指针啦,这是node内核的第一个版本,基本对then()链进行了底层的驱动操作,实现js promise机制调用then()链的并发操作,也设计了一套操作结构进行其他js层的本地操作实现,主要借助V8引擎这个桥梁,关于V8,会在以后的文章做讨论,不过window下导入libuv库真的很麻烦,v8也是,还是在linux下进行编译开发来的痛快,毕竟可以看到g++全套编译过程的命令,makefile脚本对我们来说也是完全透明的,win下libuv库编译生成的obj并没有对其他模块导出自己的符号表,导致其他模块导出符号表中的符号找不到源头,最后冒出lnk错误,linux下就好很多了,ok,本次的分享就到这里啦~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值