為了解決多線程服務(wù)器在高并發(fā)的I/O密集型應(yīng)用中的不足,同時避免早期簡單單線程服務(wù)器的性能障礙,Node.js采用了基于"事件循環(huán)"的非阻塞式單線程模型,實現(xiàn)了如下兩個目標(biāo):
(1)保證每個請求都可以快速響應(yīng);
(2)實現(xiàn)遠(yuǎn)超過多線程模型的并發(fā)連接數(shù)。
提示:Node.js在JS層面是單線程的——沒有創(chuàng)建新線程的機(jī)制。但是在底層的C/C++層面是多線程的,即訪問底層操作系統(tǒng)服務(wù)時,存在多個并發(fā)工作線程的情形——使用了線程池。
阻塞執(zhí)行
阻塞(Block):也稱為同步執(zhí)行(Synchronize),只有前面的操作全部執(zhí)行完成,才能開始后續(xù)操作。(常規(guī)的多線程服務(wù)器內(nèi)部都是采用的是阻塞執(zhí)行)
var conn=mysql.createConnection(...); //步驟1
var result=conn,query('INSERT...'); //步驟2
conn.end(); //步驟3
非阻塞執(zhí)行
非阻塞(Non—block):也稱為異步執(zhí)行(Asynchronize),后面的操作不必等待之前操作的執(zhí)行完畢,可以先執(zhí)行。
const fs=require('fs');
console.log('讀取請求數(shù)據(jù)'); //操作1
fs.writeFile('app.log','訪問日志',()=>{ //操作2
console.log('寫出操作日志');
})
console.log('輸出響應(yīng)數(shù)據(jù)'); //操作3
//讀取請求數(shù)據(jù)
//輸出響應(yīng)數(shù)據(jù)
//寫出操作日志
異步回調(diào)
Node.js中的業(yè)務(wù)代碼,都是在單一的主線程中執(zhí)行的;當(dāng)遇到耗時的阻塞操作時(如文件IO、網(wǎng)絡(luò)訪問、數(shù)據(jù)庫請求等),不會等待其執(zhí)行完畢,而是注冊一個處理函數(shù)執(zhí)行結(jié)果的回調(diào)函數(shù),繼續(xù)執(zhí)行后續(xù)的代碼。
待耗時的阻塞操作執(zhí)行完成時,其對應(yīng)的回調(diào)函數(shù)會轉(zhuǎn)入回調(diào)函數(shù)隊列,主線程在下次事件循環(huán)時會執(zhí)行這些回調(diào)函數(shù)。
同步函數(shù)調(diào)用
const fs=require('fs');
var data=fs.readFileSync('app.log');
console.log('文件內(nèi)容:',data);
console.log('程序執(zhí)行完成!');
//文件內(nèi)容:121212
//程序執(zhí)行完成!
異步函數(shù)調(diào)用
const fs=require('fs');
fs.readFile('app.log',function(err,data){
console.log('文件內(nèi)容:',data)
})
console.log('程序執(zhí)行完成!');
//程序執(zhí)行完成!
//文件內(nèi)容:121212
事件驅(qū)動
事件驅(qū)動編程是一種以事件為基礎(chǔ)的編程范式,其中程序的執(zhí)行是由事件的發(fā)生和相應(yīng)的事件處理程序來驅(qū)動的。讓我們詳細(xì)了解事件循環(huán)和異步編程,并結(jié)合示例說明。
事件循環(huán)
事件循環(huán)是事件驅(qū)動編程的核心機(jī)制之一,它負(fù)責(zé)監(jiān)聽和分發(fā)事件,并調(diào)用相應(yīng)的事件處理程序來響應(yīng)事件。事件循環(huán)在一個持續(xù)運行的循環(huán)中不斷地從事件隊列中獲取事件,并按照事件的順序依次處理。常見的事件循環(huán)模型是基于單線程的,每次只處理一個事件,避免了并發(fā)訪問的問題。
示例
const eventEmitter = require('events');
// 創(chuàng)建事件發(fā)射器
const emitter = new eventEmitter();
// 監(jiān)聽事件
emitter.on('event', (data) => {
console.log('Event received:', data);
});
// 觸發(fā)事件
emitter.emit('event', 'Hello, world!');
在這個示例中,我們創(chuàng)建了一個事件發(fā)射器,并使用on
方法來監(jiān)聽一個名為"event"的事件。當(dāng)"event"事件被觸發(fā)時,事件處理程序會被調(diào)用,并將事件數(shù)據(jù)傳遞給它。最后,我們使用emit
方法觸發(fā)了"event"事件,并傳遞了數(shù)據(jù)"Hello, world!"。這樣,事件處理程序會被調(diào)用,并輸出"Event received: Hello, world!"。
異步編程
異步編程是事件驅(qū)動編程中的重要概念,它允許程序在執(zhí)行耗時操作時不阻塞主線程,而是通過回調(diào)函數(shù)、Promise、async/await等機(jī)制來處理操作的結(jié)果。這樣可以提高程序的并發(fā)性能,避免了長時間的等待。
libuv
利用編譯好的libuv庫文件,我們可以開始寫一個簡單又經(jīng)典的例子: Hello world。
#include "stdio.h"
#include "uv.h"
int main() {
uv_loop_t *loop = uv_default_loop();
printf("hello libuv");
uv_run(loop, UV_RUN_DEFAULT);
}
event-loop線程
我們都知道線程是操作系統(tǒng)最基本的調(diào)度單元,而進(jìn)程是操作系統(tǒng)的最基本的資源分配單元,因此可以知道進(jìn)程其實是不能運行,能運行的是進(jìn)程中的線程。進(jìn)程僅僅是一個容器,包含了線程運行中所需要的數(shù)據(jù)結(jié)構(gòu)等信息。一個進(jìn)程創(chuàng)建時,操作系統(tǒng)會創(chuàng)建一個線程,這就是主線程,而其他的從線程,都要在主線程的代碼來創(chuàng)建,也就是由程序員來創(chuàng)建。因此每一個可執(zhí)行的運用程序都至少有一個線程
于是libuv一開始便啟動了event-loop線程,再在這個主線程上利用線程池去創(chuàng)建更多的線程。在event-loop線程中是一段while(1)
的死循環(huán)代碼,直到?jīng)]有活躍的句柄的時候才會退出,這個時候libuv進(jìn)程才被銷毀掉。清楚這點對于后面的學(xué)習(xí)至關(guān)重要。
Handle
中文翻譯為句柄,如[譯文]libuv設(shè)計思想概述一文所屬,整個libuv的實現(xiàn)都是基于Handle和Request。所以理解句柄以及l(fā)ibuv提供的所有句柄實例才能夠真的掌握libuv。按照原文所述,句柄是:
表示能夠在活動時執(zhí)行某些操作的長生命周期對象。
理解這句話的意思,首先我們抓住兩個關(guān)鍵詞:長生命周期、對象。Libuv所有的句柄都需要初始化,而初始化都會調(diào)用類似這種函數(shù):uv_xxx_init
。xxx
表示句柄的類型,在該函數(shù)中,會將傳入的形參handle
初始化,并賦值返回具體的對象,比如初始化tcp句柄:
... // 隨便截取一段初始化代碼
handle->tcp.serv.accept_reqs = NULL;
handle->tcp.serv.pending_accepts = NULL;
handle->socket = INVALID_SOCKET;
handle->reqs_pending = 0;
handle->tcp.serv.func_acceptex = NULL;
handle->tcp.conn.func_connectex = NULL;
handle->tcp.serv.processed_accepts = 0;
handle->delayed_error = 0
...
理解了句柄其實就是個對象,那么長生命周期要是怎樣的?還是以TCP句柄為例子,你在這個例子tcpserver.c中,可以看到后面tcp服務(wù)器的操作:綁定端口、監(jiān)聽端口都是基于tcp句柄,整個句柄存活于整個應(yīng)用程序,只要tcp服務(wù)器沒有掛掉就一直在,因此說是長生命周期的對象。
libuv提供的所有句柄如下:
接下去我們簡單介紹以下所有的Libuv的句柄
uv_handle_t
首先libuv有一個基本的handle, uv_handle_t
,libuv是所有其他handle的基本范式,任何handle都可以強(qiáng)轉(zhuǎn)為該類型,并且和該Handle相關(guān)的所有API都可以為其他handle使用。
libuv能否一直運行下去的前提是檢查是否有活躍的句柄存在,而檢查一個句柄是否活躍(可以使用方法uv_is_active(const uv_handle_t* handle)
檢查),根據(jù)句柄類型不同,其含義也不一樣:
-
uv_async_t
句柄總是活躍的并且不能停用,除非使用uv_close
關(guān)閉掉 -
uv_pipe_t
、uv_tcp_t
,uv_udp_t
等,這些牽扯到I/O的句柄一般也都是活躍 -
uv_check_t
,uv_idle_t
,uv_timer_t
等,當(dāng)這些句柄開始調(diào)用uv_check_start()
,uv_idle_start()
的時候也是活躍的。
而檢查哪些句柄活躍則可以使用這個方法:uv_print_active_handles(handle->loop, stderr);
以tcpserver.c為例子,我們啟動tcp服務(wù)器后,啟動一個定時器去打印存在的句柄,結(jié)果如下:
[-AI] async 0x10f78e9d8
[RA-] tcp 0x10f78e660
[RA-] timer 0x7ffee049d7c0
可以看到tcp的例子中一直存活的句柄是async、tcp、timer。它們前面中括號的標(biāo)志解釋如下:
R 表示該句柄被引用著
A 表示該句柄此時處于活躍狀態(tài)
I 表示該句柄是內(nèi)部使用的
uv_timer_t
顧名思義,Libuv的計時器,用來在將來某個時候調(diào)用對應(yīng)設(shè)置的回調(diào)函數(shù)。其調(diào)用時機(jī)是在整個輪詢的最最開始,后面我們會說到輪詢的整個步驟。
uv_idle_t
Idle句柄在每次循環(huán)迭代中運行一次給定的回調(diào),而且執(zhí)行順序是在prepare句柄
之前。
與prepare句柄
的顯著區(qū)別在于,當(dāng)存在活動的空閑句柄時,循環(huán)將執(zhí)行零超時輪詢,而不是阻塞I/O。
在uv_backend_timeout
方法中我們可以看到返回的輪詢I/O超時時間是0:
if (!QUEUE_EMPTY(&loop->idle_handles))
return 0;
idle句柄的回調(diào)一般用來執(zhí)行一些低優(yōu)先級的任務(wù)。
**注意:盡管名稱叫做“idle”,空閑句柄在每次循環(huán)迭代時都會調(diào)用它們的回調(diào)函數(shù),
而不是在循環(huán)實際上是“空閑”的時候。**
uv_prepare_t?
prepare句柄將在每次循環(huán)迭代中運行一次給定的回調(diào),而且是選擇在I/O輪詢之前。
問題是:libuv為什么要創(chuàng)造這么一種句柄?其實從名稱來猜測,libuv應(yīng)該是想提供一種方式讓你可以在輪詢I/O之前做些事情,然后在輪詢I/O之后使用check句柄進(jìn)行一些結(jié)果的校驗。
uv_check_t
check句柄將在每次循環(huán)迭代中運行一次給定的回調(diào),而且是選擇在I/O輪詢之后。其目的在上面已經(jīng)提過
uv_async_t
Async句柄允許用戶“喚醒”事件循環(huán),并在主線程(原文翻譯為another thread,其實不對)調(diào)用一開始注冊的回調(diào)。這里說的喚醒其實就是發(fā)送消息給主線程(event-loop線程),讓其可以執(zhí)行一開始注冊的回調(diào)了。
**注意:libuv會對`uv_async_send()`做一個聚合處理。
也就是說它并不會調(diào)用一次就執(zhí)行一次回調(diào)。**
我們使用thread.c為例子,使用uv_queue_work
和uv_async_send
來實踐,得到的結(jié)果打印如下:
// 打印出主進(jìn)程ID號和event-loop線程ID
I am the master process, processId => 90714
I am event loop thread => 0x7fff8c2d9380
// 這個是uv_queue_work執(zhí)行的回調(diào),
從線程ID可以看到回調(diào)函數(shù)是在線程池中的某個線程中執(zhí)行
I am work callback, calling in some thread in thread pool, pid=>90714
work_cb thread id 0x700001266000
// 這個是uv_queue_work執(zhí)行完回調(diào)后結(jié)束的回調(diào),
從線程ID可以看到這個回調(diào)已經(jīng)回到了主線程中執(zhí)行
I am after work callback, calling from event loop thread, pid=>90714
after_work_cb thread id 0x7fff8c2d9380
// 這個是uv_async_init的回調(diào),其觸發(fā)是因為在work callback中執(zhí)行了uv_async_send,
可以從0x700001266000得到驗證,該回調(diào)也是在主線程中執(zhí)行
I am async callback, calling from event loop thread, pid=>90714
async_cb thread id 0x7fff8c2d9380
I am receiving msg: This msg from another thread: 0x700001266000
uv_poll_t
Poll句柄用于監(jiān)視文件描述符的可讀性、可寫性和斷開連接,類似于poll(2)的目的。
Poll句柄的目的是支持集成外部庫,這些庫依賴于事件循環(huán)來通知套接字狀態(tài)的更改,比如c-ares
或libssh2
。不建議將uv_poll_t用于任何其他目的;因為像uv_tcp_t
、uv_udp_t
等提供了一個比uv_poll_t
更快、更可伸縮的實現(xiàn),尤其是在Windows上。
可能輪詢處理偶爾會發(fā)出信號,表明文件描述符是可讀或可寫的,即使它不是。因此,當(dāng)用戶試圖從fd讀取或?qū)懭霑r,應(yīng)該總是準(zhǔn)備再次處理EAGAIN錯誤或類似的EAGAIN錯誤。
同一個套接字不能有多個活躍的Poll句柄,因為這可能會導(dǎo)致libuv出現(xiàn)busyloop
或其他故障。
當(dāng)活躍的Poll句柄輪詢文件描述符時,用戶不應(yīng)關(guān)閉該文件描述符。否則可能導(dǎo)致句柄報告錯誤,但也可能開始輪詢另一個套接字。但是,可以在調(diào)用uv_poll_stop()
或uv_close()
之后立即安全地關(guān)閉fd。
下面羅列的是輪詢的事件類型:
enum uv_poll_event {
UV_READABLE = 1,
UV_WRITABLE = 2,
UV_DISCONNECT = 4,
UV_PRIORITIZED = 8
};
uv_signal_t
Signal句柄在每個事件循環(huán)的基礎(chǔ)上實現(xiàn)Unix風(fēng)格的信號處理。在udpserver.c中展示了Signal句柄的使用方式:
uv_signal_t signal_handle;
r = uv_signal_init(loop, &signal_handle);
CHECK(r, "uv_signal_init");
r = uv_signal_start(&signal_handle, signal_cb, SIGINT);
void signal_cb(uv_signal_t *handle, int signum) {
printf("signal_cb: recvd CTRL+C shutting down\n");
uv_stop(uv_default_loop()); //stops the event loop
}
關(guān)于Signal句柄有幾個點要知悉:
- 以編程方式調(diào)用
raise()
或abort()
觸發(fā)的信號不會被libuv檢測到;所以這些信號不會對應(yīng)的回調(diào)函數(shù)。 - SIGKILL和SIGSTOP是不可能被捕捉到的
- 通過libuv處理SIGBUS、SIGFPE、SIGILL或SIGSEGV會導(dǎo)致未定義的行為
uv_process_t
process句柄將會新建一個新的進(jìn)程并且能夠允許用戶控制該進(jìn)程并使用流去建立通信通道。對應(yīng)的demo可以查看:process.c,值得注意的是,args
中提供的結(jié)構(gòu)體的第一個參數(shù)path指的是可執(zhí)行程序的路徑,比如在demo中:
const char* exepath = exepath_for_process();
char *args[3] = { (char*) exepath, NULL, NULL };
實例中的exepath是:FsHandle
的執(zhí)行路徑。
另外一個注意點就是父子進(jìn)程的std的配置,demo中提供了一些參考,如果使用管道的話還可以參考另外一個demo:pipe
?uv_stream_t
流句柄提供了雙工通信通道的抽象。uv_stream_t
是一種抽象類型,libuv以uv_tcp_t
、uv_pipe_t
和uv_tty_t
的形式提供了3種流實現(xiàn)。這個沒有具體實例。但是libuv有好幾個方法的入?yún)⒍际?code>uv_stream_t,說明這些方法都是可以被tcp/pipe/tty
使用,具體有:
int uv_shutdown(uv_shutdown_t* req, uv_stream_t* handle, uv_shutdown_cb cb)
int uv_listen(uv_stream_t* stream, int backlog, uv_connection_cb cb)
int uv_accept(uv_stream_t* server, uv_stream_t* client)
int uv_read_start(uv_stream_t* stream, uv_alloc_cb alloc_cb, uv_read_cb read_cb)
int uv_read_stop(uv_stream_t*)
int uv_write(uv_write_t* req, uv_stream_t* handle, const uv_buf_t bufs[], unsigned int nbufs, uv_write_cb cb)
int uv_write2(uv_write_t* req, uv_stream_t* handle, const uv_buf_t bufs[], unsigned int nbu
uv_tcp_t
tcp句柄可以用來表示TCP流和服務(wù)器。上小節(jié)說到的uv_stream_t
是uv_tcp_t
的”父類“,這里使用結(jié)構(gòu)體繼承的方式實現(xiàn),uv_handle_t
、uv_stream_t
、uv_tcp_t
三者的結(jié)構(gòu)關(guān)系如下圖:
使用libuv創(chuàng)建tcp服務(wù)器的步驟可以歸納為:
1、初始化uv_tcp_t: uv_tcp_init(loop, &tcp_server)
2、綁定地址:uv_tcp_bind
3、監(jiān)聽連接:uv_listen
4、每當(dāng)有一個連接進(jìn)來之后,調(diào)用uv_listen的回調(diào),回調(diào)里要做如下事情:
4.1、初始化客戶端的tcp句柄:uv_tcp_init()
4.2、接收該客戶端的連接:uv_accept()
4.3、開始讀取客戶端請求的數(shù)據(jù):uv_read_start()
4.4、讀取結(jié)束之后做對應(yīng)操作,如果需要響應(yīng)客戶端數(shù)據(jù),調(diào)用uv_write,回寫數(shù)據(jù)即可。
uv_pipe_t
?
Pipe句柄在Unix上提供了對本地域套接字的抽象,在Windows上提供了命名管道。它是uv_stream_t
的“子類”。管道的用途很多,可以用來讀寫文件,還可以用來做線程間的通信。我們在實例中用來實現(xiàn)主線程與多個子線程的互相通信。實現(xiàn)的模型是這樣的:
uv_udp_t
UDP句柄為客戶端和服務(wù)器封裝UDP通信。使用libuv創(chuàng)建udp服務(wù)器的步驟可以概括為:
1、初始化接收端的uv_udp_t: uv_udp_init(loop, &receive_socket_handle)
2、綁定地址:uv_udp_bind
3、開始接收消息:uv_udp_recv_start
4、uv_udp_recv_start里執(zhí)行回調(diào),可以使用下面方法回寫數(shù)據(jù)發(fā)送給客戶端
4.1、uv_udp_init初始化send_socket_handle
4.2、uv_udp_bind綁定發(fā)送者的地址,地址可以從recv獲取
4.3、uv_udp_send發(fā)送指定消息
uv_fs_event_t
FS事件句柄允許用戶監(jiān)視一個給定的路徑的更新事件,例如,如果文件被重命名或其中有一個通用更改。這個句柄使用每個平臺上最佳的解決方案。
?
uv_fs_poll_t
FS輪詢句柄允許用戶監(jiān)視給定的更改路徑。與uv_fs_event_t
不同,fs poll句柄使用stat
檢測文件何時發(fā)生了更改,這樣它們就可以在不支持fs事件句柄的文件系統(tǒng)上工作。
Request
那么接下去就說到Request這個短生命周期的概念,中文翻譯為”請求“,類似于nodejs中的req,它也是一個結(jié)構(gòu)體。還是以上述的tcp服務(wù)器為例子,有這么一段代碼:
if (r < 0) {
// 如果接受連接失敗,需要清理一些東西
uv_shutdown_t *shutdown_req = malloc(sizeof(uv_shutdown_t));
r = uv_shutdown(shutdown_req, (uv_stream_t *)tcp_client_handle, shutdown_cb);
CHECK(r, "uv_shutdown");
}
當(dāng)客戶端連接失敗,需要關(guān)閉掉這個連接,于是我們就會初始化一個request
,然后傳遞給我們需要請求的操作,這里是關(guān)閉請求shutdown
。
?uv_request_t
是基本的request,其他任何request都是基于該結(jié)構(gòu)進(jìn)行擴(kuò)展,它定義的所有api其他request都可以使用。和uv_handle_t
一樣的功效。文章來源:http://www.zghlxwxcb.cn/news/detail-717452.html
libuv運行的三種模式
接著說說Libuv提供的三種運行模式:文章來源地址http://www.zghlxwxcb.cn/news/detail-717452.html
- UV_RUN_DEFAULT 默認(rèn)輪詢模式,此模式會一直運行事件循環(huán)直到?jīng)]有活躍句柄、引用句柄、和請求句柄
- UV_RUN_ONCE 一次輪詢模式,此模式如果pending_queue中有回調(diào),則會執(zhí)行回調(diào)而直接跨過uv__io_poll。如果沒有,則此方式只會執(zhí)行一次I/O輪詢(uv__io_poll)。如果在執(zhí)行過后有回調(diào)壓入到了pending_queue中,則uv_run會返回非0,你需要在未來的某個時間再次觸發(fā)一次uv_run來清空pending_queue。
- UV_RUN_NOWAIT 一次輪詢(無視pending_queue)模式,此模式類似UV_RUN_ONCE但是不會判斷pending_queue是否存在回調(diào),直接進(jìn)行一次I/O輪詢。
到了這里,關(guān)于Node.js中的單線程服務(wù)器的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!