当前位置: 首页 > 后端技术 > Node.js

使用Node来处理I-O密集型任务

时间:2023-04-03 16:33:20 Node.js

摩尔定律摩尔定律由英特尔联合创始人戈登·摩尔于1965年提出,即一块集成电路上可容纳的元器件数量每增加18倍就会翻一番24个月,业绩翻番。也就是说,处理器(CPU)性能大约每两年翻一番。自摩尔定律提出以来,已经过去了50多年。现在,随着芯片组件越来越接近单个原子的规模,跟上摩尔定律的步伐越来越难。2019年,英伟达CEO黄仁勋在ECS展上表示:“摩尔定律过去每5年增长10倍,每10年增长100倍。现在,摩尔定律每年只能增长几个百分点,而且可能每10年只能出现100次。”2次。因此,摩尔定律结束了。”单处理器(CPU)的性能越来越接近瓶颈,要突破这个瓶颈,就需要充分利用多线程技术,让单颗或多颗CPU可以同时执行多个进程线程,更快的完成计算机任务,我们都知道Node的多线程,Javascript是单线程语言,Nodejs利用Javascript的特性,使用事件驱动模型实现异步I/O,异步I/O背后是多线程调度,非常适合处理I/O/O密集型任务,Node异步I/O的实现可以参考Go语言中ParkLing的《深入浅出 Node.js》,可以通过创建Goroutine来显式调用新线程,并通过环境变量GOMAXPROCS控制最大并发数。在Node中,可以使用worker_threads创建一个新的Worker来产生新的工作线程。工作线程用于执行CPU密集型JavaScript操作。它们在I/O密集型工作中不是很有用。Node.js内置的异步I/O操作比工作线程更高效。Node本身实现了一些异步I/OAPI,比如fs.readFile,http.request。这些异步I/O的底层是调用一个新的线程来执行异步任务,然后使用事件驱动的方式获取执行结果。服务器端开发和工具开发可能都需要处理I/O密集型任务。比如处理复杂的爬虫任务、处理并发请求、文件处理等等……当我们使用多线程处理I/O密集型任务时,我们必须控制最大并发任务数。因为没有控制最大并发数,可能会导致文件描述符耗尽导致的错误、带宽不足导致的网络错误、端口限制导致的错误等等。Node中并没有控制最大并发数的API和环境变量,所以接下来,我们将通过简单的几行代码来实现。对于代码实现,我们假设如下需求场景。我有一个爬虫,每天需要爬取100篇掘金文章。如果一篇一篇的爬取太慢,一次爬取100篇文章会因为网络连接太多,导致很多请求直接失败。那么我们就可以实现了,每次请求10篇文章,分10次完成。这样不仅可以提高10倍的效率,而且运行稳定。我们来看一个单一的请求任务,代码实现如下:constaxios=require("axios");asyncfunctionsingleRequest(article_id){//这里直接使用axios库做请求constreply=awaitaxios.post("https://api.juejin.cn/content_api/v1/article/detail",{article_id,});returnreply.data;}为了方便演示,这里我们请求同一个地址100次,我们创建100个请求任务,代码实现如下://请求任务列表constrequestFnList=newArray(100).fill("6909002738705629198").map((id)=>()=>singleRequest(id));接下来,我们来一个实现并发请求的方法。该方法支持同时执行多个异步任务,可以限制最大并发数。任务池中的任务执行完毕后,会推送新的异步任务继续执行,保证任务池的高利用率。代码实现如下:constchalk=require("chalk");const{log}=require("console");/***执行多个异步任务*@param{*}fnList任务列表*@param{*}max最大并发数限制*@param{*}taskName任务名*/asyncfunctionconcurrentRun(fnList=[],max=5,taskName="unnamed"){if(!fnList.length)return;log(chalk.blue(`开始执行多个异步任务,最大并发数:${max}`));常量回复列表=[];//收集任务执行结果constcount=fnList.length;//任务总数conststartTime=newDate().getTime();//记录任务执行开始时间letcurrent=0;//任务执行程序constschedule=async(index)=>{returnnewPromise(async(resolve)=>{constfn=fnList[index];if(!fn)returnresolve();//执行当前异步taskconstreply=awaitfn();replyList[index]=reply;log(`${taskName}交易进度${((++current/count)*100).toFixed(2)}%`);//执行完当前任务后,继续执行任务池中剩余的任务awaitschedule(index+max);resolve();});};//任务池执行器constscheduleList=newArray(max).fill(0).map((_,index)=>schedule(i指数));//使用Promise.all批量执行constr=awaitPromise.all(scheduleList);constcost=(newDate().getTime()-startTime)/1000;log(chalk.green(`执行完成,最大并发数:${max},耗时:${cost}s`));returnreplyList;}从上面的代码我们可以看出使用Node实现并发请求的关键是Promise.all,Promise.all可以同时执行多个异步任务上面的代码中,一个长度为max的数组maximum创建并发数,并在数组中放入相应数量的异步任务。然后使用Promise.all同时执行这些异步任务。当单个异步任务执行完毕后,会从任务池中取出一个新的异步任务继续执行,效率最大化。接下来,我们使用如下代码执行测试(代码实现如下)(async()=>{constrequestFnList=newArray(100).fill("6909002738705629198").map((id)=>()=>singleRequest(id));constreply=awaitconcurrentRun(requestFnList,10,"请求掘金文章");})();最终的执行结果如下图所示:到这里,我们的并发请求就完成了!接下来我们来测试一下不同并发的速度~首先是1并发,也就是没有并发(如下图)需要11.462秒!当不使用并发时,任务需要很长时间。接下来看看其他并发数情况下的耗时情况(如下图)。从上图可以看出,随着并发数的增加,任务执行速度越来越快。快点!这就是高并发的优势,在某些情况下可以提高几倍甚至几十倍的效率!如果我们仔细看一下上面的耗时,会发现随着并发数的增加,耗时还是会有一个阈值,不能完全成倍增加。这是因为Node实际上并没有为每个任务都开一个线程,而只是为异步I/O任务开一个新的线程。因此,该方案更适合处理I/O密集型任务。如果是CPU(计算)密集型任务,需要考虑使用worker_threads进行处理。至此,我们关于使用Node处理I/O密集型任务的介绍就结束了。如果要改进程序,还需要考虑任务超时时间和容错机制。如果你有兴趣,可以自己实现。参考资料Nodejs官网文档《深入浅出 Nodejs》MBA智库百科百度百科上一篇如果你已经看过了,希望你在走之前能喜欢一下~你的点赞是对作者最大的鼓励,也可以让更多人受益很多人看到这篇文章!如果您觉得本文对您有帮助,请帮忙点亮github上的star,鼓励一下!