转自 Heiher's Blog https://heiher.info/848.html
GLib 实现了一个功能强大的事件循环分发处理机制,被抽象成为 GMainLoop,用于循环处理事件源上的事件。每个 GMainLoop 都工作在指定的 GMainContext 上。事件源在 GLib 中则被抽象成了 GSource。在 GMainContext 中有一个 GSource 列表。GLib 内部定义实现了三种类型的事件源,分别是 Idle, Timeout 和 I/O。同时也支持创建自定义的事件源。
自定义事件源的基本作用
自定义的事件源可以用来将外部信号(事件)挂到程序中的指定主循环上,从而在 g_main_loop_run 中可以响应这些事件。
如何创建自定义事件源
GLib 提供了一系列的接口用于创建自定义的事件源,下面我们先讲解一下创建事件源的基本函数和数据结构,最后给出一些实例。
自定义的事件源是一个继承 GSource 的结构体,即自定义事件源的结构体 的第一个成员是 GSource 结构体, 其后便可放置程序所需数据, 例如:
typedef struct _MySource MySource; struct _MySource { GSource _source; gchar text[256]; } |
实现了事件源数据结构的定义之后,还需要实现事件源所规定的接口,主要为 prepare, check, dispatch, finalize 等事件处理函数(回调函数),它们包含于 GSourceFuncs 结构体中。将 GSourceFuncs 结构以及事件源结构的存储空间宽度作为参数传给 g_source_new 便可构造一个新的事件源,继而可使用 g_source_attach 函数将新的事件源添加到主循环上下文中。下面这个示例可创建一个只会讲“Hello world!”的事件源,并将其添加到主事件循环默认的 GMainContext 中。
#include <glib.h> #include <glib/gprintf.h> typedef struct _MySource MySource; struct _MySource { GSource source; gchar text[256]; }; static gboolean prepare(GSource *source, gint *timeout) { *timeout = 0; return TRUE; } static gboolean check(GSource *source) { return TRUE; } static gboolean dispatch(GSource *source, GSourceFunc callback, gpointer user_data) { MySource *mysource = (MySource *)source; g_print("%s\n", mysource->text); return TRUE; } int main(void) { GMainLoop *loop = g_main_loop_new(NULL, TRUE); GMainContext *context = g_main_loop_get_context(loop); GSourceFuncs source_funcs = {prepare, check, dispatch, NULL}; GSource *source = g_source_new(&source_funcs, sizeof(MySource)); g_sprintf(((MySource *)source)->text, "Hello world!"); g_source_attach(source, context); g_source_unref(source); g_main_loop_run(loop); g_main_context_unref(context); g_main_loop_unref(loop); return 0; } |
上述程序的 g_main_loop_run 函数运行时,会迭代访问 GMainContext 的事件源列表,步骤大致如下:
a. g_main_loop_run 通过调用事件源的 prepare 接口并判断其返回值以确定各事件源是否作好准备。如果各事件源的 prepare 接口的返回值为 TRUE,即表示该事件源已经作好准备,否则表示尚未做好准备。显然,上述程序所定义的事件源是已经作好了准备。
b. 若某事件源尚未作好准备 ,那么 g_main_loop 会在处理完那些已经准备好的事件后再次询问该事件源是否作好准备 ,这一过程是通过调用事件源的 check 接口而实现的,如果事件源依然未作好准备,即 check 接口的返回 FALSE,那么 g_main_loop_run 会让主事件循环进入睡眠状态。主事件循环的睡眠时间是步骤 a 中遍历时间源时所统计的最小时间间隔 ,例如在 prepare 接口中可以像下面这样设置时间间隔。到达一定时间后, g_main_loop_run 会唤醒主事件循环,再次询问。如此周而复始,直至事件源的 prepare 接口返回值为 TRUE。
static gboolean prepare(GSource *source, gint *timeout) { *timeout = 1000; /* set time interval one second */ return TRUE; } |
c. 若事件源 prepare 与 check 函数返回值均为 TRUE,则 g_main_loop_run 会调用事件源的 dispatch 接口,由该接口调用事件源的响应函数。事件源的响应函数是回调函数,可使用 g_source_set_callback 函数进行设定。在上例中, 我们没有为自定义的事件源提供响应函数。
上文自定义的事件源实际是 Idle 类型的,此类事件源,是指那些只有在主事件循环无其他事件源处理时才会被处理的事件源。GLib 提供了预定义的空闲事件源类型,其用法见下面的示例。
#include <glib.h> static gboolean idle_func(gpointer data) { g_print("%s\n", (gchar *)data); return TRUE; } int main(void) { GMainLoop *loop = g_main_loop_new(NULL, TRUE); GMainContext *context = g_main_loop_get_context(loop); g_idle_add(idle_func, "Hello world!"); g_main_loop_run(loop); g_main_context_unref(context); g_main_loop_unref(loop); return 0; } |
上述示例中,idle_func 是 idle 事件源的响应函数,如果该函数返回值为 TRUE,那么它会在主事件循环空闲时重复被执行;如果 idle_func 的返回值为 FALSE,那么该函数在执行一次后,便被主事件循环从事件源中移除。g_idle_add 函数内部定义了一个空闲事件源,并将用户定义的回调函数设为空闲事件源的响应函数, 然后将该事件源挂到主循环上下文。
Timeout 类事件源,GLib 也提供了预定义的定时器事件源,其用法与 GLib 预定义的空闲事件源类似。例如:
#include <glib.h> static gboolean timeout_func(gpointer data) { static guint i = 0; i += 2; g_print ("%d\n", i); return TRUE; } int main(void) { GMainLoop *loop = g_main_loop_new(NULL, TRUE); GMainContext *context = g_main_loop_get_context(loop); g_timeout_add(2000, timeout_func, loop); g_main_loop_run(loop); g_main_context_unref(context); g_main_loop_unref(loop); return 0; } |
如果要自定义定时器类型的事件源,只需让事件源的 prepare 与 check 接口在时间超过所设定的时间间隔时返回 TRUE, 否则返回 FALSE。
I/O 类型的事件源要稍微难理解一些,因为涉及到了操作系统层面的 poll 机制。所谓 poll 机制,就是操作系统提供的对文件描述符所关联的 I/O 的状态监视功能 ,例如向文件中写入数据 ,那么 I/O 的状态可以表示为 POLLOUT, 而从文件中读取数据,那么 I/O 的状态就变为 POLLIN。GLib 为 Unix 系统与Windows 系统的 poll 机制进行了封装,并且可以将文件与主事件循环的事件源建立关联,在主循环的过程中, g_main_loop_run 会轮询各个关联到文件的事件源,并处理相应的事件响应。I/O 类型的事件源, prepare,其 check, dispatch 等接口的执行次序如下:
a. 主事件循环会首先调用 check 接口, 询问事件源是否准备好。因为此时, g_main_loop_run 尚未轮询那些与 I/O 相关联的事件源, 所以 I/O 类型的事件源, check 接口的返回值应该是 FALSE。其主事件循环调用 g_main_context_iteration 轮询各事件源,探寻是否有 I/O 类型事件源的状态发生变化,并记录变化结果。
b. 主循环调用 check 接口, 询问事件是否准备好。此时, 如果 I/O 类型事件源的状态变化符合要求,那么就返回 TRUEE,否则返回 FALSE。
c. 如果 prepare 与 check接口的返回值均为 TRUE, 那么此时主事件循环会调用 dispatch 接口分发消息。
下面的示例展示了一个自定义的 I/O 类型事件源的基本用法。该示例所产生的程序接受用户在终端中输入的字符串,并统计输入的字符数量。
#include <glib.h> typedef struct _MySource MySource; struct _MySource { GSource _source; GIOChannel *channel; GPollFD fd; }; static gboolean watch(GIOChannel *channel) { gsize len = 0; gchar *buffer = NULL; g_io_channel_read_line(channel, &buffer, &len, NULL, NULL); if(len > 0) g_print("%d\n", len); g_free(buffer); return TRUE; } static gboolean prepare(GSource *source, gint *timeout) { *timeout = -1; return FALSE; } static gboolean check(GSource *source) { MySource *mysource = (MySource *)source; if(mysource->fd.revents != mysource->fd.events) return FALSE; return TRUE; } static gboolean dispatch(GSource *source, GSourceFunc callback, gpointer user_data) { MySource *mysource = (MySource *)source; if(callback) callback(mysource->channel); return TRUE; } static void finalize(GSource *source) { MySource *mysource = (MySource *)source; if(mysource->channel) g_io_channel_unref(mysource->channel); } int main(int argc, char* argv[]) { GMainLoop *loop = g_main_loop_new(NULL, FALSE); GSourceFuncs funcs = {prepare, check, dispatch, finalize}; GSource *source = g_source_new(&funcs, sizeof(MySource)); MySource *mysource = (MySource *)source; mysource->channel = g_io_channel_new_file("test", "r", NULL); mysource->fd.fd = g_io_channel_unix_get_fd(mysource->channel); mysource->fd.events = G_IO_IN; g_source_add_poll(source, &mysource->fd); g_source_set_callback(source, (GSourceFunc)watch, NULL, NULL); g_source_set_priority(source, G_PRIORITY_DEFAULT_IDLE); g_source_attach(source, NULL); g_source_unref(source); g_main_loop_run(loop); g_main_context_unref(g_main_loop_get_context(loop)); g_main_loop_unref(loop); return 0; } |
像 Idle 类型与 Timeout 类型事件源那样,GLib 也提供了预定义的 I/O 类型事件源,使用它可以将上例简化为:
#include <glib.h> gboolean io_watch(GIOChannel *channel, GIOCondition condition, gpointer data) { gsize len = 0; gchar *buffer = NULL; g_io_channel_read_line(channel, &buffer, &len, NULL, NULL); if(len > 0) g_print("%d\n", len); g_free(buffer); return TRUE; } int main(int argc, char* argv[]) { GMainLoop *loop = g_main_loop_new(NULL, FALSE); GIOChannel* channel = g_io_channel_unix_new(1); if(channel) { g_io_add_watch(channel, G_IO_IN, io_watch, NULL); g_io_channel_unref(channel); } g_main_loop_run(loop); g_main_context_unref(g_main_loop_get_context(loop)); g_main_loop_unref(loop); return 0; } |
Over!