GMainLoop的實現原理和代碼模型

轉載時請註明出處和作者聯繫方式
文章出處:
http://blog.csdn.net/jack0106 
作者聯繫方式:馮牮 
[email protected]

 

 

做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的源代碼實現,這樣才有助於理解。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章