异步是Node
的特性之一,下面在Windows
下,简要分析Node
异步I/O
的实现
事件循环与观察者
- 事件循环
事件循环模型是Node
回调机制的支撑,在进程启动时,Node
便会创建一个循环,每一次循环称之为Tick
,每个Tick
的过程即为查看是否有事件等待处理,若有,就取出事件及其关联的回调函数,若函数存在,就予以执行,进入下一个循环,若不再有事件处理,就退出程序。 - 观察者
在每个Tick
的过程中,要判断是否有事件要处理,就要用到观察者的概念,每个事件循环中可以有一个或多个观察者,判断是否有事件要处理,就是向这些观察者询问是否有要处理的事件。
该情景就像餐馆做菜,后厨每做完一盘菜,就询问前台是否还有新的菜要做,若没有,就可以休息下班,若有,就要继续做,在此过程中,后厨每进行一次询问,就是一次Tick
,而前台就是观察者,客人向前台下单就是一个事件,客人要求做的菜就是事件要处理的数据,而菜做完后要如何处理(打包还是上桌,上到哪桌)就是该事件的回调。前台可以有多个,客人可以不止向一个前台点餐,每个前台可以接受多个客人点餐,客人每次点菜可以点多个,即每个事件循环中,可以有多个观察者,一个观察者里也可能有多个事件,每个事件可能触发多个回调。
Node
中,I/O
请求就是产生相应的事件,同样,有文件I/O
观察者与之对应。
请求对象
在Node
中,异步I/O
调用时,回调函数不用开发人员调用,从发起调用到内核执行完I/O
操作的过程中,存在一种叫做请求对象的中间产物。以fs.open()
调用为例,其源码调用实现如下:
fs.open = function(path, flags, mode, callback) {
callback = makeCallback(arguments[arguments.length - 1]);
mode = modeNum(mode, 438 /*=0666*/);
if (!nullCheck(path, callback)) return;
binding.open(pathModule._makeLong(path), //调用binding.open()
stringToFlags(flags),
mode,
callback);
}
可以看到,fs.open()
通过调用binding.open()
来进行操作,binding
的定义如下:
var binding = process.binding('fs')
process.binding()
可以看作一个桥梁,由于Node
的一些核心模块是由C/C++
编写的,process.binding()
即可看作对该种模块的绑定,其接收一个参数,将该参数对应的C/C++
模块取出绑定,这里接受了参数fs
,证明该模块是由C/C++
实现的,将该模块的实现取出绑定给了binding
,fs
对应的C/C++
实现模块为node_file.cc
,其为文件操作给出了最终实现,因此,最终调用到的函数为node_file.cc
中的Open
函数,其源码如下:
static void Open(const FunctionCallbackInfo<Value>& args) {
// ...
String::Utf8Value path(args[0]);
int flags = args[1]->Int32Value();
int mode = static_cast<int>(args[2]->Int32Value());
if (args[3]->IsFunction()) { //如果存在回调
ASYNC_CALL(open, args[3], *path, flags, mode)
} else {
SYNC_CALL(open, *path, *path, flags, mode)
args.GetReturnValue().Set(SYNC_RESULT);
}
}
在Open
中,获取到传入的所有参数,并将其传送给SYNC_CALL
,其源码如下:
#define ASYNC_CALL(func, callback, ...) \
Environment* env = Environment::GetCurrent(args.GetIsolate()); \ //获取当前系统环境
FSReqWrap* req_wrap = new FSReqWrap(env, #func); \ //请求对象
int err = uv_fs_ ## func(env->event_loop(), \ //根据平台执行相应函数
&req_wrap->req_, \
__VA_ARGS__, \
After); \
req_wrap->object()->Set(env->oncomplete_string(), callback); \ //将回调函数放置在oncomplete_string()属性上
req_wrap->Dispatched(); \ //线程分发
if (err < 0) { \
uv_fs_t* req = &req_wrap->req_; \
req->result = err; \
req->path = NULL; \
After(req); \
} \
args.GetReturnValue().Set(req_wrap->persistent());
SYNC_CALL
首先获取当前系统环境,若是 *nix
平台,则执行dep/uv/src/unix/fs.c
中的uv_fs_open
方法,若是windows
平台,则调用dep/uv/src/win/fs.c
中的uv_fs_open
方法,在其调用过程中,会创建一个FREeqWrap
请求对象,JavaScript
层所传入的参数和当前方法都会被封装在这个请求对象中,将回调函数放置在oncomplete_string()
属性上,封装完毕后,将该对象分发入线程池等待运行。至此,JavaScript
调用立即返回,并执行当前任务的后续操作,即达到了异步的目的。
执行回调
上面已经组装好了请求对象并送入了I/O
线程池等待执行,下一步便是回调通知。当线程池的I/O
操作调用完毕后,会将获取到的结果放到req->result
属性上,并通知IOCP
(I/O 完成端口,是windows
下的内核级异步I/O
实现),向IOCP
提交执行状态,在整个过程中,每次Tick
执行过程中,都会调用IOCP
的相关方法检查线程池中是否有已经完成的请求,若有,就将该请求对象放入I/O
观察者的队列中,将其当作事件处理。I/O
观察者回调函数的行为就是取出请求对象的result
属性作为参数,oncomplete_string()
属性作为方法,调用执行,达到调用JavaScript
中传入的回调函数的目的。
至此,整个异步I/O
的操作流程从发出请求到执行回调全部结束。