淺析Node高併發的原理
我們先來看幾個常見的說法
- nodejs是單執行緒非阻塞I/O模型
- nodejs適合高併發
- nodejs適合I/O密集型應用,不適合CPU密集型應用 【相關教學推薦:nodejs影片教學】
在具體分析這幾個說法是不是、為什麼之前,我們先來做一些準備工作
從頭聊起
一個常見web應用程式會做哪些事情
- 運算(執行業務邏輯、數學運算、函數呼叫等。主要工作在CPU進行)
- I/O(如讀寫檔案、讀寫資料庫、讀寫網絡請求等。主要工作在各種I/O設備,如磁碟、網卡等)
#一個典型的傳統web應用實作
- 多進程,一個請求fork一個(子)進程阻塞I/O(即blocking I/O或BIO)
- 多線程,一個請求創建一個線程阻塞I/O
多進程web應用範例偽代碼
listenFd = new Socket(); // 创建监听socket Bind(listenFd, 80); // 绑定端口 Listen(listenFd); // 开始监听 for ( ; ; ) { // 接收客户端请求,通过新的socket建立连接 connFd = Accept(listenFd); // fork子进程 if ((pid = Fork()) === 0) { // 子进程中 // BIO读取网络请求数据,阻塞,发生进程调度 request = connFd.read(); // BIO读取本地文件,阻塞,发生进程调度 content = ReadFile('test.txt'); // 将文件内容写入响应 Response.write(content); } }
多執行緒應用實際上和多進程類似,只不過將一個請求分配一個進程換成了一個請求分配一個執行緒。執行緒對比進程更輕量,在系統資源佔用上更少,上下文切換(ps:所謂上下文切換,稍微解釋一下:單核心CPU的情況下同一時間只能執行一個進程或執行緒中的任務,而為了宏觀上的並行,則需要在多個進程或線程之間按時間片來回切換以保證各進、線程都有機會被執行)的開銷也更小;同時線程間更容易共享內存,便於開發
上文中提到了web應用的兩個核心要點,一個是進(線)程模型,一個是I/O模型。那阻塞I/O到底是什麼?又有哪些其他的I/O模型呢?別急,首先我們看一下什麼是阻塞
什麼是阻塞?什麼是阻塞I/O?
簡而言之,阻塞是指函數呼叫返回之前,當前進(線)程會被掛起,進入等待狀態,在這個狀態下,當前進(線)程暫停運行,引起CPU的進(線)程調度。函數只有在內部工作全部執行完成後才會回傳給呼叫者
所以阻塞I/O是,應用程式透過API呼叫I/O操作後,當前進(線)程將會進入等待狀態,程式碼無法繼續往下執行,這時CPU可以進行進(線)程調度,即切換到其他可執行的進(線)程繼續執行,當前進(線)程在底層I/O請求處理完後才會回傳並且可以繼續執行
多進(線)程阻塞I/O模型有什麼問題?
在了解了什麼是阻塞和阻塞I/O後,我們來分析一下傳統web應用多進(線)程 阻塞I/O模型有什麼弊端。
因為一個請求需要分配一個進(線)程,這樣的系統在並發量大時需要維護大量進(線)程,且需要進行大量的上下文切換,這都需要大量的CPU、記憶體等系統資源支撐,所以在高並發請求進來時CPU和記憶體開銷會急劇上升,可能會迅速拖垮整個系統導致服務不可用
##nodejs應用實作
#接下來我們來看看nodejs應用程式是如何實現的。- 事件驅動,單執行緒(主執行緒)
- #非阻塞I/O 在官網上可以看到,nodejs最主要的兩大特點,一個是單線程事件驅動,一個是「非阻塞」I/O模型。單線程 事件驅動比較好理解,前端同學應該都很熟悉js的單線程和事件循環這套機制了,那我們主要來研究一下這個“非阻塞I/O”是怎麼一回事。首先來看一段nodejs服務端應用常見的程式碼,
const net = require('net'); const server = net.createServer(); const fs = require('fs'); server.listen(80); // 监听端口 // 监听事件建立连接 server.on('connection', (socket) => { // 监听事件读取请求数据 socket.on('data', (data) => { // 异步读取本地文件 fs.readFile('test.txt', (err, data) => { // 将读取的内容写入响应 socket.write(data); socket.end(); }) }); });
read操作基本步驟
首先看下一個read操作需要經歷哪些步驟- 用户程序调用I/O操作API,内部发出系统调用,进程从用户态转到内核态
- 系统发出I/O请求,等待数据准备好(如网络I/O,等待数据从网络中到达socket;等待系统从磁盘上读取数据等)
- 数据准备好后,复制到内核缓冲区
- 从内核空间复制到用户空间,用户程序拿到数据
接下来我们看一下操作系统中有哪些I/O模型
几种I/O模型
阻塞式I/O
非阻塞式I/O
I/O多路复用(进程可同时监听多个I/O设备就绪)
信号驱动I/O
异步I/O
那么nodejs里到底使用了哪种I/O模型呢?是上图中的“非阻塞I/O”吗?别着急,先接着往下看,我们来了解下nodejs的体系结构
nodejs体系结构,线程、I/O模型分析
最上面一层是就是我们编写nodejs应用代码时可以使用的API库,下面一层则是用来打通nodejs和它所依赖的底层库的一个中间层,比如实现让js代码可以调用底层的c代码库。来到最下面一层,可以看到前端同学熟悉的V8,还有其他一些底层依赖。注意,这里有一个叫libuv的库,它是干什么的呢?从图中也能看出,libuv帮助nodejs实现了底层的线程池、异步I/O等功能。libuv实际上是一个跨平台的c语言库,它在windows、linux等不同平台下会调用不同的实现。我这里主要分析linux下libuv的实现,因为我们的应用大部分时候还是运行在linux环境下的,且平台间的差异性并不会影响我们对nodejs原理的分析和理解。好了,对于nodejs在linux下的I/O模型来说,libuv实际上提供了两种不同场景下的不同实现,处理网络I/O主要由epoll函数实现(其实就是I/O多路复用,在前面的图中使用的是select函数来实现I/O多路复用,而epoll可以理解为select函数的升级版,这个暂时不做具体分析),而处理文件I/O则由多线程(线程池) + 阻塞I/O模拟异步I/O实现
下面是一段我写的nodejs底层实现的伪代码帮助大家理解
listenFd = new Socket(); // 创建监听socket Bind(listenFd, 80); // 绑定端口 Listen(listenFd); // 开始监听 for ( ; ; ) { // 阻塞在epoll函数上,等待网络数据准备好 // epoll可同时监听listenFd以及多个客户端连接上是否有数据准备就绪 // clients表示当前所有客户端连接,curFd表示epoll函数最终拿到的一个就绪的连接 curFd = Epoll(listenFd, clients); if (curFd === listenFd) { // 监听套接字收到新的客户端连接,创建套接字 int connFd = Accept(listenFd); // 将新建的连接添加到epoll监听的list clients.push(connFd); } else { // 某个客户端连接数据就绪,读取请求数据 request = curFd.read(); // 这里拿到请求数据后可以发出data事件进入nodejs的事件循环 ... } } // 读取本地文件时,libuv用多线程(线程池) + BIO模拟异步I/O ThreadPool.run((callback) => { // 在线程里用BIO读取文件 String content = Read('text.txt'); // 发出事件调用nodejs提供的callback });
通过I/O多路复用 + 多线程模拟的异步I/O配合事件循环机制,nodejs就实现了单线程处理并发请求并且不会阻塞。所以回到之前所说的“非阻塞I/O”模型,实际上nodejs并没有直接使用通常定义上的非阻塞I/O模型,而是I/O多路复用模型 + 多线程BIO。我认为“非阻塞I/O”其实更多是对nodejs编程人员来说的一种描述,从编码方式和代码执行顺序上来讲,nodejs的I/O调用的确是“非阻塞”的
总结
至此我们应该可以了解到,nodejs的I/O模型其实主要是由I/O多路复用和多线程下的阻塞I/O两种方式一起组成的,而应对高并发请求时发挥作用的主要就是I/O多路复用。好了,那最后我们来总结一下nodejs线程模型和I/O模型对比传统web应用多进(线)程 + 阻塞I/O模型的优势和劣势
- nodejs利用單執行緒模型省去了系統維護和切換多進(線)程的開銷,同時多路復用的I/O模型可以讓nodejs的單執行緒不會阻塞在某一個連接上。在高並發場景下,nodejs應用程式只需要創建和管理多個客戶端連接對應的socket描述符而不需要創建對應的進程或線程,系統開銷上大大減少,所以能同時處理更多的客戶端連接
- nodejs並不能提升底層真正I/O操作的效率。如果底層I/O成為系統的效能瓶頸,nodejs依然無法解決,即nodejs可以接收高並發請求,但如果需要處理大量慢I/O操作(例如讀寫磁碟),仍可能造成系統資源過載。所以高並發並不能簡單的透過單執行緒非阻塞I/O模型來解決
- CPU密集型應用程式可能會讓nodejs的單執行緒模型成為效能瓶頸
- nodejs適合高並發處理少量業務邏輯或快I/O(例如讀寫記憶體)
更多node相關知識,請造訪:nodejs 教學!
以上是淺析Node高併發的原理的詳細內容。更多資訊請關注PHP中文網其他相關文章!

熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

記事本++7.3.1
好用且免費的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強大的PHP整合開發環境

Dreamweaver CS6
視覺化網頁開發工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

要連接 MySQL 資料庫,需要遵循以下步驟:安裝 mysql2 驅動程式。使用 mysql2.createConnection() 建立連接對象,其中包含主機位址、連接埠、使用者名稱、密碼和資料庫名稱。使用 connection.query() 執行查詢。最後使用 connection.end() 結束連線。

Node.js 中存在以下全域變數:全域物件:global核心模組:process、console、require執行階段環境變數:__dirname、__filename、__line、__column常數:undefined、null、NaN、Infinity、-Infinity

Node.js 安裝目錄中有兩個與 npm 相關的文件:npm 和 npm.cmd,區別如下:擴展名不同:npm 是可執行文件,npm.cmd 是命令視窗快捷方式。 Windows 使用者:npm.cmd 可以在命令提示字元中使用,npm 只能從命令列執行。相容性:npm.cmd 特定於 Windows 系統,npm 跨平台可用。使用建議:Windows 使用者使用 npm.cmd,其他作業系統使用 npm。

PiNetwork節點詳解及安裝指南本文將詳細介紹PiNetwork生態系統中的關鍵角色——Pi節點,並提供安裝和配置的完整步驟。 Pi節點在PiNetwork區塊鏈測試網推出後,成為眾多先鋒積極參與測試的重要環節,為即將到來的主網發布做準備。如果您還不了解PiNetwork,請參考Pi幣是什麼?上市價格多少? Pi用途、挖礦及安全性分析。什麼是PiNetwork? PiNetwork項目始於2019年,擁有其專屬加密貨幣Pi幣。該項目旨在創建一個人人可參與

Node.js 和 Java 的主要差異在於設計和特性:事件驅動與執行緒驅動:Node.js 基於事件驅動,Java 基於執行緒驅動。單執行緒與多執行緒:Node.js 使用單執行緒事件循環,Java 使用多執行緒架構。執行時間環境:Node.js 在 V8 JavaScript 引擎上運行,而 Java 在 JVM 上運行。語法:Node.js 使用 JavaScript 語法,而 Java 使用 Java 語法。用途:Node.js 適用於 I/O 密集型任務,而 Java 適用於大型企業應用程式。

是的,Node.js可用於前端開發,主要優勢包括高效能、豐富的生態系統和跨平台相容性。需要考慮的注意事項有學習曲線、工具支援和社群規模較小。
