我們先來看幾個常見的說法
- 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(); }) }); });可以看到在nodejs中,我們可以以非同步的方式去進行I/O操作,透過API呼叫I/O操作後會馬上返回,緊接著就可以繼續執行其他程式碼邏輯,那為什麼nodejs中的I/O是「非阻塞」的呢?回答這個問題之前我們再做一些準備工作,參考nodejs進階影片講解:
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中文網其他相關文章!

我使用您的日常技術工具構建了功能性的多租戶SaaS應用程序(一個Edtech應用程序),您可以做同樣的事情。 首先,什麼是多租戶SaaS應用程序? 多租戶SaaS應用程序可讓您從唱歌中為多個客戶提供服務

本文展示了與許可證確保的後端的前端集成,並使用Next.js構建功能性Edtech SaaS應用程序。 前端獲取用戶權限以控制UI的可見性並確保API要求遵守角色庫

JavaScript是現代Web開發的核心語言,因其多樣性和靈活性而廣泛應用。 1)前端開發:通過DOM操作和現代框架(如React、Vue.js、Angular)構建動態網頁和單頁面應用。 2)服務器端開發:Node.js利用非阻塞I/O模型處理高並發和實時應用。 3)移動和桌面應用開發:通過ReactNative和Electron實現跨平台開發,提高開發效率。

JavaScript的最新趨勢包括TypeScript的崛起、現代框架和庫的流行以及WebAssembly的應用。未來前景涵蓋更強大的類型系統、服務器端JavaScript的發展、人工智能和機器學習的擴展以及物聯網和邊緣計算的潛力。

JavaScript是現代Web開發的基石,它的主要功能包括事件驅動編程、動態內容生成和異步編程。 1)事件驅動編程允許網頁根據用戶操作動態變化。 2)動態內容生成使得頁面內容可以根據條件調整。 3)異步編程確保用戶界面不被阻塞。 JavaScript廣泛應用於網頁交互、單頁面應用和服務器端開發,極大地提升了用戶體驗和跨平台開發的靈活性。

Python更适合数据科学和机器学习,JavaScript更适合前端和全栈开发。1.Python以简洁语法和丰富库生态著称,适用于数据分析和Web开发。2.JavaScript是前端开发核心,Node.js支持服务器端编程,适用于全栈开发。

JavaScript不需要安裝,因為它已內置於現代瀏覽器中。你只需文本編輯器和瀏覽器即可開始使用。 1)在瀏覽器環境中,通過標籤嵌入HTML文件中運行。 2)在Node.js環境中,下載並安裝Node.js後,通過命令行運行JavaScript文件。

如何在Quartz中提前發送任務通知在使用Quartz定時器進行任務調度時,任務的執行時間是由cron表達式設定的。現�...


熱AI工具

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

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

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

AI Hentai Generator
免費產生 AI 無盡。

熱門文章

熱工具

DVWA
Damn Vulnerable Web App (DVWA) 是一個PHP/MySQL的Web應用程序,非常容易受到攻擊。它的主要目標是成為安全專業人員在合法環境中測試自己的技能和工具的輔助工具,幫助Web開發人員更好地理解保護網路應用程式的過程,並幫助教師/學生在課堂環境中教授/學習Web應用程式安全性。 DVWA的目標是透過簡單直接的介面練習一些最常見的Web漏洞,難度各不相同。請注意,該軟體中

Safe Exam Browser
Safe Exam Browser是一個安全的瀏覽器環境,安全地進行線上考試。該軟體將任何電腦變成一個安全的工作站。它控制對任何實用工具的訪問,並防止學生使用未經授權的資源。

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

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

MantisBT
Mantis是一個易於部署的基於Web的缺陷追蹤工具,用於幫助產品缺陷追蹤。它需要PHP、MySQL和一個Web伺服器。請查看我們的演示和託管服務。