探究如何整合 GLib Main Event Loop 和 Node.js 的 libuv

时间:2021-07-17 23:57:37

这个作者写了许多glib的东西,值得借鉴,牛人呀

原文地址:http://iteches.com/archives/5259

在普通情况下,整合 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 是不好的做法,是否有其他方式可以解决我们一开始的课题?解决方法是有的,从事件引擎的运作细节,我们可以发现一些端倪。

一般来说,大部份的事件引擎都包括了几个部份:

探究如何整合 GLib Main Event Loop 和 Node.js 的 libuv

从 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,
   &amp;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);
 }
}

注:Polling 这段在实现 GLib 内部函数 g_main_context_poll() 的工作。

监听的部份,若有事件被触发则呼叫 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 &amp;
  ((events &amp; UV_READABLE ? G_IO_IN : 0) |
  (events &amp; UV_WRITABLE ? G_IO_OUT : 0));

 uv_poll_stop(handle);
 uv_close((uv_handle_t *)handle, NULL);
 delete _pfd;
}

注:revents 在监听前会被我们归零,在事件触发时被我们设值,而其用途是告知 GLib main context 该事件被什么行为(读或写)触发。

最后是 check_cb(),我们要停止所有对事件的 Polling,然后让 GLib main context 去检查并处理事件:

void check_cb(uv_check_t *handle, int status)
{
 gint i;
 struct gcontext *ctx = &amp;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 来操作事件。