异步I/O

2020-09-19  本文已影响0人  jluemmmm

为什么要用异步I/O?

单线程同步编程会引阻塞I/O导致硬件资源得不到更优使用,多线程编程会出现死锁、状态同步等问题,node的解决方案:利用单线程,避免多线程死锁、状态同步问题,利用异步I/O,让单线程远离阻塞,更好使用CPU。为了弥补单线程无法利用多核CPU的缺点,node提供了类似前端浏览器中webworker的子进程,子进程可以通过工作进程高效利用CPU和I/O。

阻塞I/O与非阻塞I/O

操作系统对计算机进行抽象,将所有输入输出设备抽象为文件,内核在进行文件I/O操作时,通过文件描述符进行管理,文件描述符类似于应用程序与系统内核之间的凭证。应用程序如果需要进行I/O调用,需要先打开文件描述符,根据文件描述符实现文件数据的读写非阻塞I/O与阻塞I/O的区别在于阻塞I/O完成整个获取数据的过程,非阻塞I/O不带数据直接返回,要获取数据,通过文件描述符再次读取。非阻塞I/O,完整的I/O没有完成,应用程序通过轮询重复调用I/O操作确认是否完成

阻塞I/O

非阻塞I/O

非阻塞I/O不带数据直接返回,要获取数据,通过文件描述符再次读取。非阻塞I/O,完整的I/O没有完成,应用程序通过轮询重复调用I/O操作确认是否完成。现有的轮询技术有

epoll方式实现轮询

异步I/O

node的单线程,是指javascript执行在单线程中,node中,无论是*nix还是windows平台,内部完成I/O任务另有线程池。部分线程进行阻塞I/O或非阻塞I/O加轮询技术完成数据获取,一个线程进行计算处理,通过线程之间的通信将I/O得到的数据进行传递。

windows系统采用IOCP实现异步I/O,调用异步方法,等待I/O完成之后的通知,执行回调,用户无需考虑轮询,内部实现仍是线程池原理。*nix使用自定义线程池实现异步I/O。

node提供libuv做为抽象层封装,兼容windows平台和*nix平台的差异,保证上层的Node与下层的自定义线程池及IOCP之间独立,Node在编译期间判断平台条件,选择性编译unix目录或是win目录下的源文件到目标程序中。

基于libuv的架构示意图

windows系统下的异步I/O调用

  1. windows系统下的异步I/O调用,以fs.open()方法为例,js调用node核心模块,核心模块调用C++内建模块,内建模块通过libuv实现系统调用。

  2. 系统调用实质是调用各个平台下的uv_fs_open方法,在uv_fs_open调用过程中,创建FSReqWrap请求对象(请求对象是异步I/O过程中重要的中间产物,所有状态都保存在这个对象上,包括送入线程池等待执行以及I/O操作完毕后的回调处理),从js层传入的参数和当前方法都被封装在请求对象中,回调函数被设置在对象的oncomplete_sym属性上,对象包装完毕后,在windows系统下,调用 QueueUserWorkItem 方法将FSReqWrap对象推入线程池中等待执行

QueueUserWorkItem(&uv_fs_thread_proc, req, WT_EXECUTEDEFAULT)

[ QueueUserWorkItem方法接受三个参数,第一个参数是将要执行的方法的引用,这里引用的是uv_fs_thread_proc,第二个参数是uv_fs_thread_proc运行时所需的参数,第三个参数是执行的标志,当线程池中有可用线程时,调用uv_fs_thread_proc方法,uv_fs_thread_proc方法根据传入参数的类型调用相应的底层函数,以uv_fs_open为例,实际上调用fs__open方法 ]

  1. 至此,js调用立即返回,由js层面发起的异步调用第一阶段结束,js线程可以继续执行当前任务的后续操作,当前的I/O操作在线程池中等待执行。
fs.open调用示意图
  1. 线程池中的I/O操作调用完毕后,会将获取的结果储存在req->result属性上,然后调用PostQueuedCompletionStstus(该事件提交的状态可以通过GetQueuedCompletionStstus获取)通知IOCP,告知当前对象操作已经完成,将线程归还线程池。

  2. 在这个过程中,使用了事件循环的I/O观察者,每个tick的执行过程中,会调用IOCP相关GetQueuedCompletionStstus 检查线程池中是否有执行完的请求,如果存在,会将请求对象加入到I/O观察者的队列中,将其当作事件处理。

  3. I/O观察者回调函数的行为就是取出请求对象的result属性作为参数,取出oncomplete_sym属性作为方法,然后调用执行,达到调用js中传入回调函数的目的。

整个异步I/O的流程

windows下主要通过IOCP向系统内核发送I/O调用和从内核中获取已完成的I/O,配合事件循环,完成异步I/O,linux通过epoll实现,线程池在windows下由内核IOCP直接提供,*nix下由libuv自行实现。在node中,除了js是单线程的,node自身是多线程的,除了用户代码无法并行执行外,所有的I/O可以并行起来。

非I/O的异步API

process.nextTick = function(callback){
  if(process._exiting) return
  if(tickDepth >= process.maxTickDepth) maxTickWarn()
  var tock = { callback: callback }
  if(process.domain) tock.domain = process.domain
  newxtTickQueue.push(tock)
  if(nextTickQueue.length) process.needTickCallback()
}
process.nextTick(function () {
  console.log('nextTick1执行');
});
setImmediate(function () {
  console.log('setImmediate执行');
});
process.nextTick(function () {
  console.log('nextTick2执行');
});
console.log('正常执行');

正常执行
nextTick1执行
nextTick2执行
setImmediate执行

具体实现上,process.nextTick 的回调函数保存在一个数组中,setImmediate 结果保存在链表中,process.nextTick在每轮循环会将数组中的回调函数全部执行,setImmediate 在每轮循环中执行链表中的一个回调函数。

process.nextTick(function () {
  console.log('nextTick􁃽􀗿执行1');
});
process.nextTick(function () {
  console.log('nextTick􁃽􀗿执行2');
});
setImmediate(function () {
  console.log('setImmediate􁃽􀗿执行1');
  process.nextTick(function () {
    console.log('􀴽势􀖭入');
  });
});
setImmediate(function () {
  console.log('setImmediate􁃽􀗿执行2');
});
console.log('正常执行');



v12.18.3执行结果

正常执行
nextTick延迟执行1
nextTick延迟执行2
setImmediate延迟执行1
强势插入
setImmediate延迟执行2


v8.9.4执行结果

nextTick延迟执行1
nextTick延迟执行2
setImmediate延迟执行1
setImmediate延迟执行2
强势插入

第一个setImmediate回调函数执行后,并没有执行第二个,而是进入下一轮循环,再次按process.nextTick优先,setTimmediate后的顺序执行,保证每轮循环能够较快执行结束,防止CPU过多阻塞后续I/O调用情况

事件驱动与高性能服务器

文件操作和网络套接字的处理都用到了异步I/O,网络套接字上侦听不到的请求,形成事件交给I/O观察者,事件循环会持续处理网络I/O事件,如果js有传入回调函数,事件最终传递到业务逻辑层进行处理。利用node构建web服务器是在此基础上实现的。

node没有采用经典的服务器模型,包括 同步式、每进程/每请求、每线程/每请求,node通过事件驱动的方式处理请求,无需为每一个请求创建额外的对应线程,可以省掉创建线程和销毁线程的开销,同时操作系统在调度任务时因为线程较少,上下文切换的代价很低。在大量连接的情况下,不受线程上下文切换开销的影响。nginx采用事件驱动机制,由C语言实现,性能较高,适合做web服务器,用于反向代理或负载均衡等服务。

上一篇 下一篇

猜你喜欢

热点阅读