NodeJs - 单线程模型和高并发处理原理

前言

我们都知道JavaScript是单线程的处理。但是我们在Node开发、Egg开发下,我们的程序又能够处理高并发的请求。明明是单线程却能高并发处理,这是什么原理呢?我们本篇文章来探究一下。

一. NodeJs 线程模型

先分块解答一下 ”单线程“、 ”高并发“ 的含义:

  • 单线程:Node遵循的是单线程单进程的模式。单线程则指的是Js的引擎只有一个实例,它在NodeJs主线程中执行。但是它可以通过事件驱动的方式来处理异步IO操作。
  • 那如何处理高并发的?Node 拥有一个主线程,但是它只负责任务的往返调度,并不会真正执行用户请求等IO操作。而所有的IO操作则交给内部的work线程池去实现。

1.1 NodeJs 模型分析

我们看一下下面一张图:
在这里插入图片描述
首先我们从上图中,将Node模型分成4个功能模块:

  • Application:应用层。即JS的交互层,例如NodeJs中的模块:http、fs等。
  • V8引擎层:解析JS语法,和下层API进行交互。
  • NodeAPI层:为上层模块系统提供系统调用、底层则和操作系统进行交互。
  • LIBUV层:一种跨平台的底层封装。实现事件循环、文件操作等功能,是Node里面实现异步的核心模块。

再来讲一下和上图有关的几个基本概念:

  • 事件循环:事件循环是一种编程构造,用于等待和分派程序中的事件或消息。主线程从 事件队列中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)
  • 事件队列(任务队列):当用户的网络请求或者其它的异步操作到来时,node都会把它放到Event Queue之中,此时并不会立即执行它,代码也不会被阻塞,继续往下走,直到主线程代码执行完毕。
  • 事件驱动:实质是通过主循环加事件触发方式运行程序。

Node.js 不是一门语言也不是框架,它只是基于 Google V8 引擎的 JS 运行时环境,是对 JS 功能的拓展。提供了网络、文件、dns 解析、进程线程等功能。

1.2 NodeJs处理事件请求的流程

结合上文的几个相关概念,流程如下:

  1. Node 主线程接收到了用户的网络请求,会把它丢到Event Queue(事件队列)中,并不会立即执行它。主线程不会阻塞。
  2. Node主线程代码执行完毕,通过Event Loop(事件循环),每次循环都取出队列中的第一个事件,把它丢给LIBUV库的线程池中的线程让它执行,我们这里就叫libuv线程。备注:这个过程中,一旦有新的事件加入到队列中,都会通知主线程去按顺序取出来处理。
  3. libuv线程池的维护则是由底层的LIBUV来完成维护。默认打开4个,最多拥有128个线程。
  4. 当事件执行完毕,就会通知主线程,主线程执行回调拿到结果,libuv线程则归还给线程池,等待下一次调度。

因此对于Node而言,主线程执行JS,这个过程是单线程的。但是可以同时处理大量的IO事件(底层的libuv线程池来完成),非常适合IO密集型的任务处理。相反,若主线程JS进行了CPU密集型,那性能就会非常差,导致长时间的阻塞。

1.3 NodeJs 和传统 Server 的对比

对比项NodeJs传统Server
高并发处理NodeJs只有一个主线程(单线程),但是可以通过底层的LIBUV库中维护的libuv线程池处理高并发请求每个请求都会生成一个线程
内存消耗由于1000个请求是丢到队列当中(并不是立马执行的),排队等待最多128个libuv线程去处理。内存消耗小得多,时间换空间。假设1个进程需要1M内存,为了能同时处理1000个请求,就可能需要1G左右的内存
上下文切换由于单线程,不需要切换例如Java,线程之间需要进行上下文的切换

这么看下来,NodeJs 更像是 ”非阻塞“ 而不是并发。

  1. 再多的请求都是丢到队列,主线程不会受到阻塞。
  2. 主线程通过回调机制来拿到结果,只负责不断的往返调度,从而实现异步非阻塞IO

但是,在我们知道 JavaScript 代码是运行在单线程上后,即一个 Node.js 进程只能运行在一个 CPU 上,那是不是就无法利用到多核运算的好处了?

并不是,NodeJs 官方提供了一种解决方案:cluster 模块。

二. Cluster 模块利用多核CPU处理

官网当中提供了一段简介:

  • 单个 Node.js 实例在单线程环境下运行。为了更好地利用多核环境,用户有时希望启动一批 Node.js 进程用于加载。
  • 集群化模块使得你很方便地创建子进程,以便于在服务端口之间共享。说白了就是多个子进程共享一个端口。

Cluster简单的说就是:

  1. 可以在服务器上同时启动多个进程。
  2. 这些进程同时监听同一个端口。跑的也是同一份源代码。
  3. 有一个专门负责启动其他进程的Master进程(包工头),它只负责启动其他进程。
  4. 而被启动的线程则叫做 Worker 进程,是真正干活的工人。接收请求,提供对外服务。而启动的Worker进程数量一般由服务器的CPU核数来决定。

即前面的小章节,都是建立在单个Worker的基础上来完成的。每个Worker都有属于他自己的一套事件队列以及底层的LIBUV库线程池。

现在有两个问题:

  1. Cluster 是如何做到多个进程共享同一个端口的?
  2. Master 是如何将接收到的请求传递给worker然后去执行的?

源码的详解可以参考:源码解析

原理如下:

  1. 主线程负责监听指定的端口,并接收来自客户端的请求。
  2. 主线程接收到请求后,将请求通过轮询负载均衡的方式丢给可用的Worker进程来处理。
  3. 主进程和 Worker 进程之间通过 IPC(进程间通信)来传递连接和其他信息。主进程将接收到的连接分配给 Worker 进程,并将连接的套接字(socket)传递给对应的 Worker 进程。Worker 进程接收到连接后,可以像单个 Node.js 应用程序一样处理连接。执行完业务逻辑处理之后,返回。
  4. 通过这种方式,多个 Worker 进程可以共享同一个端口,并且能够处理并发的连接请求。

三. 总结

总结下NodeJs的单线程模型和高并发处理原理,假设我们有一台4核CPU的机器。

  • NodeJs会创建出一个Master进程,它负责创建Worker进程,数量一般会和服务器的核数一致,也就是4个Worker进程。
  • 外部的用户请求到服务器,会由Master进程进行轮询负载均衡丢给Worker进程来处理。
  • 同时Master会通过IPC通信,传递相关的信息给Worker进程,让他们能够正确地处理用户请求。

这里是nodejs对多核服务的一个利用机制。每个CPU同一时间可以处理一个用户请求。

那再说一下单个Worker进程的处理:

  1. Worker 进程开始处理当前用户请求。这个请求过程中遇到的任何一个IO请求(事件),统一丢给Event Queue事件队列中,而非立即执行。
  2. 主线程(JS单线程)处理完毕之后(即你的代码已经跑完了,但是期间涉及到的异步任务还没执行完毕),会进行事件循环,不断地访问事件队列中的队列,依次取出,将它丢给LIBUV库中维护的线程池。
  3. 最终的异步任务则由底层的线程池来完成,完成好后通知给主线程,主线程通过回调机制拿到结果。
  4. 从而实现了主线程的非阻塞异步IO
<think>好的,用户想了解Node.js如何通过单线程模型处理高并发请求,特别是事件循环机制。首先,我需要回忆一下Node.js的基本架构。根据引用[1],Node.js主线程是单线程运行V8JavaScript,而子线程通过事件循环调度。这说明主线程处理代码执行,而异步操作交给子线程。 接下来,用户提到的高并发处理。引用[2]指出Node.js虽然单线程,但能有效处理高并发,主要通过事件循环、非阻塞I/O线程池。需要详细说明这几个部分。事件循环是关键,根据引用[4],主线程执行完代码后,会检查事件队列,处理异步事件。这里需要解释事件队列事件循环的流程,比如分阶段执行不同类型的回调。 非阻塞I/O也很重要,主线程遇到I/O操作时,不会等待而是继续执行,等I/O完成后通过回调处理。引用[4]提到异步操作会被放到事件队列,主线程之后处理。线程池部分,Node.js内部使用libuv,有固定数量的线程处理文件I/O等任务,避免阻塞主线程。 用户可能对事件循环的具体阶段不太清楚,需要分阶段解释,比如timers、pending callbacks等。同时,要强调主线程不会被阻塞,通过事件循环不断处理事件。另外,引用[3][4]说明主线程只有一个执行栈,事件队列线程池的协作机制。 需要确保回答结构清晰,分点说明机制,然后给出相关问题引用。可能的问题包括事件循环阶段、回调执行顺序、优缺点等。要检查引用是否正确,比如引用[4]详细描述了事件队列循环的步骤,需要正确标注。</think>### Node.js单线程高并发处理机制 Node.js单线程高并发处理机制主要依赖于**事件循环(Event Loop)****非阻塞I/O模型**,结合**线程池**实现高效的任务调度。以下是具体实现原理的分步说明: --- #### 1. **单线程主循环** Node.js主线程仅负责执行JavaScript代码协调任务调度[^3]。所有同步代码直接在主线程执行,例如: ```javascript console.log("主线程执行"); ``` 但遇到I/O操作(如文件读写、网络请求)时,主线程不会阻塞等待结果,而是将任务交给系统内核或线程池处理[^4]。 --- #### 2. **非阻塞I/O操作** 当主线程遇到异步操作时(如`fs.readFile`或`http.request`): - 将任务交给底层库(如`libuv`)的线程池执行。 - 主线程继续执行后续代码,**不会被阻塞**。 - 操作完成后,结果通过回调函数放入**事件队列(Event Queue)**。 例如: ```javascript fs.readFile('file.txt', (err, data) => { console.log("异步回调执行"); }); console.log("主线程继续执行"); ``` 输出顺序为: ``` 主线程继续执行 异步回调执行 ``` --- #### 3. **事件循环的分阶段调度** 事件循环按固定顺序分阶段处理不同类型任务[^4]: 1. **Timers阶段**:执行`setTimeout``setInterval`回调。 2. **Pending Callbacks**:处理系统操作(如TCP错误)的回调。 3. **Idle/Prepare**:Node.js内部使用。 4. **Poll阶段**:检索新的I/O事件并执行回调(如文件读取完成)。 5. **Check阶段**:执行`setImmediate`回调。 6. **Close Callbacks**:处理关闭事件(如`socket.on('close')`)。 每个阶段执行完所有任务后,才会进入下一阶段。 --- #### 4. **线程池的辅助** Node.js通过`libuv`库维护一个**固定大小的线程池(默认4个线程)**,用于处理以下任务: - 文件系统操作(如`fs`模块) - DNS解析 - CPU密集型计算(如`crypto`模块) 线程池完成任务后,将回调推入事件队列,等待主线程调度。 --- #### 5. **高并发处理示例** 假设同时有1000个HTTP请求到达: 1. 主线程接收请求后,将网络I/O交给操作系统内核处理。 2. 内核完成TCP连接后,通知事件循环将回调放入Poll阶段队列。 3. 主线程在Poll阶段依次执行这些回调,生成响应。 整个过程**仅需一个主线程协调**,避免了多线程的上下文切换开销[^2]。 --- ### 核心优势与限制 | 优势 | 限制 | |------|------| | 减少内存消耗(无线程切换) | 不适合CPU密集型任务 | | 简化并发编程模型 | 单个错误可能导致整个进程崩溃 | | 高I/O吞吐量 | 回调嵌套可能导致“回调地狱” | ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

第七个香粽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值