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

nodejs真的是单线程的吗?

时间:2023-04-03 11:04:17 Node.js

1、多线程和单线程语言像java、python都可以有多线程。多线程同步模式是这样的,CPU被分成几个线程,每个线程同步运行。Node.js采用单线程异步非阻塞模式,即每次计算独占cpu,遇到I/O请求时不阻塞后续计算。当I/O完成后,会以事件的形式通知它,并继续执行计算2。事件驱动、异步、单线程、非阻塞I/O,这是我们听到最多的关于nodejs的介绍。看到上面的关键词,我们可能会好奇:为什么运行在浏览器中的Javascript可以在这么低的层次上与操作系统进行交互呢?既然nodejs是单线程的,如何实现异步非阻塞I/O呢?Nodejs都是异步调用和非阻塞I/O,你真的不关心并发数吗?nodejs事件驱动是如何实现的?和浏览器的事件循环一样吗?nodejs擅长什么?你不擅长什么?2.nodejs的内在秘密要搞清楚以上问题,首先要搞清楚nodejs是如何工作的。我们可以看到Node.js的结构大致分为三个层次:1.Node.js标准库,用Javascript编写,也就是我们在使用过程中可以直接调用的API。在源码中的lib目录下可以看到。2.节点绑定,这一层是Javascript与底层C/C++通信的关键。前者通过绑定调用后者相互交换数据。3.这一层是支撑Node.js运行的关键,由C/C++实现。V8:Google推出的JavascriptVM,也是Node.js为什么使用Javascript的关键。它为Javascript在非浏览器端运行提供了一个环境。它的高效是Node.js高效的原因之一。libuv:它为Node.js提供了跨平台、线程池、事件池、异步I/O等能力,是Node.js强大的关键。C-ares:提供异步处理DNS的能力。http_parser、OpenSSL、zlib等:提供包括http解析、SSL、数据压缩等其他能力。3、libuv简介可以看出,几乎所有与操作系统打交道的部分都离不开libuv的支持。libuv也是node跨操作系统实现的核心。4、我们来看一下我一开始提出的问题。问题一:为什么运行在浏览器中的Javascript可以在如此底层与操作系统进行交互?举个简单的例子,如果我们要打开一个文件,进行一些操作,我们可以写下面这段代码:varfs=require('fs');fs.open('./test.txt',"w",function(err,fd){//..做某事});fs.open=function(path,flags,mode,callback){//...binding.open(pathModule._makeLong(path),stringToFlags(flags),mode,callback);};这段代码的调用过程大致可以描述为:lib/fs.js→src/node_file.cc→uv_fs从JavaScript调用Node的核心模块,核心模块调用C++的内置模块,Built-in模块通过libuv进行系统调用,这是Node中经典的调用方式。一般来说,我们在Javascript中调用的方法,最终都会通过node-bindings传递到C/C++层面,最终由它们来执行真正的操作。这就是Node.js与操作系统交互的方式。问题2:由于nodejs是单线程的,如何实现异步非阻塞I/O?顺便答题nodejs真的是单线程的吗?其实只是js执行是单线程的,I/O明显是其他线程。js执行线程是单线程的,把需要做的I/O交给libuv,马上返回去做其他事情,然后libuv会在指定的时间回调。其实简化的过程就是姜子!为了细化,nodejs首先会通过node-bindings从js代码中调用C/C++代码,然后通过C/C++代码封装一个“请求对象”交给libuv。这个请求对象无非就是需要执行的函数。+回调之类的东西,实现libuv执行和执行的回调。综上所述,一个异步I/O的大致流程如下:1.发起一个I/O调用。用户通过Javascript代码调用Node核心模块,向核心模块传递参数和回调函数;Node核心模块将传入的参数和回调函数封装成一个request对象;请求对象被压入I/O线程池执行;Javascript发起的异步调用结束,Javascript线程继续执行后续操作。2.执行回调I/O操作完成后,会取出request对象中封装的回调函数,执行回调函数,完成Javascript回调的目的。(这里回调的细节在下面解释)从这里可以看出,我们一直对Node.js的单线程存在误解。其实它的单线程是指自身Javascript运行环境的单线程。Node.js没有在执行Javascript时创建新线程的能力。最终的实际操作是通过Libuv及其事件循环进行的。这就是为什么Javascript这种单线程语言在Node.js中可以实现异步操作,而且两者并不冲突。问题3:nodejs都是异步调用和非阻塞I/O,所以并发数真的不重要吗?之前我们提到了线程池的概念,发现nodejs不是单线程的,是有并行事件的。同时,线程池默认大小为4,也就是说可以有4个线程同时做文件I/O工作,剩下的请求会被挂起等待,直到线程池免费。所以nodejs是受并发数限制的。线程池的大小可以通过环境变量UV_THREADPOOL_SIZE改变,也可以通过nodejs代码中的process.env.UV_THREADPOOL_SIZE重新设置。问题四:nodejs事件驱动是如何实现的?和浏览器的事件循环一样吗?事件循环是一种执行模型,在不同的地方有不同的实现。浏览器和nodejs基于不同的技术实现了自己的事件循环。简单的说:nodejs的事件是基于libuv的,而浏览器的事件循环在html5的规范中有明确的定义。libuv已经实现了事件循环,但是html5规范只定义了事件循环在浏览器中的模型,具体实现留给浏览器厂商。上面我们提到libuv已经接管了js传递过来的I/O请求,那么回调什么时候处理呢?libuv有一个事件循环(eventloop)机制来接受和管理回调函数的执行。事件循环是libuv的核心。上面我们提到js会把回调和任务交给libuv。当libuv调用时,回调由事件循环控制。事件循环首先在内部维护了多个事件队列(或者叫观察者watchers),比如时间队列、网络队列等,用户可以在watchers中注册回调。当有事件发生时,事件会进入pending状态,然后在下一次循环时,将它们取出来依次执行,libuv会执行一个相当于whiletrue的无限循环,不断检查是否有pending状态事件需要在每个watcher上处理,如果是,则按顺序触发存储在队列中的事件,同时,由于libuv的事件循环一次只执行一个回调,避免了竞争。libuv的事件循环执行图:nodejs的事件循环分为6个阶段,每个阶段的作用如下:timers:在setTimeout()和setInterval()中执行过期回调。I/O回调:上一个周期的少量I/O回调会延迟到本轮这个阶段执行idle,prepare:只内部使用poll:最重要的阶段,执行I/O回调,在这个阶段适当的Check在某些情况下会被阻塞:executesetImmediate的回调closecallbacks:执行close事件的回调,比如socket.on("close",func)事件循环的每个循环都需要走依次经过以上阶段。每个阶段都有自己的回调队列。每当进入一个阶段,回调就会从队列中取出来执行。当队列为空或者执行的回调数达到系统最大数时,进入下一阶段。这六个阶段的完成称为一个循环。附事件循环源码:intuv_run(uv_loop_t*loop,uv_run_modemode){inttimeout;诠释;intran_pending;/*从uv__loop_alive我们知道事件循环继续的条件是以下三种之一:1.有活跃的句柄(Libuv定义句柄为一些长寿命的对象,比如tcpserver)2.有活跃的请求3.Closing_handlesinloop*/r=uv__loop_alive(loop);如果(!r)uv__update_time(循环);while(r!=0&&loop->stop_flag==0){uv__update_time(loop);//更新时间变量,这个变量将在uv__run_timers中使用uv__run_timers(loop);//定时器阶段ran_pending=uv__run_pending(loop);//从libuv的文档可以看出,这其实是I/O回调阶段,ran_pending表示队列是否为空uv__run_idle(loop);//空闲阶段uv__run_prepare(loop);//准备阶段timeout=0;/**设置poll阶段的超时时间,以下情况会设置timeout为0,表示此时poll阶段不会被阻塞,这个我们会在后面的poll阶段1中详细讨论,stop_flag不为02,没有activehandles和request3,idle、I/Ocallback、close阶段的handlequeue不为空,否则设置为callbackqueue中距离当前时间最近的一个在定时器阶段**/if((mode==UV_RUN_ONCE&&!ran_pending)||模式==UV_RUN_DEFAULT)超时=uv_backend_timeout(循环);uv__io_poll(loop,timeout);//pollphaseuv__run_check(loop);//checkphaseuv__run_closing_handles(loop);//closephase//如果mode==UV_RUN_ONCE(意思是进程继续往前走),定时器会所有阶段结束后检查一次。这个逻辑原因不清楚if(mode==UV_RUN_ONCE){uv__update_time(loop);uv__run_timers(循环);}r=uv__loop_alive(loop);如果(模式==UV_RUN_ONCE||模式==UV_RUN_NOWAIT)中断;}if(loop->stop_flag!=0)loop->stop_flag=0;返回r;}这里我们详细了解一下poll阶段:poll阶段主要有两个功能:1.执行达到最小时间的定时器的回调2.处理poll队列中的事件。当事件循环进入轮询阶段并且没有设置定时器(没有定时器被调度)时,会发生以下两种情况之一:1.如果轮询队列不为空,事件循环将遍历队列并执行同步回调,直到队列清空或者回调执行次数达到系统上限;2.如果轮询队列为空,则会发生以下两种情况之一:(1)如果代码已经通过setImmediate()设置了回调,事件循环将结束轮询阶段,进入检查阶段执行检查队列(其中的回调)。(2)如果代码没有被setImmediate()设置为回调,事件循环会阻塞在这个阶段,等待回调被添加到轮询队列,并立即执行。但是,当事件循环进入轮询阶段并设置了定时器后,一旦轮询队列为空(轮询阶段空闲状态):事件循环将检查定时器,如果一个或多个定时器的下限时间已到已经达到,事件循环将环绕到计时器阶段。一个事件循环示例告诉我们:varfs=require('fs');functionsomeAsyncOperation(callback){//假设这个任务需要95msfs.readFile('/path/to/file',callback);}vartimeoutScheduled=Date.now();setTimeout(function(){vardelay=Date.now()-timeoutScheduled;console.log(delay+"mshavepassedsinceIwasscheduled");},100);//someAsyncOperationto它需要95毫秒完成someAsyncOperation(function(){varstartCallback=Date.now();//需要10毫秒...while(Date.now()-startCallback<10){;//什么都不做}});while事件循环进入轮询阶段,它有一个空队列(fs.readFile()还没有结束)。因此它将等待剩余的毫秒数,直到达到最近的计时器的下限时间。当它等待95毫秒时,fs.readFile()首先完成,然后将其回调添加到轮询队列并执行-此回调需要10毫秒。之后,由于队列中没有其他回调,事件循环会检查定时器最近到达的下限时间,然后返回定时器阶段执行定时器的回调。所以在例子中,从设置回调到执行回调的时间间隔是105ms。这里再总结一下,整个异步IO过程:问题5,nodejs擅长什么?你不擅长什么?Node.js通过libuv处理与操作系统的交互,因此具有异步、非阻塞和事件驱动的能力。因此,NodeJS可以响应大量的并发请求。因此,NodeJS适合在高并发、I/O密集、业务逻辑量少的场景下使用。上面说了,如果是I/O任务,Node.js会把任务交给线程池异步处理,高效简单,所以Node.js适合处理I/O密集型任务。但并非所有任务都是I/O密集型任务。当遇到CPU密集型任务,只使用CPU计算的操作,比如数据加解密(node.bcrypt.js),数据压缩解压(node-tar),此时Node.js会自己处理,一一计算。如果前面的任务没有完成,后面的任务就只能等待了。我们看下面的代码:varstart=Date.now();//获取当前时间戳setTimeout(function(){console.log(Date.now()-start);for(vari=0;i<1000000000;i++){//执行长循环}},1000);setTimeout(function(){console.log(Date.now()-开始);},2000);最后,我们的打印结果是:(结果可能因为你的机器不同)10003738对于我们期望2秒后执行的setTimeout函数,实际执行了3738毫秒。也就是说,因为执行了一个很长的for循环,我们整个Node.js主线程都被阻塞了那么,如果我们处理100个用户请求,第一个需要这么大的计算量,那么剩下的99个就会被延迟。如果操作系统本身是单核的,那还好,但是现在大部分服务器都是多CPU或者多核,而Node.js只有一个EventLoop,也就是只占用一个CPU核。任务被占用,导致其他任务阻塞,但仍有CPU核心处于空闲状态,造成资源浪费。事实上,Node.js虽然可以处理几千个并发,但一个Node.js进程在某一时刻实际上只是在处理一个请求。因此,Node.js不适合CPU密集型任务。参考文章:https://www.cnblogs.com/chris...https://www.cnblogs.com/onepi...https://blog.csdn.net/scandly...http://liyangready.github.io/...https://blog.csdn.net/xjtrodd...https://blog.csdn.net/sinat_2...