nodejs异步处理执行顺序,nodejs异步任务

  nodejs异步处理执行顺序,nodejs异步任务

  node.js速度课程简介:进入学习

  Node的诞生最初是为了构建一个高性能的Web服务器。作为JavaScript服务器运行时,具有事件驱动、异步I/O、单线程的特点。基于事件周期的异步编程模型使节点具有处理高并发的能力,大大提高了服务器的性能。同时,由于保持了JavaScript的单线程特性,Node不需要处理多线程下的状态同步和死锁问题,也不存在线程上下文切换带来的性能开销。基于这些特点,Node具有高性能、高并发的先天优势,可以基于它构建各种高速、可扩展的网络应用平台。

  本文将深入探讨节点异步和事件循环的底层实现和执行机制,希望对你有所帮助。

  

为什么要异步?

   Node为什么使用异步作为核心编程模型?

  如前所述,Node诞生的初衷是为了构建一个高性能的Web服务器。假设在一个业务场景中有几组不相关的任务要完成,有以下两种现代主流解决方案:

  单线程串行执行。

  多线程完成。

  单线程串行执行是一种同步编程模型。虽然更符合程序员按顺序思考的思维方式,但是很容易写出更方便的代码。但由于I/O是同步执行的,同一时间只能处理单个请求,会导致服务器响应速度较慢,在高并发的应用场景下无法应用。而且因为I/O被阻塞,CPU会一直等待I/O完成,无法做其他事情,这样CPU的处理能力就无法得到。

  但是多线程编程模式也会因为编程中的状态同步、死锁等问题而让开发者头疼。虽然多线程可以有效提高多核CPU上的CPU利用率。

  单线程串行顺序执行、多线程并行完成的编程模式虽然各有优势,但在性能和开发难度上也存在一定的不足。

  另外,从响应客户端请求的速度出发,如果客户端同时获取两个资源,同步模式的响应速度将是两个资源的响应速度之和,而异步模式的响应速度将是两者中最大的,相比同步模式性能优势明显。随着应用复杂度的增加,这种场景将演变为同时响应N个请求,异步相对于同步的优势将凸显出来。

  综上,Node给出了自己的答案:使用单线程,避免多线程死锁,状态同步等问题;利用异步I/O使单线程远离阻塞,从而更好地利用CPU。这就是Node使用异步作为其核心编程模型的原因。

  此外,为了弥补单线程无法利用多核CPU的缺点,Node还在浏览器中提供了一个类似Web Workers的子进程,可以通过工作进程高效利用CPU。

  

如何实现异步?

  说完了,为什么要用异步?那么如何实现异步呢?

  一般来说,异步操作分为两类:一类是文件I/O、网络I/O等I/O相关的操作;二是与I/O无关的操作,如setTimeOut、setInterval等。显然,我们所说的异步操作指的是与I/O相关的操作,即异步I/O。

  异步I/O的提出是希望I/O的调用不会阻塞后续程序的执行,将I/O完成的原有等待时间分配给其他需要的服务执行。为了实现这个目标,我们需要使用非阻塞I/O。

  阻塞I/O是指CPU发起I/O调用后,会一直阻塞,等待I/O完成。知道了阻塞I/O,非阻塞I/O就好理解了。CPU会在发起I/O调用后立即返回,而不是阻塞等待。在I/O完成之前,CPU可以处理其他事务。很明显,与阻塞I/O相比,非阻塞I/O明显比性能提升更多。

  那么,既然使用了非阻塞I/O,并且CPU发起I/O调用后可以立即返回,那么它是如何知道I/O完成的呢?答案是民意测验。

  为了及时获得I/O调用的状态,CPU会反复调用I/O操作来确认I/O是否已经完成。这种反复调用以确定操作是否已完成的技术称为轮询。

  很明显,轮询会让CPU反复进行状态判断,很浪费CPU资源。此外,轮询之间的间隔很难控制。如果间隔太长,I/O操作的完成将得不到及时的响应,间接降低了应用程序的响应速度。如果间隔太短,必然会使CPU花费更多的时间轮询,降低CPU资源的利用率。

  所以,轮询虽然满足了非阻塞I/O不会阻塞后续程序执行的要求,但对于应用来说只能算是一种同步,因为应用还是需要等待I/O的完全返回,等待还是要花很多时间的。

  我们期望的完美异步I/O应该是应用发起一个非阻塞调用,I/O调用的状态不需要不断被轮询查询。而是可以直接处理下一个任务,在I/O完成后通过信号量或者回调把数据传递给应用。

  如何实现这种异步I/O?答案是线程池。

  虽然本文一直提到Node是单线程执行的,但是这里的单线程是指JavaScript代码在单线程上执行。对于与主业务逻辑无关的I/O操作,不会影响或阻塞主线程的运行,但可以提高主线程的执行效率,实现异步I/O。

  通过线程池,主线程只进行I/O调用,其他线程进行阻塞I/O或非阻塞I/O加轮询技术完成数据采集,然后从I/O得到的数据通过线程间的通信进行传输,轻松实现异步I/O:

  主线程进行I/O调用,线程池进行I/O操作完成数据采集,然后通过线程间的通信将数据传递给主线程,从而完成一次I/O调用。然后主线程使用回调函数将数据暴露给用户,用户再使用数据完成业务逻辑操作,这是Node中一个完整的异步I/O过程。对于用户来说,不需要关心底层繁琐的实现细节,只需要调用Node封装的异步API,传入回调函数来处理业务逻辑,如下图:

  const fs=require( fs );

  fs.readFile(example.js ,(data)={

  //处理业务逻辑。

  });Nodejs的异步底层实现机制在不同平台有所不同:在Windows中,I/O调用发送到系统内核并通过IOCP从内核获取I/O操作,异步I/O过程由事件循环完成;这个过程是在Linux下用epoll实现的;在BSD下,通过kqueue实现,在Solaris下,通过事件端口实现。线程池是Windows下内核(IOCP)直接提供的,而*nix系列是libuv自己实现的。

  由于Windows平台和*nix平台的不同,Node提供了libuv作为抽象封装层,使得所有的平台兼容性判断都由该层完成,保证了上层节点独立于下层的自定义线程池和IOCP。在编译过程中,Node会判断平台条件,有选择地将unix目录或win目录下的源文件编译到目标程序中:

  以上是Node对异步的实现。

  (线程池的大小可以通过环境变量UV_THREADPOOL_SIZE来设置,默认值为4。用户可以根据实际情况调整该值。)

  那么问题来了,主线程从线程池中获取数据后,如何以及何时调用回调函数?答案是事件周期。

  

基于事件循环的异步编程模型

  既然用回调函数来处理I/O数据,就必然涉及到什么时候调用回调函数以及如何调用回调函数的问题。在实际开发中,经常会出现涉及多种类型异步I/O调用的场景。如何合理安排这些异步I/O回调,保证异步回调的有序进行,是一个难题。除了异步I/O,还有I/O以外的异步调用,比如定时器,实时性强,优先级更高。如何安排不同优先级的回调?

  因此,必须有一个调度机制来协调不同优先级和不同类型的异步任务,以保证这些任务在主线程上有序运行。和浏览器一样,Node选择了事件循环来承担这项重要任务。

  根据节点任务的种类和优先级,将其分为七类:定时器、挂起、空闲、准备、轮询、检查和关闭。对于每种类型的任务,都有一个先进先出的任务队列来存储任务及其回调(计时器存储在一个小的顶层堆中)。基于这七种类型,Node将事件循环的执行分为以下七个阶段:

  

timers

  此阶段具有最高的执行优先级。

  在这个阶段,循环会检查计时器的数据结构(最小堆),遍历其中的计时器,将当前时间与过期时间逐一比较,判断计时器是否过期。如果是,将取出并执行计时器的回调函数。

  

pending

  此阶段将在网络、IO等出现问题时执行回调。都不正常。*nix报告的一些错误将在此阶段处理。此外,一些应该在上一个周期的轮询阶段执行的I/O回调将被推迟到这个阶段。

  

idle、prepare

  这两个阶段仅用于事件循环内部。

  

poll

  检索新的I/O事件;执行I/O相关的回调(几乎所有回调set immediate());除了关闭回调、计时器计划的回调和);该节点将在适当的时候被阻止。

  Poll,即轮询阶段,是事件周期中最重要的阶段,网络I/O和文件I/O的回调主要在这个阶段处理。这个阶段有两个主要功能:

  计算在此阶段应该阻塞和轮询I/O的时间。

  处理I/O队列中的回调。

  当事件循环进入轮询阶段且定时器未设置时:

  如果轮询队列不为空,事件循环将遍历队列并同步执行它们,直到队列为空或达到可执行文件的最大数量。

  如果轮询队列为空,将会发生以下两种情况之一:

  如果有setImmediate()回调要执行,立即结束轮询阶段,进入检查阶段执行回调。

  如果没有要执行的setImmediate()回调,事件循环将停留在这个阶段,等待回调被添加到队列中,然后立即执行它们。事件循环将停留并等待,直到超时。我选择留在这里是因为Node主要处理IO,这样可以更及时的响应IO。

  一旦轮询队列为空,事件循环将检查已经达到时间阈值的计时器。如果一个或多个计时器达到时间阈值,事件循环将返回到计时器阶段,以执行这些计时器的回调。

  

check

   set immediate()的回调将依次在此阶段执行。

  

close

  此阶段将执行一些回调以关闭资源,如socket.on(close ),).这个阶段的后期实现也影响不大,优先级最低。

  节点进程启动时,会初始化事件循环,执行用户输入的代码,调用相应的异步API,调度定时器等。然后开始进入事件循环:

  计时器

   待定的回调

   闲着,准备

  来袭:

   民意测验的人脉,

  数据等。

   检查

  关闭回调

  `

  好了,以上是事件周期的基本执行流程。现在我们来看另一个问题。

  对于以下场景:

  const server=net . create server(()={ })。听(8080);

  server.on(listening ,()={ });当服务成功绑定到端口8000,即成功调用listen()时,此时监听事件的回调还没有绑定,所以端口绑定成功后,不会执行传入监听事件的回调。

  思考另一个问题,我们在开发中可能会有一些需求,比如处理错误、清理不必要的资源等优先级低的任务。如果同步执行这些逻辑,会影响当前任务的执行效率;如果setImmediate()是异步传入的,比如以回调的形式传入,并且不能保证它们执行的时机,那么实时性能就不高。那么如何处理这些逻辑呢?

  基于这些问题,Node参考浏览器,实现了一套微任务机制。在节点中,除了调用new Promise()传入的回调函数之外。然后()会封装成一个微任务,process.nextTick()的回调也会封装成一个微任务,后者的执行优先级高于前者。

  有了微任务,事件循环的执行过程是怎样的?换句话说,微任务的执行时间是什么时候?

  在节点11和11之后的版本中,一旦一个阶段中的任务完成,微任务队列立即执行,队列被清空。

  在node11之前,微任务的执行在一个阶段之后开始。

  因此,对于微任务,在事件周期的每个周期中,将首先执行计时器阶段的任务,然后是process.nextTick()和new Promise()的微任务队列。然后()会按顺序清空,然后执行timers阶段或者下一个阶段的下一个任务,也就是pending阶段的一个任务,以此类推。

  使用process.nextTick(),Node可以解决上面的端口绑定问题:在listen()方法内部,listen事件的发布将被封装成一个回调,并传递到process.nextTick()中,如下面的伪代码所示:

  函数listen() {

  //执行监听端口的操作。

  .

  //将listening 事件的问题封装为回调,并将其传递到process.nextTick()

  process.nextTick(()={

  发出(“监听”);

  });

  };当前代码执行后,会开始执行微任务,从而发出监听事件,触发事件回调的调用。

  

一些注意事项

  由于异步本身的不可预测性和复杂性,在使用Node提供的异步API的过程中,虽然我们已经掌握了事件循环的执行原理,但仍然可能会出现一些不直观或不预期的现象。

  例如,计时器(setTimeout、setImmediate)的执行顺序将根据调用它们的上下文而不同。如果两者都是从顶级上下文中调用的,那么它们的执行时间取决于进程或机器的性能。

  让我们看下面的例子:

  setTimeout(()={

  console.log(timeout )。

  }, 0);

  setImmediate(()={

  console . log(“immediate”);

  });以上代码的执行结果是什么?根据我们刚才对事件循环的描述,你可能会有这样的答案:由于timers阶段会在check阶段之前执行,所以会先执行setTimeout()的回调,再执行setImmediate()的回调。

  事实上,这段代码的输出结果是不确定的,可能会先输出超时或立即。这是因为两个计时器都是在全局上下文中调用的。当事件循环开始运行并执行到计时器阶段时,当前时间可能大于1 ms或更短,这取决于机器的执行性能。所以实际上在第一个timers阶段是否会执行setTimeout()是不确定的,所以会出现不同的输出结果。

  当delay(SetTimeout的第二个参数)的值大于2147483647或小于1时,delay将被设置为1。)

  让我们看看下面的代码:

  const fs=require( fs );

  fs.readFile(__filename,()={

  setTimeout(()={

  console.log(timeout )。

  }, 0);

  setImmediate(()={

  console . log(“immediate”);

  });

  });正如您所看到的,在这段代码中,两个计时器都被封装为回调函数,并传递给readFile。很明显,调用回调时,当前时间必须大于1 ms,所以setTimeout的回调会在setImmediate的回调之前调用,所以打印出来的结果是:timeout immediate。

  以上是使用Node时需要注意的与定时器相关的事项。另外还要注意process.nextTick()和new Promise()的执行顺序。then()和setImmediate()。由于这部分比较简单,前面已经提到过,这里就不赘述了。

  

总结

  文章首先从为什么和如何实现异步两个角度详细描述了节点事件循环的实现原理,并提到了一些相关的注意事项,希望对你有所帮助。

  更多关于node的信息,请访问:nodejs教程!这是节点异步和事件循环的底层实现和执行机制的细节。请多关注我们的其他相关文章!

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: