GMainLoop, GMainContext和GSource学习笔记

    做过GUI开发相关工作的人的人都知道,很多应用程序都是事件驱动的。这些事件大部分都来自于用户,比如键盘事件、鼠标事件或遥控器事件。还有一些事件来自于系统内部,比如定时事件、socket事件和其它文件事件等等。在没有任何事件的情况下,应用程序处于睡眠状态。

    因为这种事件驱动机制,GUI应用程序都毫无例外的需要一个主循环(main loop)。主循环(main loop)控制应用程序什么时候进入睡眠状态,什么时候被唤醒。

   

    目前常见的主循环设计主要是三种思路:

  1. 消息队列 + 信号量(semaphore) + sem_wait;
  2. 事件源 + select;
  3. 事件源 + poll。

  

   对于第一种设计思路,主循环不断的从消息队列中提取消息分发给窗口,如果消息队列中没有消息,主循环调用 sem_wait 进入休眠状态,直到有信息出现被唤醒。MiniGUI 的主循环就是这样实现的。

   对于第二种思路,主循环使用 poll 函数监听事件源,若有事件出现则进行处理,否则进入休眠状态,直到有事件到来。GTK+ 的主循环就是采用的这种思路。

   第三种思路与第二种思路类似,不同之处是主循环使用 select 函数监听事件源, QT 的主循环就是采用的这种思路。

MiniGUI 应用程序的主循环与 Win32 类似,大致如下:

    while (GetMessage (&Msg, hMainWnd)) {
        DispatchMessage (&Msg);
    }


在 MiniGUI 的主循环中,它不断的从消息队列中提取消息,然后分发给消息的目标(通常是窗口),直到GetMessage返回FALSE(收到WM_QUIT消息,一般调用 PostQuitMessage)为止,如果队列中没有消息,应用程序就进入睡眠状态。

这种方法的优点是简单明了,但缺陷也是明显的,它只能挂在消息队列上,而不能同时挂在多个事件源上(如管道和socket等)。要挂在多个事件源上,需要使用其它方式,比如用WaitForMultipleObjects,那就比较麻烦了。

关于主循环的休眠,MiniGUI 采用的是信号量的方式(sem_wait)。我们进入 GetMessage 接口的实现:

...

checkagain:

    LOCK_MSGQ (pMsgQueue);

    ... peek message ...

    UNLOCK_MSGQ (pMsgQueue);

    if (bWait) {
        /* no message, wait again. */
        sem_wait (&pMsgQueue->wait);
        goto checkagain;
    }


   GetMessage 调用 sem_wait 函数等待消息队列,若没有消息主循环进入休眠等待状态。

 

QT 的主循环

QT 采用的是基于 select 的主循环。主循环使用 select 函数监听事件源,当有事件发生时对其进行处理,若没有事件则休眠等待。

QT 的事件处理模块如下图所示:

类 QAbstractEventDispatcher 封装了所有的事件处理接口,包括事件的处理 (processEvents),Socket 的登记和注销 (xxxSocketNotifier),Timer 的注册与注销(xxxTimer)等。

不同版本的 QT 代码实现不同的 QAbstractEventDispathcer 子类。这里,我分析了 X11 平台的 QT 实现,它实现了 QEventDispathcerUnix 类,并派生出 QEventDispatcherX11 类。

processEvents 方法负责处理事件;select 方法提供了监听事件的接口,它实际上去调系统的 select 函数。

QT 相关代码在 src/gui/kernel 和 src/corelib/kernel 目录下。

 

    因为在项目中要用到gstreamer,因此简单学习了下glib和gobject.

    GLib对核心应用的支持包括事件循环、内存操作、线程操作、动态链接库的操作和出错处理与日志等。

    基本上,所有需要异步操作的地方都可以用事件循环。像文件、管道、设备、套接字、定时器、idle和其他自定义的事件都可以产生事件.

    今天,让我们简单的了解下GMainLoop, GMainContext和GSource。

    要让GMainLoop能够处理事件,首先就必须把它们加到GMainLoop去。
    首先我们需要了解事件循环的三个基本结构:GMainLoop, GMainContext和GSource。
    它们之间的关系是这样的:
    GMainLoop -> GMainContext -> {GSource1, GSource2, GSource3......}
    每个GmainLoop都包含一个GMainContext成员,而这个GMainContext成员可以装各种各样的GSource,GSource则是具体的各种Event处理逻辑了。在这里,可以把GMainContext理解为GSource的容器。(不过它的用处不只是装GSource)
    创建GMainLoop使用函数g_main_loop_new, 它的第一个参数就是需要关联的GMainContext,如果这个值为空,程序会分配一个默认的Context给GMainLoop。把GSource加到GMainContext呢,则使用函数g_source_attach。

    我们先看一段简单的示例代码:

    GMainContext* main_context = NULL;

    main_context = g_main_context_new();
    if(!main_context)
    {
       return SYS_FAILED;
    }
 
    main_loop = g_main_loop_new(main_context,FALSE);
   /*unref main ctx here so that main ctx is freed when mainloop was freed*/
    g_main_context_unref(main_context);
    if(!main_loop)
    {
      return SYS_FAILED;
    }

    g_main_loop_run(main_loop);

    g_main_loop_new创建一个main loop对象,一个main loop对象只能被一个线程使用,但一个线程可以有多个main loop对象。

g_main_loop_run则是进入主循环,它会一直阻塞在这里,直到让它退出为止。有事件时,它就处理事件,没事件时就睡眠。

在GTK+应用中,一个线程使用多个main loop的主要用途是实现模态对话框,它在gtk_dialog_run函数里创建一个新的main loop,通过该main loop分发消息,直到对话框关闭为止。

g_main_loop_quit则是用于退出主循环,相当于Win32下的PostQuitMessage函数。

   

    Glib main loop 的最大特点就是支持多事件源,使用非常方便。来自用户的键盘和鼠标事件、来自系统的定时事件和socket事件等等,还支持一个称为idle的事件源,其主要用途是实现异步事件。

Glib Main loop的设计思想是基于 poll 的 "prepare/check/dispatch" 模式,它的基本组成如下图所示:

 

GMainLoop的主要部件是GMainContext,GMainContext可以在多个GMainLoop间共享,但要求这些GMainLoop都在同一个线程中运行,前面提到的模态对话框就属于这一类。GMainContext通常由多个GSource组成,GSource是事件源的抽象,任何事件源,只要实现GSource规定的接口,都可以挂到GMainContext中来。

    Gsource的定义如下:

struct _GSource
{
  /*< private >*/
  gpointer callback_data;
  GSourceCallbackFuncs *callback_funcs;

  GSourceFuncs *source_funcs;----gsource的接口函数
  guint ref_count;

  GMainContext *context;

  gint priority;
  guint flags;
  guint source_id;

  GSList *poll_fds;
 
  GSource *prev;
  GSource *next;

  char    *name;

  GSourcePrivate *priv;
};

 

GSource的接口函数有:

struct _GSourceFuncs
{
  gboolean (*prepare)  (GSource    *source,
                        gint       *timeout_);
  gboolean (*check)    (GSource    *source);
  gboolean (*dispatch) (GSource    *source,
                        GSourceFunc callback,
                        gpointer    user_data);
  void     (*finalize) (GSource    *source); /* Can be NULL */

}

  1. gboolean (*prepare) (GSource *source, gint *timeout_);进入睡眠之前,在g_main_context_prepare里,mainloop调用所有Source的prepare函数,计算最小的timeout时间,该时间决定下一次睡眠的时间。
  2. gboolean (*check) (GSource *source); poll被唤醒后,在 g_main_context_check里,mainloop调用所有Source的check函数,检查是否有Source已经准备好了。如果poll是由于错误或者超时等原因唤醒的,就不必进行dispatch了。
  3. gboolean (*dispatch) (GSource*source, GSourceFunc callback,gpointer user_data); 当有Source准备好了,在 g_main_context_dispatch里,mainloop调用所有Source的dispatch函数,去分发消息。
  4. void (*finalize) (GSource *source); 在Source被移出时,mainloop调用该函数去销毁Source。

Main loop的工作流程简图如下:

来自资料1

下面我们看看几个内置Source的实现机制:

Idle 它主要用实现异步事件,功能类似于Win32下的PostMessage。但它还支持重复执行的特性,根据用户注册的回调函数的返回值而定。

  1. g_idle_prepare把超时设置为0,也就是即时唤醒,不进入睡眠状态。
  2. g_idle_check 始终返回TRUE,表示准备好了。
  3. g_idle_dispatch 调用用户注册的回调函数。

Timeout 它主要用于实现定时器,支持一次定时和重复定时,根据用户注册的回调函数的返回值而定。

  1. g_timeout_prepare 计算下一次的超时时间。
  2. g_timeout_check 检查超时时间是否到了,如果到了就返回TRUE,否则返回FALSE。
  3. g_timeout_dispatch调用用户注册的回调函数。

线程可以向自己的mainloop中增加Source,也可以向其它线程的mainloop增加 Source。向自己的mainloop中增加Source时,mainloop已经唤醒了,所以不会存在什么问题。而向其它线程的mainloop增加Source时,对方线程可能正挂在poll 里睡眠,所以要想法唤醒它,否则Source可能来不及处理。在Linux下,这是通过wake_up_pipe管道实现的,mainloop在poll时,它除了等待所有的Source外,还会等待wake_up_pipe管道。要唤醒poll,调用 g_main_context_wakeup_unlocked向wake_up_pipe里写入字母A就行了。

接下来看这个例子,它的作用是从stdin读取字符串,然后反转字符串并输出到屏幕。

//mainloop2.c
#include
#include
#include

GMainLoop* loop;

//当stdin有数据可读时被GSource调用的回调函数
gboolean callback(GIOChannel *channel)
{
    gchar* str;
    gsize len;
    //从stdin读取一行字符串
    g_io_channel_read_line(channel, &str, &len, NULL, NULL);
    //去掉回车键()
    while(len > 0 && (str[len-1] == '/r' || str[len-1] == '/n'))
        str[--len]='/0';
    //反转字符串
    for(;len;len--)
        g_print("%c",str[len-1]);
    g_print("/n");
    //判断结束符
    if(strcasecmp(str, "q") == 0){
        g_main_loop_quit(loop);
    }
    g_free(str);
}

void add_source(GMainContext *context)
{
    GIOChannel* channel;
    GSource* source;
    //这里我们监视stdin是否可读, stdin的fd默认等于1
    channel = g_io_channel_unix_new(1);
    //g_io_create_watch创建一个默认的io监视作用的GSource,下次再研究自定义GSource。参数G_IO_IN表示监视stdin的读取状态。
    source = g_io_create_watch(channel, G_IO_IN);
    g_io_channel_unref(channel);
    //设置stdin可读的时候调用的回调函数
    g_source_set_callback(source, (GSourceFunc)callback, channel, NULL);
    //把GSource附加到GMainContext
    g_source_attach(source, context);
    g_source_unref(source);
}

int main(int argc, char* argv[])
{
    GMainContext *context;

    if(g_thread_supported() == 0)
        g_thread_init(NULL);
    //新建一个GMainContext
    context = g_main_context_new();
    //然后把GSource附到这个Context上
    add_source(context);
    //把Context赋给GMainLoop
    loop = g_main_loop_new(context, FALSE);

    g_print("input string('q' to quit)/n");
    g_main_loop_run(loop);

    g_main_loop_unref(loop);
    //Context用完计数器减1
    g_main_context_unref(context);

    return 0;
}

 

你可能感兴趣的:(GMainLoop, GMainContext和GSource学习笔记)