GMainLoop的实现原理和代码模型

转载时请注明出处和作者联系方式
文章出处:http://blog.csdn.net/jack0106 
作者联系方式:冯牮 fengjian0106@yahoo.com.cn

 

 

做linux程序开发有一段时间了,也使用过好几个UI库,包括gtk,qt,还有clutter。其中感觉最神秘的,就是所谓的“主事件循环",在qt中,就是QApplication,gtk中是gtk_main(),clutter中则是clutter_main()。这些事件循环对象,都被封装的很“严密",使用的时候,代码都很简单。而我们在编写应用程序的过程中,通常也只需要重载widget的event处理函数(或者是处理event对应的信号),至于event是怎样产生和传递的,这就是个谜。

 

最近时间比较充裕,仔细研究了一下事件循环,参考的代码是glib中的GMainLoop。gtk_main()和clutter_main(),都是基于GMainLoop的。另外,其实事件循环的概念,也不仅仅使用在UI编程中,在网络编程中,同样大量的使用,可以这样说,event loop,是编程模型中,最基本的一个概念。可惜在大学教材中,从来没有看到过这个概念,玩单片机的时候,也用不到这个概念,只有在有操作系统的环境下,才会有event loop。

 

event loog的代码基础,还要用到一个概念--I/O的多路复用。目前常用的api接口,有3个,select,poll以及epoll。glib是一个跨平台的库,在linux上,使用的是poll函数,在window上,使用的是select。而epoll这个接口,在linux2.6中才正式推出,它的效率,比前两者更高,在网络编程中大量使用。而本质上,这三个函数,其实是相同的。

 

如果对I/O多路复用还不了解,请先自行google学习。下面,仅仅给出一个使用poll接口的代码模型片段。

 

 

 

 

上面这个代码,我们可以把它拆分成3部分:

1. 准备要检测的文件集合(不是简单的准备“文件描述符"的集合,而是准备struct pollfd结构体的集合。这就包括了文件描述符,以及希望监控的事件,如可读/可写/或可执行其他操作等)。

2. 执行poll,等待事件发生(文件描述符对应的文件可读/可写/或可执行其他操作等)或者是函数超时返回。

3. 遍历文件集合(struct pollfd结构体的集合),判断具体是哪些文件有“事件"发生,并且进一步判断是何种“事件"。然后,根据需求,执行对应的操作(上面的代码中,用...表示的对应操作)。

其中2和3对应的代码,都放在一个while循环中。而在3中所谓的“对应的操作",还可以包括一种“退出"操作,这样的话,就可以从while循环中退出,这样的话,整个进程也有机会正常结束。

 

再次提醒一下,请先把上面这段代码看懂,最好是有过实际的使用经验,这样更有助于理解。

 

下面开始讨论重点。这段代码仅仅是演示,所以它很简单。但是,从另外一个角度来看,这个代码片段又很死板,尤其是对于新手或者是没有I/O多路复用实际使用经验的朋友来说,很容易被这段代码模型“框住"。它还能变得更灵活吗?怎样才能变得更灵活?详细解释之前,先提几个小问题。

1. 前面的代码,仅打开了2个文件,并且传递给poll函数。如果,在程序运行过程中,想动态的增加或者删除poll函数监控的文件,怎么办?

2. 前面的代码,设置的超时时间,是固定的。假设,某个时刻,有100个文件需要被监控,而针对这100个不同的文件,每个文件期望设置的超时时间都不一样,怎么办?

3. 前面的代码,当poll函数返回,对文件集合进行遍历的时候,是逐个进行判断并且执行“对应的操作"。如果,有100个文件被监控,当poll返回时,这100个文件,都满足条件,可以进行“对应的操作",其中的50个文件的“对应的操作"很耗时间,但是并不是这么紧急(可以稍后再处理,比如等到下一轮poll返回时再处理),而另外50个文件的“对应的操作"需要立即执行,并且很快(在下一次poll的时候)又会有新的事件发生并且满足判断时的条件,怎么办?

 

对第1个问题,可以想到,需要对所有的文件(struct pollfd)做一个统一的管理,需要有添加和删除文件的功能。用面向对象的思想来看,这就是一个类,暂且叫做类A。

对第2个问题,可以想到,还需要对每一个被监控的文件(struct pollfd),做更多的控制。也可以用一个类来包装被监控的文件,对这个文件进行管理,在该对象中,包含了struct pollfd结构体,该类还可以提供对应的文件所期望的超时时间。暂且叫做类B。

对第3个问题,可以考虑为每一个被监控的文件设置一个优先级,然后就可以根据优先级优先执行更“紧急"的“对应的操作"。这个优先级信息,也可以存储在类B中。设计出了类B之后,类A就不再是直接统一管理文件了,而是变成统一管理类B,可以看成是类B的一个容器类。

 

 

有了这3个解答之后,就可以对这个代码片段添油加醋,重新组装,让它变得更灵活了。glib中的GMainLoop,做的就是这样的事情,而且,它做的事情,除了这3个解答中描述的内容外,还有更让人“吃惊的惊喜"。

 

:-),这里又要提醒一下了,下面将对GMainLoop进行描述,所以,最好是先使用一下GMainLoop,包括其中的g_timeout_source_new(guint interval),g_idle_source_new(void)以及g_child_watch_source_new(GPid pid)。顺便再强调一下,学习编程的最好的办法,就是看代码,而且是看高质量的代码。

 

后面的讲解,主要是从原理上来介绍GMainLoop的实现机制,并不是代码的情景分析。代码的详细阅读,还是需要自己老老实实的去实践的。后面的这些介绍,只是为了帮助大家更容易的理解源代码。

 

glib的主事件循环框架,由3个类来实现,GMainLoop,GMainContext和GSource,其中的GMainLoop仅仅是GMainContext的一个外壳,最重要的,还是GMainContext和GSource。GMainContext就相当于前面提到的类A,而GSource就相当于前面提到的类B。从原理上讲,g_main_loop_run(GMainLoop *loop)这个函数的内部实现,和前面代码片段中的while循环,是一致的。(还有一点要说明的,在多线程的环境下,GMainLoop的代码实现显得比较复杂,为了学习起来更容易些,可以先不考虑GMainLoop中线程相关的代码,这样的话,整体结构就和前面的代码片段是一致的。后面的讲解以及代码片段,都略去了线程相关的代码,这并不影响对event loop的学习和理解)。

 

1.GSource----GSource相当于前面提到的类B,它里面会保存优先级信息,同时,GSource要管理对应的文件(保存struct pollfd结构体的指针,而且是以链表的形式保存),而且,GSource和被管理的文件的对应关系,不是 1对1,而是 1对n,这个n,甚至可以是0(这就是一个“吃惊的惊喜",后面会有更详细的解释)。GSource还必须提供3个重要的函数(从面向对象的角度看,GSource是一个抽象类,而且有三个重要的纯虚函数,需要子类来具体实现),这3个函数就是:

  gboolean (*prepare)  (GSource *source, gint *timeout_);

  gboolean (*check)    (GSource *source);

  gboolean (*dispatch) (GSource *source, GSourceFunc callback, gpointer user_data);

 

再看一下前面代码片段中的3部分,这个prepare函数,就是要在第一部分被调用的,check和dispathch函数,就是在第3部分被调用的。有一点区别是,prepare函数,也要放到while循环中,而不是在循环之外(因为要动态的增加或者删除poll函数监控的文件)。

 

prepare函数,会在执行poll之前被调用。该GSource中的struct pollfd是否希望被poll函数监控,就由prepare函数的返回值来决定,同时,该GSource希望的超时时间,也由参数timeout_返回。

check函数,在执行poll之后被调用。该GSource中的struct pollfd是否有事件发生,就由check函数的返回值来描述(在check函数中可以检测struct pollfd结构体中的返回信息)。

dispatch函数,在执行poll和check函数之后被调用,并且,仅当对应的check函数返回true的时候,对应的dispatch函数才会被调用,dispatch函数,就相当于“对应的操作"。

 

2.GMainContext----GMainContext是GSource的容器,GSource可以添加到GMainContext里面(间接的就把GSource中的struct pollfd也添加到GMainContext里面了),GSource也可以从GMainContext中移除(间接的就把GSource中的struct pollfd从GMainContext中移除了)。GMainContext可以遍历GSource,自然就有机会调用每个GSource的prepare/check/dispatch函数,可以根据每个GSource的prepare函数的返回值来决定,是否要在poll函数中,监控该GSource管理的文件。当然可以根据GSource的优先级进行排序。当poll返回后,可以根据每个GSource的check函数的返回值来决定是否需要调用对应的dispatch函数。

 

下面给出关键的代码片段,其中的g_main_context_iterate()函数,就相当于前面代码片段中的循环体中要做的动作。循环的推出,则是靠loop->is_running这个标记变量来标识的。

 

 

 

 

 

仔细看一下g_main_context_iterate()函数,也可以把它划分成3个部分,和前面代码片段的3部分对应上。

1. 第一部份,准备要检测的文件集合

 

 

首先是调用g_main_context_prepare(context, &max_priority),这个就是遍历每个GSource,调用每个GSource的prepare函数,选出一个最高的优先级max_priority,函数内部其实还计算出了一个最短的超时时间。

然后调用g_main_context_query,其实这是再次遍历每个GSource,把优先级等于max_priority的GSource中的struct pollfd,添加到poll的监控集合中。

 

这个优先级,也是一个“吃惊的惊喜"。按照通常的想法,文件需要被监控的时候,会立刻把它放到监控集合中,但是有了优先级这个概念后,我们就可以有一个“隐藏的后台任务",g_idle_source_new(void)就是最典型的例子。

 

2. 第二部份,执行poll,等待事件发生。

 

就是调用g_main_context_poll(context, timeout, max_priority, fds, nfds),g_main_context_poll只是对poll函数的一个简单封装。

 

3. 第三部分,遍历文件集合(struct pollfd结构体的集合),执行对应的操作。

 

 

通常的想法,可能会是这种伪代码形式(这种形式也和前面代码片段的形式是一致的)

foreach(all_gsouce) {

    if (gsourc->check) {

     gsource->dispatch();

    }

}

 

实际上,glib的处理方式是,先遍历所有的GSource,执行g_main_context_prepare(context, &max_priority),调用每个GSource的check函数,然后把满足条件的GSource(check函数返回true的GSource),添加到一个内部链表中。

然后执行g_main_context_dispatch(context),遍历刚才准备好的内部链表中的GSource,调用每个GSource的dispatch函数。

 

ok,分析到此结束,总结一下,重点,首先是要先理解poll函数的使用方法,建立I/O多路复用的概念,然后,建议看一下GMainContext的源代码实现,这样才有助于理解。

 

 

  • 5
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
以下是一个简单的使用 gstreamer C 语言实现音视频同步的代码示例: ``` #include <gst/gst.h> static gboolean bus_call(GstBus *bus, GstMessage *msg, gpointer data) { GMainLoop *loop = (GMainLoop *)data; switch (GST_MESSAGE_TYPE(msg)) { case GST_MESSAGE_EOS: g_print("End of stream\n"); g_main_loop_quit(loop); break; case GST_MESSAGE_ERROR: { gchar *debug; GError *error; gst_message_parse_error(msg, &error, &debug); g_free(debug); g_printerr("Error: %s\n", error->message); g_error_free(error); g_main_loop_quit(loop); break; } default: break; } return TRUE; } int main(int argc, char *argv[]) { GMainLoop *loop; GstElement *pipeline, *src, *sink, *audio_sink, *video_sink, *convert; GstBus *bus; guint bus_watch_id; GstPad *video_pad, *audio_pad; /* Initialize GStreamer */ gst_init(&argc, &argv); /* Create the elements */ src = gst_element_factory_make("filesrc", "source"); convert = gst_element_factory_make("decodebin", "convert"); audio_sink = gst_element_factory_make("autoaudiosink", "audio_sink"); video_sink = gst_element_factory_make("autovideosink", "video_sink"); /* Create the empty pipeline */ pipeline = gst_pipeline_new("test-pipeline"); if (!pipeline || !src || !convert || !audio_sink || !video_sink) { g_printerr("Not all elements could be created.\n"); return -1; } /* Build the pipeline */ gst_bin_add_many(GST_BIN(pipeline), src, convert, audio_sink, video_sink, NULL); if (gst_element_link(src, convert) != TRUE || gst_element_link(convert, audio_sink) != TRUE || gst_element_link(convert, video_sink) != TRUE) { g_printerr("Elements could not be linked.\n"); gst_object_unref(pipeline); return -1; } /* Set the URI to play */ g_object_set(src, "location", argv[1], NULL); /* Connect to the bus */ loop = g_main_loop_new(NULL, FALSE); bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline)); bus_watch_id = gst_bus_add_watch(bus, bus_call, loop); gst_object_unref(bus); /* Start playing */ gst_element_set_state(pipeline, GST_STATE_PLAYING); /* Wait for the playback to finish */ g_main_loop_run(loop); /* Clean up */ gst_element_set_state(pipeline, GST_STATE_NULL); gst_object_unref(GST_OBJECT(pipeline)); g_source_remove(bus_watch_id); g_main_loop_unref(loop); return 0; } ``` 这个示例代码使用了 GStreamer 库来实现音视频播放,首先需要创建各个元素(例如`filesrc`、`decodebin`、`autoaudiosink`、`autovideosink`),然后将它们添加到管道中,并且使用`gst_element_link`函数将它们连接起来。接着,设置要播放的音视频文件的 URI,并将管道设置为播放状态。最后,使用`g_main_loop_run`函数等待播放结束。在播放过程中,使用`bus_call`函数来处理各种 GStreamer 消息,例如错误消息和结束消息。 在音视频同步方面,需要使用`gst_pad_set_offset`函数来设置音频和视频之间的时间偏移值,以确保它们能够同步播放。在管道中连接`decodebin`元素后,可以使用`gst_element_get_static_pad`函数获取音频和视频源的输入端口,并使用`gst_pad_set_offset`函数设置它们之间的时间偏移值。 由于每个音视频文件的编码和格式都不同,因此需要根据实际情况进行修改和调整。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值