Libuv 基礎
libuv 采用了 異步 (asynchronous), 事件驅動 (event-driven)的編程風格, 其主要任務是為開人員提供了一套事件循環和基於I/O(或其他活動)通知的回調函數, libuv 提供了一套核心的工具集, 例如定時器, 非阻塞網絡編程的支持, 異步訪問文件系統, 子進程以及其他功能.
事件循環(Event loops)
在事件編程模型中, 應用程序通常會關注某些特定的事件, 並在事件發生后對其作出響應. 而收集事件或監控其他事件源則是 libuv 的職責, 編程人員只需要對感興趣的事件注冊回調函數, 在事件發生后 libuv 將會調用相應的回調函數. 只要程序不退出(被系統管理人員 kill 掉), 事件循環通常會一直運行, 下面是事件驅動編程模型的偽代碼:
while there are still events to process: e = get the next event if there is a callback associated with e: call the callback
適用於事件驅動編程模型的例子如下:
- 文件已經准備好可寫入數據.
- 某一 socket 上存在數據可讀.
- 定時器已超時.
事件循環由 uv_run 函數封裝, 在使用 libuv 編程時, 該函數通常在最后才被調用.
計算機程序最基本的活動是輸入輸出的處理, 而不是大量的數值計算, 而使用傳統輸入輸出函數(read, fprintf 等)的問題是它們都是 阻塞 的. 將數據寫入磁盤或者從網絡讀取數據都會消耗大量時間, 而阻塞函數直到任務完成后才返回, 在此期間你的程序什么也沒有做, 浪費了大量的 CPU 時間. 對於追求高性能的程序而言, 在其他活動或者 I/O 操作在進行盡量讓 CPU 不被阻塞.
標准的解決方案是使用線程, 每個阻塞的 I/O 操作都在一個單獨的線程(或線程池)中啟動, 當阻塞函數被調用時, 處理器可以調度另外一個真正需要 CPU 的線程來執行任務.
Libuv 采用另外一種方式處理阻塞任務, 即 異步 和 非阻塞 方式.大多數現代操作系統都提供了事件通知功能, 例如, 調用 read 讀取網絡套接字時程序會阻塞, 直到發送者最終發送了數據(read 才返回). 但是, 應用程序可以要求操作系統監控套接字, 並在套接字上注冊事件通知. 應用程序可以在適當的時候查看它所監視的事件並獲取數據(若有). 整個過程是 異步 的, 因為程序在某一時刻關注了它感興趣的事件, 並在另一個時刻獲取(使用)數據, 這也是 非阻塞 的, 因為該進程還可以處理另外的任務. Libuv 的事件循環方式很好地與該模型匹配, 因為操作系統事件可以視為另外一種 libuv 事件. 非阻塞方式可以保證在其他事件到來時被盡快處理 [1].
Note
I/O 是如何在后台運行的不是我們所關心的, 但是由於我們計算機硬件的工作方式, 線程是處理器最基本的執行單元, thread as the basic unit of the , libuv 和操作系統通常會運行后台/工作者線程, 或者采用非阻塞方式來輪流執行任務.
Hello World
具備了上面最基本的知識后, 我們就來編寫一個簡單 libuv 的程序吧.該程序並沒有做任何具體的事情, 只是簡單的啟動了一個會退出的事件循環.
#include <stdio.h> #include <uv.h> int main() { uv_loop_t *loop = uv_loop_new(); printf("Now quitting.\n"); uv_run(loop, UV_RUN_DEFAULT); return 0; }
該程序啟動后就會直接退出, 因為你沒有事件可處理. 我們可以使用 libuv 提供了各種 API 來告知 libuv 我們感興趣的事件.
libuv 的默認事件循環(Default loop)
libuv 提供了一個默認的事件循環, 你可以通過 uv_default_loop 來獲得該事件循環, 如果你的程序中只有一個事件循環, 你就應該使用 libuv 為我們提供的默認事件循環.
Note
node.js 使用默認事件循環作為它的主循環,如果你正在編寫 node.js 的綁定, 你應該意識到這一點.
監視器(Watchers)
libuv 通過監視器(Watcher)來對特定事件進行監控, 監視器通常是類似 uv_TYPE_t 結構體的封裝, TYPE 代表該監視器的用途, libuv 所有的監視器類型如下:
typedef struct uv_loop_s uv_loop_t; typedef struct uv_err_s uv_err_t; typedef struct uv_handle_s uv_handle_t; typedef struct uv_stream_s uv_stream_t; typedef struct uv_tcp_s uv_tcp_t; typedef struct uv_udp_s uv_udp_t; typedef struct uv_pipe_s uv_pipe_t; typedef struct uv_tty_s uv_tty_t; typedef struct uv_poll_s uv_poll_t; typedef struct uv_timer_s uv_timer_t; typedef struct uv_prepare_s uv_prepare_t; typedef struct uv_check_s uv_check_t; typedef struct uv_idle_s uv_idle_t; typedef struct uv_async_s uv_async_t; typedef struct uv_process_s uv_process_t; typedef struct uv_fs_event_s uv_fs_event_t; typedef struct uv_fs_poll_s uv_fs_poll_t; typedef struct uv_signal_s uv_signal_t;
所有監視器的結構都是 uv_handle_t 的”子類”, 在 libuv 和本文中都稱之為句柄( handlers ).
監視器由相應類型的初始化函數設置, 如下:
uv_TYPE_init(uv_TYPE_t*)
某些監視器初始化函數的第一個參數為事件循環的句柄.
監視器再通過調用如下類型的函數來設置事件回調函數並監聽相應事件:
uv_TYPE_start(uv_TYPE_t*, callback)
而停止監聽應調用如下類型的函數:
uv_TYPE_stop(uv_TYPE_t*)
當 libuv 所監聽事件發生后, 回調函數就會被調用. 應用程序特定的邏輯通常都是在回調函數中實現的, 例如, 定時器回調函數在發生超時事件后也會被調用, 另外回調函被調用時傳入的相關參數都與特定類型的事件有關, 例如, IO 監視器的回調函數在發生了IO事件后將會收到從文件讀取的數據.
空轉(Idling)
接下來我們通過例子來講述監視器的使用. 例子中空轉監視器回調函數被不斷地重復調用, 當然其中也有一些深層次的語言,我們將會在 工具集 進一步討論, 但現在我們只是跳過具體細節. 我們只是使用了一個空轉監視器回調來看看監視器的生命周期, 通過例子我們也可以了解到: 由於設置了監視器, 所以調用 uv_run() 是程序會阻塞, 空轉監視器將會在計數器達到設定的值時停止(監視), uv_run() 會退出因為此時程序中沒有活動的監視器了.
#include <stdio.h> #include <uv.h> int64_t counter = 0; void wait_for_a_while(uv_idle_t* handle, int status) { counter++; if (counter >= 10e6) uv_idle_stop(handle); } int main() { uv_idle_t idler; uv_idle_init(uv_default_loop(), &idler); uv_idle_start(&idler, wait_for_a_while); printf("Idling...\n"); uv_run(uv_default_loop(), UV_RUN_DEFAULT); return 0; }