探究如何整合 GLib Main Event Loop 和 Node.js 的 libuv
在普通情況下,整合 GLib Main Event Loop 和 libuv 不是件平常人會做的事,因為,一般人使用著 GTK+、Clutter、DBus 等等函式庫(Library)時,永遠只會使用 GLib 而不會使用到第二套事件引擎。但是,在 Node.js 中,其事件引擎並不是 GLib,而是使用自己的『libuv』,想同時運行兩套事件引擎是不可能的,所以這將注定我們無法以 Node.js 去引入 GTK+、Clutter、DBus 等函式庫來使用。不過,天下文章一大抄,世界上所有事件引擎的設計差異不大,在理解 GLib Context 的運作後,我們還是可以嘗試將兩者整合在一起,協同運行。
簡單理解一下事件引擎,其說白了就是一個跑到天荒地老的無窮迴圈,不停的去檢查是否有事件被喚醒。所以,由此可知,兩套事件引擎不能被同時跑起來,因為任何一個事件處理的無窮迴圈,都將導致另一個事件處理的迴圈無法正常運作。所以,首要解決的課題,就是讓兩個無窮迴圈可以同時運作。
為了解決這樣的問題,你可能會想到去使用 Thread (執行緒/線程),只要在建立兩個 Thread,然後各自跑不同的事件引擎就可以了。但是這樣做,問題馬上就會出現。由於程式正在運作的過程中,事件數量相當多,事件被安插和喚醒的次數非常頻繁,這將導致兩個 Thread 之間很難維持穩定的資料交換,你得實作 Mutex Lock 等機制來達成 Thread-safe,更還要想辦法解決兩邊各類大小資料交換的需求。種種原因,都會造成兩個事件引擎大量的彼此等待,而效能不彰。此外,也不易同時控制兩邊的事件觸發順序,以及事件被喚醒時間的精確度,如計時器(Timer)。
既然使用 Thread 是不好的做法,是否有其他方式可以解決我們一開始的課題?解決方法是有的,從事件引擎的運作細節,我們可以發現一些端倪。
一般來說,大部份的事件引擎都包括了幾個部份:
從 Initial 出發, Event Loop 的迴圈每運行一次,都經歷了『prepared』、『Polling』和『Dispatching』三種狀態,若用白話來說,他們所代表的意義就是『已準備好事件清單』、『監控事件』、『喚醒、分配並處理事件』。
如果你去參考事件引擎程式碼,會發現在三個狀態之間,分別有『prepare()』、『query()』、『check()』和『dispatch()』等四個動作,而這四個動作的行為簡單來說是『準備事件清單』、『取得需要被監控的事件』、『確認是否事件被觸發』、『喚醒並處理被觸發的事件』。
理解了事件引擎的運作流程後,該如何讓兩個引擎協同運作便有了答案。就如同本文一開頭所說,其實所有的事件引擎都大同小異,所以 GLib 和 libuv 也有差不多的行為,同樣有『prepare()』、『check()』等動作。那麼,我們只需要將 GLib 的事件處理機制,原封不動的一一搬到 libuv 上功能對應的地方即可。
理論上,移植 GLib 的工作並不難,我們只要參考 GLib Source Code,把 g_main_loop_run() 和 g_main_context_iterate() 內的工作拆解出來,放到 libuv 的 prepare() 和 check() 就能達到目的。
首先,在註冊兩個 libuv 的事件(用於 prepare 和 check):
然後在 uv_prepare_t 的 Callback Function(check_cb),取得 GLib main context 的事件清單並監聽:
註:Polling 這段在實作 GLib 內部函數 g_main_context_poll() 的工作。
監聽的部份,若有事件被觸發則呼叫 poll_cb(),所以我們也定義:
註:revents 在監聽前會被我們歸零,在事件觸發時被我們設值,而其用途是告知 GLib main context 該事件被什麼行為(讀或寫)觸發。
最後是 check_cb(),我們要停止所有對事件的 Polling,然後讓 GLib main context 去檢查並處理事件:
到目前為止,GLib 的事件引擎已經可以良好的跑在 libuv 上。
此外,由於筆者本身開發了許多 Node.js Module 都有用到 GLib Main Event Loop,如:『dbus』、『jsdx-toolkit』等,常有這樣的需求,所以直接將這個部份寫成 C++ Class,以便日後其他專案使用。如果你有需要,可以參考兩支檔案:
將 Header 引入你的程式碼後,就可以直接宣告並使用它:
後記
libuv 的歷史和說明,本文就不多提,欲知更多細節,可參閱舊文『Node.js v0.8 大變革?!Native Module 開發者的福音!』。而接下來,你只需要知道,日後『libuv』將是 Node.js 內統一的事件處理機制就可以,這對撰寫 Node.js C/C++ Add-on 的人來說尤為重要,因為無論是在哪一個作業系統,都可以直接使用 libuv API 來操作事件。
簡單理解一下事件引擎,其說白了就是一個跑到天荒地老的無窮迴圈,不停的去檢查是否有事件被喚醒。所以,由此可知,兩套事件引擎不能被同時跑起來,因為任何一個事件處理的無窮迴圈,都將導致另一個事件處理的迴圈無法正常運作。所以,首要解決的課題,就是讓兩個無窮迴圈可以同時運作。
為了解決這樣的問題,你可能會想到去使用 Thread (執行緒/線程),只要在建立兩個 Thread,然後各自跑不同的事件引擎就可以了。但是這樣做,問題馬上就會出現。由於程式正在運作的過程中,事件數量相當多,事件被安插和喚醒的次數非常頻繁,這將導致兩個 Thread 之間很難維持穩定的資料交換,你得實作 Mutex Lock 等機制來達成 Thread-safe,更還要想辦法解決兩邊各類大小資料交換的需求。種種原因,都會造成兩個事件引擎大量的彼此等待,而效能不彰。此外,也不易同時控制兩邊的事件觸發順序,以及事件被喚醒時間的精確度,如計時器(Timer)。
既然使用 Thread 是不好的做法,是否有其他方式可以解決我們一開始的課題?解決方法是有的,從事件引擎的運作細節,我們可以發現一些端倪。
一般來說,大部份的事件引擎都包括了幾個部份:
從 Initial 出發, Event Loop 的迴圈每運行一次,都經歷了『prepared』、『Polling』和『Dispatching』三種狀態,若用白話來說,他們所代表的意義就是『已準備好事件清單』、『監控事件』、『喚醒、分配並處理事件』。
如果你去參考事件引擎程式碼,會發現在三個狀態之間,分別有『prepare()』、『query()』、『check()』和『dispatch()』等四個動作,而這四個動作的行為簡單來說是『準備事件清單』、『取得需要被監控的事件』、『確認是否事件被觸發』、『喚醒並處理被觸發的事件』。
理解了事件引擎的運作流程後,該如何讓兩個引擎協同運作便有了答案。就如同本文一開頭所說,其實所有的事件引擎都大同小異,所以 GLib 和 libuv 也有差不多的行為,同樣有『prepare()』、『check()』等動作。那麼,我們只需要將 GLib 的事件處理機制,原封不動的一一搬到 libuv 上功能對應的地方即可。
理論上,移植 GLib 的工作並不難,我們只要參考 GLib Source Code,把 g_main_loop_run() 和 g_main_context_iterate() 內的工作拆解出來,放到 libuv 的 prepare() 和 check() 就能達到目的。
首先,在註冊兩個 libuv 的事件(用於 prepare 和 check):
/* Prepare */ uv_prepare_init(uv_default_loop(), &prepare_handle); uv_prepare_start(&prepare_handle, prepare_cb); /* Check */ uv_check_init(uv_default_loop(), &check_handle); uv_check_start(&check_handle, check_cb);
然後在 uv_prepare_t 的 Callback Function(check_cb),取得 GLib main context 的事件清單並監聽:
void prepare_cb(uv_prepare_t *handle, int status) { gint i; gint timeout; struct gcontext *ctx = &g_context; g_main_context_prepare(ctx->gc, &ctx->max_priority); /* Getting all sources from GLib main context */ while(ctx->allocated_nfds < (ctx->nfds = g_main_context_query(ctx->gc, ctx->max_priority, &timeout, ctx->fds, ctx->allocated_nfds))) { g_free(ctx->fds); g_free(ctx->poll_handles); ctx->allocated_nfds = ctx->nfds; ctx->fds = g_new(GPollFD, ctx->allocated_nfds); ctx->poll_handles = g_new(uv_poll_t, ctx->allocated_nfds); } /* Polling */ for (i = 0; i < ctx->nfds; ++i) { GPollFD *pfd = ctx->fds + i; uv_poll_t *pt = ctx->poll_handles + i; struct gcontext_pollfd *data = new gcontext_pollfd; data->pfd = pfd; pt->data = data; pfd->revents = 0; uv_poll_init(uv_default_loop(), pt, pfd->fd); uv_poll_start(pt, UV_READABLE | UV_WRITABLE, poll_cb); } }
監聽的部份,若有事件被觸發則呼叫 poll_cb(),所以我們也定義:
void poll_cb(uv_poll_t *handle, int status, int events) { struct gcontext_pollfd *_pfd = (struct gcontext_pollfd *)handle->data; GPollFD *pfd = _pfd->pfd; pfd->revents |= pfd->events & ((events & UV_READABLE ? G_IO_IN : 0) | (events & UV_WRITABLE ? G_IO_OUT : 0)); uv_poll_stop(handle); uv_close((uv_handle_t *)handle, NULL); delete _pfd; }
最後是 check_cb(),我們要停止所有對事件的 Polling,然後讓 GLib main context 去檢查並處理事件:
void check_cb(uv_check_t *handle, int status) { gint i; struct gcontext *ctx = &g_context; /* Release all polling events which aren't finished yet. */ for (i = 0; i < ctx->nfds; ++i) { GPollFD *pfd = ctx->fds + i; uv_poll_t *pt = ctx->poll_handles + i; if (uv_is_active((uv_handle_t *)pt)) { uv_poll_stop(pt); uv_close((uv_handle_t *)pt, NULL); delete (struct gcontext_pollfd *)pt->data; } } g_main_context_check(ctx->gc, ctx->max_priority, ctx->fds, ctx->nfds); g_main_context_dispatch(ctx->gc); }
到目前為止,GLib 的事件引擎已經可以良好的跑在 libuv 上。
此外,由於筆者本身開發了許多 Node.js Module 都有用到 GLib Main Event Loop,如:『dbus』、『jsdx-toolkit』等,常有這樣的需求,所以直接將這個部份寫成 C++ Class,以便日後其他專案使用。如果你有需要,可以參考兩支檔案:
將 Header 引入你的程式碼後,就可以直接宣告並使用它:
Context *context = new Context; context->Init();
後記
libuv 的歷史和說明,本文就不多提,欲知更多細節,可參閱舊文『Node.js v0.8 大變革?!Native Module 開發者的福音!』。而接下來,你只需要知道,日後『libuv』將是 Node.js 內統一的事件處理機制就可以,這對撰寫 Node.js C/C++ Add-on 的人來說尤為重要,因為無論是在哪一個作業系統,都可以直接使用 libuv API 來操作事件。
留言
張貼留言