GMainLoop的原型

转载时请注明出处和作者联系方式
文章出处: http://blog.csdn.net/jack0106  

作者联系方式:冯牮 fengjian0106@yahoo.com.cn

作者简明扼要的讲述了GMainLoop的基本原理。GMainLoop本身不知有多少价值,但是这个框架可以大家学习和使用。据说对理解qt和gtk的框架也很有帮助。下为原文:

做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接口的代码模型片段。

[cpp]  view plain copy
  1. ...
  2. struct pollfd fds[2];
  3. int timeout_msecs = 500;
  4. int ret;
  5. int i;
  6. /* Open STREAMS device. */
  7. fds[0].fd = open("/dev/dev0", ...);
  8. fds[1].fd = open("/dev/dev1", ...);
  9. fds[0].events = POLLOUT | POLLWRBAND;
  10. fds[1].events = POLLOUT | POLLWRBAND;
  11. while(1) {
  12. ret = poll(fds, 2, timeout_msecs);
  13. if (ret > 0) {
  14. /* An event on one of the fds has occurred. */
  15. for (i=0; i<2; i++) {
  16. if (fds[i].revents & POLLWRBAND) {
  17. /* Priority data may be written on device number i. */
  18. ...
  19. }
  20. if (fds[i].revents & POLLOUT) {
  21. /* Data may be written on device number i. */
  22. ...
  23. }
  24. if (fds[i].revents & POLLHUP) {
  25. /* A hangup has occurred on device number i. */
  26. ...
  27. }
  28. }
  29. }
  30. }
  31. ...
[cpp]  view plain copy
  1. ...  
  2. struct pollfd fds[2];  
  3. int timeout_msecs = 500;  
  4. int ret;  
  5. int i;  
  6. /* Open STREAMS device. */  
  7. fds[0].fd = open("/dev/dev0", ...);  
  8. fds[1].fd = open("/dev/dev1", ...);  
  9. fds[0].events = POLLOUT | POLLWRBAND;  
  10. fds[1].events = POLLOUT | POLLWRBAND;  
  11. while(1) {  
  12.     ret = poll(fds, 2, timeout_msecs);  
  13.     if (ret > 0) {  
  14.         /* An event on one of the fds has occurred. */  
  15.         for (i=0; i<2; i++) {  
  16.         if (fds[i].revents & POLLWRBAND) {  
  17.         /* Priority data may be written on device number i. */  
  18.         ...  
  19.         }  
  20.         if (fds[i].revents & POLLOUT) {  
  21.         /* Data may be written on device number i. */  
  22.         ...  
  23.         }  
  24.         if (fds[i].revents & POLLHUP) {  
  25.         /* A hangup has occurred on device number i. */  
  26.         ...  
  27.         }  
  28.         }  
  29.     }  
  30. }  
  31. ...  

上面这个代码,我们可以把它拆分成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这个标记变量来标识的。

[cpp]  view plain copy
  1. void g_main_loop_run (GMainLoop *loop)
  2. {
  3. GThread *self = G_THREAD_SELF;
  4. g_return_if_fail (loop != NULL);
  5. g_return_if_fail (g_atomic_int_get (&loop->ref_count) > 0);
  6. g_atomic_int_inc (&loop->ref_count);
  7. loop->is_running = TRUE;
  8. while (loop->is_running)
  9. g_main_context_iterate (loop->context, TRUE, TRUE, self);
  10. UNLOCK_CONTEXT (loop->context);
  11. g_main_loop_unref (loop);
  12. }
  13. static gboolean g_main_context_iterate(GMainContext *context, gboolean block,
  14. gboolean dispatch, GThread *self) {
  15. gint max_priority;
  16. gint timeout;
  17. gboolean some_ready;
  18. gint nfds, allocated_nfds;
  19. GPollFD *fds = NULL;
  20. UNLOCK_CONTEXT (context);
  21. if (!context->cached_poll_array) {
  22. context->cached_poll_array_size = context->n_poll_records;
  23. context->cached_poll_array = g_new (GPollFD, context->n_poll_records);
  24. }
  25. allocated_nfds = context->cached_poll_array_size;
  26. fds = context->cached_poll_array;
  27. UNLOCK_CONTEXT (context);
  28. g_main_context_prepare(context, &max_priority);
  29. while ((nfds = g_main_context_query(context, max_priority, &timeout, fds,
  30. allocated_nfds)) > allocated_nfds) {
  31. LOCK_CONTEXT (context);
  32. g_free(fds);
  33. context->cached_poll_array_size = allocated_nfds = nfds;
  34. context->cached_poll_array = fds = g_new (GPollFD, nfds);
  35. UNLOCK_CONTEXT (context);
  36. }
  37. if (!block)
  38. timeout = 0;
  39. g_main_context_poll(context, timeout, max_priority, fds, nfds);
  40. some_ready = g_main_context_check(context, max_priority, fds, nfds);
  41. if (dispatch)
  42. g_main_context_dispatch(context);
  43. LOCK_CONTEXT (context);
  44. return some_ready;
  45. }
[cpp]  view plain copy
  1. void g_main_loop_run (GMainLoop *loop)  
  2. {  
  3.     GThread *self = G_THREAD_SELF;  
  4.     g_return_if_fail (loop != NULL);  
  5.     g_return_if_fail (g_atomic_int_get (&loop->ref_count) > 0);  
  6.     g_atomic_int_inc (&loop->ref_count);  
  7.     loop->is_running = TRUE;  
  8.     while (loop->is_running)  
  9.        g_main_context_iterate (loop->context, TRUE, TRUE, self);  
  10.     UNLOCK_CONTEXT (loop->context);  
  11.     g_main_loop_unref (loop);  
  12. }  
  13. static gboolean g_main_context_iterate(GMainContext *context, gboolean block,  
  14.         gboolean dispatch, GThread *self) {  
  15.     gint max_priority;  
  16.     gint timeout;  
  17.     gboolean some_ready;  
  18.     gint nfds, allocated_nfds;  
  19.     GPollFD *fds = NULL;  
  20.     UNLOCK_CONTEXT (context);  
  21.     if (!context->cached_poll_array) {  
  22.         context->cached_poll_array_size = context->n_poll_records;  
  23.         context->cached_poll_array = g_new (GPollFD, context->n_poll_records);  
  24.     }  
  25.     allocated_nfds = context->cached_poll_array_size;  
  26.     fds = context->cached_poll_array;  
  27.     UNLOCK_CONTEXT (context);  
  28.     g_main_context_prepare(context, &max_priority);  
  29.     while ((nfds = g_main_context_query(context, max_priority, &timeout, fds,  
  30.             allocated_nfds)) > allocated_nfds) {  
  31.         LOCK_CONTEXT (context);  
  32.         g_free(fds);  
  33.         context->cached_poll_array_size = allocated_nfds = nfds;  
  34.         context->cached_poll_array = fds = g_new (GPollFD, nfds);  
  35.         UNLOCK_CONTEXT (context);  
  36.     }  
  37.     if (!block)  
  38.         timeout = 0;  
  39.     g_main_context_poll(context, timeout, max_priority, fds, nfds);  
  40.     some_ready = g_main_context_check(context, max_priority, fds, nfds);  
  41.     if (dispatch)  
  42.         g_main_context_dispatch(context);  
  43.     
  44.     LOCK_CONTEXT (context);  
  45.     return some_ready;  
  46. }  

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

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

[cpp]  view plain copy
  1. g_main_context_prepare(context, &max_priority);
  2. while ((nfds = g_main_context_query(context, max_priority, &timeout, fds,
  3. allocated_nfds)) > allocated_nfds) {
  4. LOCK_CONTEXT (context);
  5. g_free(fds);
  6. context->cached_poll_array_size = allocated_nfds = nfds;
  7. context->cached_poll_array = fds = g_new (GPollFD, nfds);
  8. UNLOCK_CONTEXT (context);
  9. }
[cpp]  view plain copy
  1. g_main_context_prepare(context, &max_priority);  
  2. while ((nfds = g_main_context_query(context, max_priority, &timeout, fds,  
  3. allocated_nfds)) > allocated_nfds) {  
  4. LOCK_CONTEXT (context);  
  5. g_free(fds);  
  6. context->cached_poll_array_size = allocated_nfds = nfds;  
  7. context->cached_poll_array = fds = g_new (GPollFD, nfds);  
  8. UNLOCK_CONTEXT (context);  
  9. }  

首先是调用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,等待事件发生。

[cpp]  view plain copy
  1. if (!block)
  2. timeout = 0;
  3. g_main_context_poll(context, timeout, max_priority, fds, nfds);
[cpp]  view plain copy
  1. if (!block)  
  2.         timeout = 0;  
  3.     g_main_context_poll(context, timeout, max_priority, fds, nfds);  

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

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

[cpp]  view plain copy
  1. some_ready = g_main_context_check(context, max_priority, fds, nfds);
  2. f (dispatch)
  3. g_main_context_dispatch(context);
[cpp]  view plain copy
  1.       some_ready = g_main_context_check(context, max_priority, fds, nfds);  
  2. f (dispatch)  
  3. g_main_context_dispatch(context);  

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

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的源代码实现,这样才有助于理解。


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值