使用事件驱动和异步I/O模式,Node.js实现了单线程和高并发的运行时环境,单线程意味着同一时间只能做一件事。那么Node.js如何利用单线程实现高度并发和异步的I/O呢?本文将围绕这个问题讨论Node.js的单线程模型:
1.高并发性
一般来说,高并发的解决方案是多线程模型。服务器为每个客户端请求分配一个线程,使用同步I/O,系统通过线程切换来弥补同步I/O调用的时间开销。阿帕奇就是这样的策略。由于I/O一般都是耗时的操作,这种策略很难实现高性能,但是非常简单,可以实现复杂的交互逻辑。
事实上,大多数网站的服务器端并不做太多计算。他们只是接收请求,将它们交给其他服务(例如从数据库中读取数据),然后等待结果返回,然后将它们发送给客户端。因此,鉴于这一事实,Node.js采用了单线程模型来处理。它不是为每个访问请求分配一个线程,而是使用一个主线程来处理所有请求,然后异步处理I/O操作,避免了创建和销毁线程以及在线程之间切换的开销和复杂性。
2.事件周期
Node.js在主线程中维护一个事件队列。收到请求后,它将请求作为事件放入队列,然后继续接收其他请求。当主线程空闲时(当它不请求访问时),它开始循环事件队列,并检查队列中是否有任何要处理的事件。此时有两种情况:如果是非I/O任务,会亲自处理,通过回调函数返回上层调用;如果是I/O任务,从线程池中取出一个线程执行这个事件,指定一个回调函数,然后继续循环队列中的其他事件。线程中的I/O任务完成后,执行指定的回调函数,完成的事件放在事件队列的末尾,等待事件循环。当主线程再次循环到事件时,它被直接处理并返回到上层进行调用。这个过程称为事件循环,如下图所示:
该图展示了整个Node.js的工作原理,从左到右,从上到下,Node.js分为四层,分别是应用层、V8引擎层、Node API层和LIBUV层。
应用层:Javascript交互层,俗称Node.js模块,如http、fsV8引擎层:由V8引擎解析Javascript语法,然后与下级API交互;NodeAPI层:为上层模块提供系统调用,通常用C语言实现,与操作系统交互;LIBUV层:事件循环,Node.js异步实现的核心,由LIBUV库实现,LIBUV中的线程池
从上面的理解来看,Node.js的单线程只是指在单线程中运行的Javascript,而不是Node.js,在Node中,无论是Linux还是Windows平台,内部都使用线程池完成IO操作,LIBUV根据不同平台的差异实现统一调用。
3.事件驱动
总结以上过程,我们可以发现Node.js的核心是使用事件驱动的方式实现异步I/O。为了更具体更清楚地理解和接受这个事实,我们用代码来描述Node.js的事件驱动模型:
3.1.事件队列
首先,我们需要定义一个事件队列。因为它是一个队列,所以它是先进先出(FIFO)数据结构。我们用JS数组来描述它,如下所示:
/* * *定义事件队列* enqueue: unshfit() *出列:pop() *空队列:length==0 */eventQueue:[],为了便于理解,我们规定数组的第一个元素是队列的尾部,数组的最后一个元素是队列的头部,unshfit是在尾部插入一个元素,pop是从队列开始
3.2.接收请求
定义接收用户请求的通用门户,如下所示:
/* * *接收用户请求*每个请求都会进入函数*传递参数request和Response */process http request :函数(request,Response){ //定义一个事件对象varevent=createevent({ params 3360 request . params,//传递请求参数result3360null,//存储请求结果callback 3360 function(){ }//指定回调函数});//在队列末尾添加eventQueue.unshift(事件);},这个功能很简单,就是把用户的请求包装成一个事件,放入队列中,然后继续接收其他请求。
3.3.事件周期
当主线程空闲时,它开始循环事件队列,因此我们定义了一个事件循环函数:
/* * *事件循环体,主线程选择执行*循环通过事件队列*处理事件*执行回调,并返回上层*/*/eventloop : function(){///如果队列不为空,则继续循环while (this。event queue . length 0){//取出一个事件var event=this.eventQueue.pop(。//如果是IO任务if(isIOTask(event)){ //从线程池中取出一个线程varthread=getthreadfromthreadpool();//交给线程处理。线。handleevent(事件)} else {//处理完非IO任务后,直接返回结果var result=handleEvent(事件);//最后通过回调函数返回到V8,再由V8返回到application event . callback . call(null,result);}}},主线程不断检查事件队列,IO任务交给线程池处理,非IO任务自己处理返回。
3.4.线程池
接收到任务后,线程池直接处理IO操作,比如读取数据库:
当IO
/* * *处理IO任务*完成后将事件添加到队列末尾*释放线程*/handleotask:函数(事件){//当前线程var curThread=this//operation database varopt database=function(params,callback){ var result=readdatafromdb(params);callback.call(null,result)};//执行IO任务optdatabase(事件。params,function(result){//返回结果并存储在事件对象event.result=result中;//IO//完成后,将不再是耗时的任务事件,event.isIOTask=false//将事件添加回队列尾部this.eventQueue.unshift(事件);//释放当前线程releaseThread(curThread) })}任务完成后,执行回调,将请求结果存储在事件中,将事件放回队列中,等待循环,最后释放线程。当主线程再次循环到事件时,它被直接处理。
4.Node.js的弱点
以上四个步骤简单描述了Node.js的事件驱动模型,至此,我们应该对Node.js有一个简单明了的了解,但是Node.js什么都做不了。
如上所述,如果是I/O任务,Nodejs会将任务交给线程池进行异步处理,既高效又简单。所以Node.js适合处理I/O密集型任务,但并不是所有的任务都是I/O密集型任务。当遇到CPU密集型任务时,是一种只使用CPU计算的操作,例如加密和解密数据(node.bcrypt.js)。数据压缩解压(node-tar),然后Node.js亲自处理,一个一个计算。前面的任务没有完成,后面的任务只能等待,如下图所示:
在事件队列中,如果前一个CPU计算任务没有完成,那么后面的任务就会被阻塞,响应就会变慢。如果操作系统本身是单核,那就算了,但是现在大多数服务器都是多CPU或者多核,而Node.js只有一个EventLoop,只占用一个CPU/核。当Node.js被CPU密集型任务占用,导致其他任务被阻塞时,仍然有CPU/内核处理空闲。因此,Node.js不适合CPU密集型任务。
5.Node.js适用场景
5.1、RESTful应用编程接口
这是Node的理想选择,因为您可以构建它来处理数万个连接。它仍然不需要很多逻辑;它本质上只是从数据库中查找一些值,然后将它们组成一个响应。由于响应是少量文本,入站请求也是少量文本,所以流量不高,甚至一台机器就能处理最繁忙公司的API需求。
5.2.实时程序
比如聊天服务,聊天应用就是Node.js优势的最好例证:轻量级、高流量,并且可以很好地应对跨平台设备上运行的密集数据(虽然计算能力较低)。同时,聊天也是一个值得学习的用例,因为它很简单,涵盖了一个典型的Node.js到目前为止将使用的大多数解决方案。
以上是边肖介绍的Node.js的单线程模型。希望对大家有帮助。如果你有任何问题,请给我留言,边肖会及时回复你。非常感谢您对我们网站的支持!