GMainLoop和GSource简介

本文详细介绍了GLib中的GMainLoop、GSource及其在事件处理中的角色,包括GSource的生命周期、事件触发机制,以及如何创建自定义GSource。通过实例演示了如何使用GMainLoop进行定时和空闲事件处理,以及多GMainLoop绑定单GMainContext的场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

参考:

GLib学习_glibconfig.h-优快云博客

一个简单的例子

先看一个使用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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值