Node.js 单线程模型探索
Node.js采用事件驱动和异步I/O的方式,实现了单线程、高并发的JavaScript运行环境。既然单线程意味着一次只能做一件事,那么Node.js如何通过一个线程实现高并发和异步I/O呢?本文将围绕这个问题探讨 Node.js 的单线程模型。
高并发策略
一般来说,高并发的解决方案是提供多线程模型。服务器为每个客户端请求分配一个线程并使用同步 I/O。系统通过线程切换来弥补同步I/O调用的时间成本。例如,Apache就使用这种策略。由于 I/O 操作通常非常耗时,因此这种方法很难获得高性能。不过,它非常简单,可以实现复杂的交互逻辑。
事实上,大多数 Web 服务器端并不执行太多计算。接收到请求后,将请求传递给其他服务(例如读取数据库),然后等待结果返回,最后将结果发送给客户端。因此,Node.js 使用单线程模型来处理这种情况。它不是为每个传入的请求分配一个线程,而是使用一个主线程来处理所有请求,然后异步处理 I/O 操作,避免了创建、销毁线程以及线程之间切换的开销和复杂性。
事件循环
Node.js 在主线程中维护一个事件队列。当收到请求时,它会作为事件添加到此队列中,然后继续接收其他请求。当主线程空闲时(没有请求传入),它开始循环遍历事件队列以检查是否有事件需要处理。有两种情况:对于非I/O任务,主线程会直接处理,并通过回调函数返回上层;对于I/O任务,它会从线程池中取出一个线程来处理事件,指定一个回调函数,然后继续循环队列中的其他事件。
一旦线程中的I/O任务完成,就执行指定的回调函数,并将完成的事件放在事件队列的末尾,等待事件循环。当主线程再次循环到这个事件时,直接处理并返回给上层。这个过程称为Event Loop,其运行原理如下图所示:
该图展示了Node.js的整体运行原理。 Node.js 从左到右、从上到下分为四层:应用层、V8 引擎层、Node API 层、LIBUV 层。
- 应用层:是JavaScript交互层。常见的例子是 Node.js 模块,例如 http 和 fs。
- V8引擎层:使用V8引擎解析JavaScript语法,然后与下层API交互。
- Node API层:为上层模块提供系统调用,通常用C实现,与操作系统交互。
- LIBUV Layer:是跨平台的底层封装,实现事件循环、文件操作等,是Node.js实现异步的核心。
无论是Linux平台还是Windows平台,Node.js内部都使用线程池来完成异步I/O操作,LIBUV统一了不同平台差异的调用。所以,Node.js 中的单线程仅意味着 JavaScript 在单线程中运行,而不是 Node.js 整体是单线程的。
工作原理
Node.js 实现异步的核心在于事件。也就是说,它将每个任务视为一个事件,然后通过事件循环来模拟异步效果。为了更具体、更清楚地理解和接受这个事实,我们下面用伪代码来描述它的工作原理。
1. 定义事件队列
由于它是一个队列,所以它是先进先出(FIFO)的数据结构。我们用JS数组来描述,如下:
/** * Define the event queue * Enqueue: push() * Dequeue: shift() * Empty queue: length === 0 */ let globalEventQueue = [];
我们用数组来模拟队列结构:数组的第一个元素是队列的头,最后一个元素是队列的尾部。 push() 在队列尾部插入一个元素,shift() 从队列头部删除一个元素。这样就实现了一个简单的事件队列。
2.定义请求接收入口
每个请求都会被拦截并进入处理函数,如下图:
/** * Receive user requests * Every request will enter this function * Pass parameters request and response */ function processHttpRequest(request, response) { // Define an event object let event = createEvent({ params: request.params, // Pass request parameters result: null, // Store request results callback: function() {} // Specify a callback function }); // Add the event to the end of the queue globalEventQueue.push(event); }
该函数只是将用户的请求封装为一个事件,放入队列中,然后继续接收其他请求。
3. 定义事件循环
当主线程空闲时,开始循环事件队列。所以我们需要定义一个函数来循环事件队列:
/** * The main body of the event loop, executed by the main thread when appropriate * Loop through the event queue * Handle non-IO tasks * Handle IO tasks * Execute callbacks and return to the upper layer */ function eventLoop() { // If the queue is not empty, continue to loop while (this.globalEventQueue.length > 0) { // Take an event from the head of the queue let event = this.globalEventQueue.shift(); // If it's a time-consuming task if (isIOTask(event)) { // Take a thread from the thread pool let thread = getThreadFromThreadPool(); // Hand it over to the thread to handle thread.handleIOTask(event); } else { // After handling non-time-consuming tasks, directly return the result let result = handleEvent(event); // Finally, return to V8 through the callback function, and then V8 returns to the application event.callback.call(null, result); } } }
主线程持续监听事件队列。对于I/O任务,它交给线程池处理,对于非I/O任务,它自己处理并返回。
4. 处理I/O任务
线程池收到任务后,直接处理I/O操作,比如读取数据库:
/** * Define the event queue * Enqueue: push() * Dequeue: shift() * Empty queue: length === 0 */ let globalEventQueue = [];
当I/O任务完成时,执行回调,将请求结果存储到事件中,并将事件放回到队列中,等待循环。最后,当前线程被释放。当主线程再次循环到该事件时,直接处理。
总结上面的过程,我们发现Node.js只使用一个主线程来接收请求。接收到请求后,并不直接处理,而是将其放入事件队列中,然后继续接收其他请求。当它空闲时,它通过事件循环处理这些事件,从而达到异步的效果。当然,对于I/O任务,还是需要依赖系统层面的线程池来处理。
因此,我们可以简单地理解为 Node.js 本身是一个多线程平台,但它在单线程中处理 JavaScript 级别的任务。
CPU 密集型任务是一个缺点
到现在为止,我们应该对 Node.js 的单线程模型有了一个简单清晰的认识。它通过事件驱动模型实现高并发和异步I/O。然而,Node.js 也有不擅长的地方。
如上所述,对于I/O任务,Node.js将其交给线程池进行异步处理,高效且简单。因此,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 密集型任务。
应用场景
- RESTful API:请求和响应只需要少量文本,不需要太多逻辑处理。因此,可以并发处理数万个连接。
- 聊天服务:轻量级,流量大,没有复杂的计算逻辑。
Leapcell:用于 Web 托管、异步任务和 Redis 的下一代无服务器平台
最后介绍一下最适合部署Node.js服务的平台:Leapcell。
1. 多语言支持
- 使用 JavaScript、Python、Go 或 Rust 进行开发。
2.免费部署无限个项目
- 只需支付使用费用——无请求,不收费。
3. 无与伦比的成本效益
- 即用即付,无闲置费用。
- 示例:25 美元支持 694 万个请求,平均响应时间为 60 毫秒。
4.简化的开发者体验
- 直观的用户界面,轻松设置。
- 完全自动化的 CI/CD 管道和 GitOps 集成。
- 实时指标和日志记录以获取可行的见解。
5. 轻松的可扩展性和高性能
- 自动扩展,轻松处理高并发。
- 零运营开销——只需专注于构建。
在文档中探索更多内容!
Leapcell Twitter:https://x.com/LeapcellHQ
以上是Node.js 事件循环内部:深入探究的详细内容。更多信息请关注PHP中文网其他相关文章!

JavaScript字符串替换方法详解及常见问题解答 本文将探讨两种在JavaScript中替换字符串字符的方法:在JavaScript代码内部替换和在网页HTML内部替换。 在JavaScript代码内部替换字符串 最直接的方法是使用replace()方法: str = str.replace("find","replace"); 该方法仅替换第一个匹配项。要替换所有匹配项,需使用正则表达式并添加全局标志g: str = str.replace(/fi

本教程向您展示了如何将自定义的Google搜索API集成到您的博客或网站中,提供了比标准WordPress主题搜索功能更精致的搜索体验。 令人惊讶的是简单!您将能够将搜索限制为Y

利用轻松的网页布局:8个基本插件 jQuery大大简化了网页布局。 本文重点介绍了简化该过程的八个功能强大的JQuery插件,对于手动网站创建特别有用

因此,在这里,您准备好了解所有称为Ajax的东西。但是,到底是什么? AJAX一词是指用于创建动态,交互式Web内容的一系列宽松的技术。 Ajax一词,最初由Jesse J创造

核心要点 JavaScript 中的 this 通常指代“拥有”该方法的对象,但具体取决于函数的调用方式。 没有当前对象时,this 指代全局对象。在 Web 浏览器中,它由 window 表示。 调用函数时,this 保持全局对象;但调用对象构造函数或其任何方法时,this 指代对象的实例。 可以使用 call()、apply() 和 bind() 等方法更改 this 的上下文。这些方法使用给定的 this 值和参数调用函数。 JavaScript 是一门优秀的编程语言。几年前,这句话可

该帖子编写了有用的作弊表,参考指南,快速食谱以及用于Android,BlackBerry和iPhone应用程序开发的代码片段。 没有开发人员应该没有他们! 触摸手势参考指南(PDF) Desig的宝贵资源

jQuery是一个很棒的JavaScript框架。但是,与任何图书馆一样,有时有必要在引擎盖下发现发生了什么。也许是因为您正在追踪一个错误,或者只是对jQuery如何实现特定UI感到好奇


热AI工具

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

AI Hentai Generator
免费生成ai无尽的。

热门文章

热工具

安全考试浏览器
Safe Exam Browser是一个安全的浏览器环境,用于安全地进行在线考试。该软件将任何计算机变成一个安全的工作站。它控制对任何实用工具的访问,并防止学生使用未经授权的资源。

PhpStorm Mac 版本
最新(2018.2.1 )专业的PHP集成开发工具

MinGW - 适用于 Windows 的极简 GNU
这个项目正在迁移到osdn.net/projects/mingw的过程中,你可以继续在那里关注我们。MinGW:GNU编译器集合(GCC)的本地Windows移植版本,可自由分发的导入库和用于构建本地Windows应用程序的头文件;包括对MSVC运行时的扩展,以支持C99功能。MinGW的所有软件都可以在64位Windows平台上运行。

WebStorm Mac版
好用的JavaScript开发工具

mPDF
mPDF是一个PHP库,可以从UTF-8编码的HTML生成PDF文件。原作者Ian Back编写mPDF以从他的网站上“即时”输出PDF文件,并处理不同的语言。与原始脚本如HTML2FPDF相比,它的速度较慢,并且在使用Unicode字体时生成的文件较大,但支持CSS样式等,并进行了大量增强。支持几乎所有语言,包括RTL(阿拉伯语和希伯来语)和CJK(中日韩)。支持嵌套的块级元素(如P、DIV),