参考:
一个简单的例子
先看一个使用GMainLoop和GSource的简单程序
static gboolean quit_loop (gpointer data)
{
GMainLoop *loop = data;
g_print("%s\n", __func__);
g_main_loop_quit(loop);
return G_SOURCE_REMOVE;
}
static void test_gloop()
{
GMainLoop *loop;
loop = g_main_loop_new (NULL, FALSE);
g_timeout_add(8000, (GSourceFunc)quit_loop, loop);
g_main_loop_run (loop);
g_main_loop_unref (loop);
}
GMainLoop的基本用法其实很简单,只要三步即可
- 用g_main_loop_new创建一个loop对象
- 用g_main_loop_run让程序在loop中迭代(循环)
- 在loop迭代中,一旦调用到g_main_loop_quit,则从g_main_loop_run()中返回,执行最后的loop释放,即g_main_loop_unref
那么,上面程序中的g_timeout_add又是什么呢?其实,这是一个GSource。更准确地说,这个g_timeout_add语句,向我们创建的loop中添加了一个“timeout”的事件源(GSource),这个事件源使得quit_loop()这个函数在8秒(8000,单位毫秒)后被执行。
g_timeout_add(8000, (GSourceFunc)quit_loop, loop);
从这个例子中可以看出,GSource是一个在GMainLoop中运行的事件源,GSource一般都有自己的触发条件和触发行为。
详解与实践
GMainLoop与GSource详解
在前面的例子中可以看到,GMainLoop就是跑在一个线程中的死循环。但事实上,和线程直接绑定的并不是GMainLoop,而是GMaincontext,即一个线程只能有一个GMaincontext。
下面是GMaincontext的部分定义,可以看到线程(GThread *owner;)是在其中的。
struct _GMainContext
{
/* The following lock is used for both the list of sources
* and the list of poll records
*/
GMutex mutex;
GCond cond;
GThread *owner;
...
};
而GMainLoop则是和GMaincontext绑定,见下面的GMainLoop结构定义:
struct _GMainLoop
{
GMainContext *context;
gboolean is_running; /* (atomic) */
gint ref_count; /* (atomic) */
};
而事件源(GSource)是直接依附于GMaincontext,也就是说,事件源(GSource)需要在GMaincontext所在的线程中、且在绑定这个GMaincontext的GMainLoop中执行。但是其它线程能够向这个GMaincontext所在的线程中、添加和删除事件源。
下面是GSource的结构定义。
struct _GSource
{
/*< private >*/
gpointer callback_data;
GSourceCallbackFuncs *callback_funcs;
const GSourceFuncs *source_funcs;
guint ref_count;
GMainContext *context;
gint priority;
guint flags;
guint source_id;
GSList *poll_fds;
GSource *prev;
GSource *next;
char *name;
GSourcePrivate *priv;
};
struct _GSourceFuncs
{
gboolean (*prepare) (GSource *source,
gint *timeout_);/* Can be NULL */
gboolean (*check) (GSource *source);/* Can be NULL */
gboolean (*dispatch) (GSource *source,
GSourceFunc callback,
gpointer user_data);
void (*finalize) (GSource *source); /* Can be NULL */
/*< private >*/
/* For use by g_source_set_closure */
GSourceFunc closure_callback;
GSourceDummyMarshal closure_marshal; /* Really is of type GClosureMarshal */
};
注意到,GSource中有一个GSourceFuncs,里面包含四个函数prepare、check、dispatch以及finalize,这四个函数代表了了一个事件源的生命不同周期:
- prepare : 设置检查事件时间超时。如果返回 TRUE, check 会立刻被调用;如果返回 FALSE 并设置了 timeout , timeout 时间后 check 会被调用。
- check:检查事件是否准备完毕。返回 TRUE 为准备完毕, dispatch 会被立刻调用;返回 FALSE 不调用 dispatch,进入下一次循环。
- dispatch:分发事件。返回 TRUE 将继续下一次操作循环;返回 FALSE 中止本事件源的事件循环。
- finalize : 当事件源被移除时被调用。
下面从g_main_loop_run中抽取部分关键代码,印证上面的结论:
void
g_main_loop_run (GMainLoop *loop)
{
...
while (g_atomic_int_get (&loop->is_running))
g_main_context_iterate (loop->context, TRUE, TRUE, self);
...
}
static gboolean
g_main_context_iterate (GMainContext *context,
gboolean block,
gboolean dispatch,
GThread *self)
{
...
g_main_context_prepare (context, &max_priority);
...
some_ready = g_main_context_check (context, max_priority, fds, nfds);
...
if (dispatch)
g_main_context_dispatch (context);
}
Timeout和Idle事件源实践
Glib内部提供了三种类型的事件源:
- Timeout
- Idle
- ChildWatch (暂不理解)
glib中已经提供了几个GSource的子类,其中的g_timeout_source_new(guint interval)和g_idle_source_new(void)这两个函数构造出的GSource子类,就并不使用文件描述符号。
GTimeoutSource,只需要保存设定的间隔时间,在poll轮循的prepare和check阶段,会去检查设定的这个间隔时间是否到达,如果到达设定的时间(check函数返回TRUE),则它的dispatch函数会被调用。
g_idle_source_new(void)其实都没有继承GSource,它也不需要和文件绑定,只不过它的优先级比较低,只有在poll轮询的空闲阶段,它的dispatch函数会被调用。而它的prepare和check函数,始终都是返回的TRUE。
下面看看Timeout和Idle两个事件源的示例:
static gboolean count_down()
{
g_print("%p: %s\n", g_thread_self(), __func__);
return G_SOURCE_REMOVE;
}
static gboolean quit_loop (gpointer data)
{
GMainLoop *loop = data;
g_print("%p: %s\n", g_thread_self(), __func__);
g_main_loop_quit(loop);
return G_SOURCE_REMOVE;
}
static gboolean say_idle()
{
g_print("%p: %s\n", g_thread_self(), __func__);
return G_SOURCE_REMOVE;
}
static gboolean set_idle_again()
{
g_print("%p: %s\n", g_thread_self(), __func__);
g_idle_add((GSourceFunc)say_idle, NULL);
return G_SOURCE_REMOVE;
}
static void test_gloop()
{
/* 1.创建一个 GMainLoop 结构体对象,作为一个主事件循环 */
GMainLoop *loop = g_main_loop_new(NULL, FALSE);
/* 2.添加超时事件源 */
g_timeout_add(1000, (GSourceFunc)count_down, NULL);
g_timeout_add(3000, (GSourceFunc)set_idle_again, NULL);
g_timeout_add(8000, (GSourceFunc)quit_loop, loop);
/* 3.添加空闲函数,没有更高优先级事件时,空闲函数就可以被执行 */
g_idle_add((GSourceFunc)say_idle, NULL);
/* 4.循环检查事件源中是否有新事件进行分发,当某事件处理函数调用 g_main_loop_quit(),函数退出 */
g_main_loop_run(loop);
/* 5.减少loop引用计数,如果计数为0,则会释放loop资源 */
g_print("unref loop\n");
g_main_loop_unref(loop);
}
程序说明:
- G_SOURCE_REMOVE和G_SOURCE_CONTINUE是glib定义的宏,分别为FALSE和TRUE
- 这个程序为GMainLoop添加了4个事件源,3个Timeout事件源,1个Idle事件源
- 在超时时间为8秒的事件源“g_timeout_add(8000, (GSourceFunc)quit_loop, loop);”中,回调函数quit_loop中会调用g_main_loop_quit退出g_main_loop_run
- 所有回调函数均返回G_SOURCE_REMOVE,表示一旦事件被触发,则不再保留这个事件源。注意:对于timeout的事件源,假设回调函数返回G_SOURCE_CONTINUE,则回调函数会按照timeout设置的时间被周期地调用。
程序输出如下:
========start test_gloop
0x558c1ae09800: say_idle
0x558c1ae09800: count_down
0x558c1ae09800: set_idle_again
0x558c1ae09800: say_idle
0x558c1ae09800: quit_loop
unref loop
========end test_gloop
多个GMainLoop绑定一个GMaincontext的实践
前面提到,事件源是依附于GMaincontext的,并非GMainLoop。而GMainLoop则绑定于GMaincontext。从GMainLoop的创建来看,一个GMainLoop只能依附于一个GMaincontext:
GMainLoop *
g_main_loop_new (GMainContext *context,
gboolean is_running)
那么一个GMaincontext是否可以有多个GMainLoop呢?答案是可以。但需要特别说明的是,一般我们并不这么用,即在实际使用时,一般一个GMaincontext只有一个GMainLoop。否则,会使程序理解起来比较困难。
下面简单看一下如果一个GMaincontext创建两个GMainLoop是什么情形。将前面的test_gloop函数改造一下,得到新的函数test_gloop_2loops:
static void test_gloop_2loops()
{
/* 1.创建一个 GMainLoop 结构体对象,作为一个主事件循环 */
GMainLoop *loop = g_main_loop_new(NULL, FALSE);
GMainLoop *loop1 = g_main_loop_new(NULL, FALSE);
/* 2.添加超时事件源 */
g_timeout_add(1000, (GSourceFunc)count_down, NULL);
g_timeout_add(2000, (GSourceFunc)set_idle_again, NULL);
g_timeout_add(4000, (GSourceFunc)quit_loop, loop);
g_timeout_add(5000, (GSourceFunc)count_down, NULL);
g_timeout_add(6000, (GSourceFunc)set_idle_again, NULL);
g_timeout_add(8000, (GSourceFunc)quit_loop, loop1);
/* 3.添加空闲函数,没有更高优先级事件时,空闲函数就可以被执行 */
g_idle_add((GSourceFunc)say_idle, NULL);
/* 4.循环检查事件源中是否有新事件进行分发,当某事件处理函数调用 g_main_loop_quit(),函数退出 */
g_print("%p: start loop run\n", g_thread_self());
g_main_loop_run(loop);
g_print("%p: start loop1 run\n", g_thread_self());
g_main_loop_run(loop1);
/* 5.减少loop引用计数,如果计数为0,则会释放loop资源 */
g_print("%p: unref loop\n", g_thread_self());
g_main_loop_unref(loop);
g_print("%p: unref loop1\n", g_thread_self());
g_main_loop_unref(loop1);
}
说明
- 可以看到,程序为默认的GMaincontext(g_main_loop_new第一个参数为NULL)创建了2个GMainLoop:loop和loop1。
- 添加Timeout和Idle事件源的时候,并不需要指定loop,因为其依附于GMaincontext,而g_timeout_add和g_idle_add只会向默认的GMaincontext添加事件源
- 程序首先会停在“g_main_loop_run(loop);”中运行,直到“g_timeout_add(4000, (GSourceFunc)quit_loop, loop);”事件源触发后、退出loop,然后才开始在“g_main_loop_run(loop1);”中运行
- 在开始运行第二个loop(loop1)时,时间已经走了4秒,因此“g_timeout_add(5000, (GSourceFunc)count_down, NULL);”将在1秒之后被触发。这里充分体现出事件源是依附于GMaincontext,而非GMainLoop。
- 两个loop均在同一个线程中运行(后面输出的线程地址就一个:0x55c1757dca00)
程序输出如下:
========start test_gloop
0x55c1757dca00: start loop run
0x55c1757dca00: say_idle
0x55c1757dca00: count_down
0x55c1757dca00: set_idle_again
0x55c1757dca00: say_idle
0x55c1757dca00: quit_loop
0x55c1757dca00: start loop1 run
0x55c1757dca00: count_down
0x55c1757dca00: set_idle_again
0x55c1757dca00: say_idle
0x55c1757dca00: quit_loop
0x55c1757dca00: unref loop
0x55c1757dca00: unref loop1
========end test_gloop
自定义GSource实践
下面看一个自定义GSource的例子,通过这个例子,可以更好地理解GSource在GMainLoop中的运行机制。
//设置检查事件时间超时。如果返回 TRUE, check 会立刻被调用;
//如果返回 FALSE 并设置了 timeout , timeout 时间后 check 会被调用。
gboolean source_prepare_cb(GSource * source,
gint * timeout)
{
g_print("prepare\n");
*timeout = 1000;
return FALSE;
}
//检查事件是否准备完毕。返回 TRUE 为准备完毕, dispatch 会被立刻调用;
//返回 FALSE 不调用 dispatch,进入下一次事件循环。
gboolean source_check_cb(GSource * source)
{
g_print("check\n");
return TRUE;
}
//分发事件。返回 TRUE 将继续下一次操作循环;
//返回 FALSE 中止本事件源的事件循环。
gboolean source_dispatch_cb(GSource * source,
GSourceFunc callback, gpointer data)
{
static int cnt = 0;
g_print("dispatch, cnt=%d\n", cnt);
if (cnt > 3)
{
//g_source_unref(source); // It's OK not to unref the GSource
return G_SOURCE_REMOVE;
}
cnt++;
return G_SOURCE_CONTINUE;
}
//当事件源被移除时被调用。
void source_finalize_cb(GSource * source)
{
g_print("finalize\n");
}
static void test_gsource()
{
GMainLoop * mainloop;
GMainContext * maincontext;
GSource * source;
GSourceFuncs sourcefuncs;
sourcefuncs.prepare = source_prepare_cb;
sourcefuncs.check = source_check_cb;
sourcefuncs.dispatch = source_dispatch_cb;
sourcefuncs.finalize = source_finalize_cb;
mainloop = g_main_loop_new(NULL, FALSE);
maincontext = g_main_loop_get_context(mainloop);
source = g_source_new(&sourcefuncs, sizeof(GSource));
g_source_attach(source, maincontext);
g_source_unref(source);
g_timeout_add(8000, (GSourceFunc)quit_loop, mainloop);
g_main_loop_run(mainloop);
g_print("unref loop\n");
g_main_loop_unref(mainloop);
}
说明:
- 在用g_source_new创建GSource之后,需要立即调用g_source_attach将这个GSource依附于GMainContext上,这是使用GSource必须要做的。
- 自定义了prepare,check,dispatch,finalize函数,以便查看执行情况。在这些函数中都添加了打印,这样能够很清楚地看出来在GMainLoop的一次迭代中,这些函数是怎样被调用的。
- 在source_prepare_cb中设置了timeout为1秒,这样每隔1秒会检查一次我们定义的GSource事件
- 在source_dispatch_cb中设置了删除事件源的条件。注意:dispatch函数只需要返回G_SOURCE_REMOVE即可,不需要调用g_source_unref(source)了。glib的框架会根据dispatch返回的G_SOURCE_REMOVE结果自动删除GSource,并且从GMainContext删除这个GSource。但是按照glib的描述:“it will still have its final reference count”。这个“final reference count”会在finalize中处理。
程序运行结果如下:
========start test_gsource
prepare
check
dispatch, cnt=0
prepare
check
dispatch, cnt=1
prepare
check
dispatch, cnt=2
prepare
check
dispatch, cnt=3
prepare
check
dispatch, cnt=4
finalize
0x559b36220600: quit_loop
unref loop
========end test_gsource
可以看到:
- 每次迭代都在依次执行prepare,check,dispatch函数
- 最后GSource被删除的时候,会调用finalize