1. 程式人生 > >關於glib的一些知識記錄

關於glib的一些知識記錄

_priv->mainloop =   g_main_loop_new( _priv->context, FALSE );    \\建立一個GMainloop

_priv->thread   =   g_thread_new( "thread", threadEntry, NULL );    \\建立一個GThread

2. 執行緒入口

g_main_loop_run( _priv->mainloop );    \\主要實現,執行緒等待GSource事件

3. 執行緒退出與資源釋放

在需要終止執行緒的地方呼叫如下程式碼

g_main_loop_quit( _priv->mainloop );    \\GMainloop退出

g_thread_join( _priv->thread );    \\重要,等待執行緒成功退出,glib內部會對GThread的reference count減1,執行緒資源才會釋放

g_main_loop_unref( _priv->mainloop );     \\釋放之前建立的GMainloop
g_main_context_unref( _priv->context );    \\釋放之前建立的GMainContext

main loop使用模式大致如下:

loop = g_main_loop_new (NULL, TRUE);

g_main_loop_run (loop);

g_main_loop_new建立一個main loop物件,一個main loop物件只能被一個執行緒使用,但一個執行緒可以有多個main loop物件。在GTK+應用中,一個執行緒使用多個main loop的主要用途是實現模態對話方塊,它在gtk_dialog_run函式裡建立一個新的main loop,通過該main loop分發訊息,直到對話方塊關閉為止。

g_main_loop_run則是進入主迴圈,它會一直阻塞在這裡,直到讓它退出為止。有事件時,它就處理事件,沒事件時就睡眠。

g_main_loop_quit則是用於退出主迴圈,相當於Win32下的PostQuitMessage

函式。

Glib main loop的最大特點就是支援多事件源,使用非常方便。來自使用者的鍵盤和滑鼠事件、來自系統的定時事件和socket事件等等,還支援一個稱為idle的事件源,其主要用途是實現非同步事件。Main loop的基本組成如下圖所示:

GMainLoop的主要部件是GMainContextGMainContext可以在多個GMainLoop間共享,但要求這些GMainLoop都在同一個執行緒中執行,前面提到的模態對話方塊就屬於這一類。GMainContext通常由多個GSource組成,GSource是事件源的抽象,任何事件源,只要實現GSource規定的介面,都可以掛到GMainContext中來。

GSource的介面函式有:

1.        gboolean (*prepare)  (GSource    *source, gint       *timeout_);進入睡眠之前,在g_main_context_prepare裡,mainloop呼叫所有Sourceprepare函式,計算最小的timeout時間,該時間決定下一次睡眠的時間。

2.        gboolean (*check)    (GSource    *source); poll被喚醒後,在g_main_context_check裡,mainloop呼叫所有Sourcecheck函式,檢查是否有Source已經準備好了。如果poll是由於錯誤或者超時等原因喚醒的,就不必進行dispatch了。

3.        gboolean (*dispatch) (GSource*source, GSourceFunc callback,gpointer user_data);當有Source準備好了,在g_main_context_dispatch裡,mainloop呼叫所有Sourcedispatch函式,去分發訊息。

4.        void     (*finalize) (GSource    *source);Source被移出時,mainloop呼叫該函式去銷燬Source

Main loop的工作流程簡圖如下

下面我們看看幾個內建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管道實現的,mainlooppoll時,它除了等待所有的Source外,還會等待wake_up_pipe管道。要喚醒poll,呼叫g_main_context_wakeup_unlockedwake_up_pipe裡寫入字母A就行了。

從文章中摘出來一些,然後自己整理了一下。

GLib 實現了一個功能強大的事件迴圈分發處理機制,GLib 內部實現了三種類型的事件源,分別是 Timeout, Idle, Child Watch。

同時也支援建立自定義的事件源——也就是新增child watch

李先靜老師的FTK就是仿照glib的事件源來實現的

1. Timeout事件源

//mainloop1.c
#include<glib.h> 
GMainLoop* loop;

gint counter = 10;
gboolean callback(gpointer arg)
{
    g_print(".");
    if(--counter ==0){
        g_print("\n");
        //退出迴圈
        g_main_loop_quit(loop);
        //登出定時器
        return FALSE;
    }
    //定時器繼續執行
    return TRUE;
}

int main(int argc, char* argv[])
{
    if(g_thread_supported() == 0)
        g_thread_init(NULL);
    g_print("g_main_loop_new\n");
    loop = g_main_loop_new(NULL, FALSE);
    //增加一個定時器,100毫秒執行一次callback
    g_timeout_add(100,callback,NULL);
    g_print("g_main_loop_run\n");
    g_main_loop_run(loop);
    g_print("g_main_loop_unref\n");
    g_main_loop_unref(loop);
    return 0;
}
編譯執行:
gcc -g `pkg-config --cflags --libs glib-2.0 gthread-2.0` mainloop1.c -o mainloop1

2. Idle事件源

已經在另一篇部落格中介紹了

3. Child Watch——系統定義的事件源

#include <glib.h>    
#include <stdio.h>    
#include <strings.h>    
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;    
}

4. Child Watch——自定義事件源

GSource * g_source_new(GSourceFuncs * source_funcs, guint struct_size);

這個函式用於建立一個自定義事件源,新的事件源可以使用 g_source_attach() 函式加入到主迴圈上下文中。
source_funcs : 包含用於實現事件行為的函式的結構
struct_size : 建立的 GSource 結構大小,不能小於 sizeof(GSource)
返回值 : 返回新建立的 GSource

建立一個新的事件源包含用於實現事件行為的函式的結構體。
prepare : 設定檢查事件時間超時。如果返回 TRUE, check 會立刻被呼叫;如果返回 FALSE 並設定了 timeout , timeout 時間後 check 會被呼叫。
check : 檢查事件是否準備完畢。返回 TRUE 為準備完畢, dispatch 會被立刻呼叫;返回 FALSE 不呼叫 dispatch,進入下一次事件迴圈。
dispatch : 分發事件。返回 TRUE 將繼續下一次操作迴圈;返回 FALSE 中止本事件源的事件迴圈。
finalize : 當事件源被移除時被呼叫。

#include <glib.h>

gboolean source_prepare_cb(GSource * source,
            gint * timeout)
{
    g_printf("prepare\n");
    *timeout = 1000;
    return FALSE;
}

gboolean source_check_cb(GSource * source)
{
    g_printf("check\n");
    return TRUE;
}

gboolean source_dispatch_cb(GSource * source,
            GSourceFunc callback, gpointer data)
{
    g_printf("dispatch\n");
    return TRUE;
}

void source_finalize_cb(GSource * source)
{
    g_printf("finalize\n");
}

int main(int argc, char * argv[])
{
    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_main_loop_run(mainloop);

    return 0;
}

注意prepare 中會返回一個超時時間

g_main_loop_new

按著glib的文件順序,先來看看事件迴圈吧。
從最簡單的例子開始:
//mainloop0.c
#include<glib.h> 
GMainLoop* loop;
int main(int argc, char* argv[])
{
    //g_thread_init是必需的,GMainLoop需要gthread庫的支援。
    if(g_thread_supported() == 0)
        g_thread_init(NULL);
    //建立一個迴圈體,先不管引數的意思。
    g_print("g_main_loop_new/n");
    loop = g_main_loop_new(NULL, FALSE);
    //讓這個迴圈體跑起來
    g_print("g_main_loop_run/n");
    g_main_loop_run(loop);
    //迴圈執行完成後,計數器減一
    //glib的很多結構型別和c++的智慧指標相似,擁有一個計數器
    //當計數器為0時,自動釋放資源。
    g_print("g_main_loop_unref/n");
    g_main_loop_unref(loop);
    return 0;
}
好了,現在編譯:
gcc -g `pkg-config --cflags --libs glib-2.0 gthread-2.0` mainloop0.c -o mainloop0
然後執行:
./mainloop0
你會發現程式會在g_main_loop_run函式阻塞,這就是glib main loop了,如果沒有人通知它退出,它是不會退出的。
通知迴圈退出的函式是g_main_loop_quit。
怎麼通知呢?主執行緒被g_main_loop_run阻塞了,沒辦法執行quit。本來我準備開一個執行緒,sleep一秒鐘,然後呼叫g_main_loop_quit。不過一想我們都在學習高精尖武器了,還用土槍土炮幹啥。使用glib的定時器吧~
//mainloop1.c
#include<glib.h> 
GMainLoop* loop;
gint counter = 10;
gboolean callback(gpointer arg)
{
    g_print(".");
    if(--counter ==0){
        g_print("/n");
        //退出迴圈
        g_main_loop_quit(loop);
        //登出定時器
        return FALSE;
    }
    //定時器繼續執行
    return TRUE;
}
int main(int argc, char* argv[])
{
    if(g_thread_supported() == 0)
        g_thread_init(NULL);
    g_print("g_main_loop_new/n");
    loop = g_main_loop_new(NULL, FALSE);
    //增加一個定時器,100毫秒執行一次callback
    g_timeout_add(100,callback,NULL);
    g_print("g_main_loop_run/n");
    g_main_loop_run(loop);
    g_print("g_main_loop_unref/n");
    g_main_loop_unref(loop);
    return 0;
}
編譯執行:
gcc -g `pkg-config --cflags --libs glib-2.0 gthread-2.0` mainloop1.c -o mainloop1
./mainloop1
Yeah!一秒鐘後,程式正常退出了!定時器好簡單!
今天到此為止。最後思考一個問題,glib的main loop主要提供給gtk使用,是gtk介面事件迴圈的基礎,這是無可非議的。但是,在別的地方,比如我們普通的console、service程式中,有必要用main loop麼?main loop還能夠應用在哪些場合?

-----------------------

回到前一天的問題,除了互動性介面程式,還有哪些地方適合使用glib的event loop呢?我認為答案應該是,所有需要非同步操作的地方都可以用event loop。像檔案、管道、裝置、socket、timer、idle和其他自定義的事件都可以產生event.
要讓GMainLoop能夠處理這些型別的event,首先就必須把它們加到GMainLoop去。
首先我們需要了解event loop的這三個基本結構: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。

接下來看這個例子,它的作用是從stdin讀取字串,然後反轉字串並輸出到螢幕。

//mainloop2.c
#include <glib.h>
#include <stdio.h>
#include <strings.h>

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;
}

1. 之前提到過,GSource和被管理的檔案的對應關係,不是 1對1,而是 1對n,這個n,甚至可以是0。這個的意思就是說,一個GSource,可以對應(管理)一個檔案描述符,也可以同時對應(管理)多個檔案描述符,也可以一個都不管理。

glib中已經提供了幾個GSource的子類,其中的g_timeout_source_new(guint interval)和g_idle_source_new(void)這兩個函式構造出的GSource子類,就並不使用檔案描述符號。

GTimeoutSource,只需要儲存設定的間隔時間,在poll輪循的prepare和check階段,會去檢查設定的這個間隔時間是否到達,如果到達設定的時間,則它的dispatch函式會被呼叫。

g_idle_source_new(void)其實都沒有繼承GSource,它也不需要和檔案繫結,只不過它的優先順序比較低,只有在poll輪詢的空閒階段,它的dispatch函式會被呼叫。而它的prepare和check函式,始終都是返回的TRUE。

2. GMainLoop怎樣根據GSource的優先順序進行排程?

在g_main_context_prepare()的時候,會從所有的GSource中找出最大的一個優先順序,然後在g_main_context_query()的時候,只會把等於這個優先順序的GSource對應的GPollFD新增到poll的監控集合中。基於這個優先順序制度,GMainLoop就可以對GSource進行一個排程。以idle source為例,如果同時有一個GTimeoutSource和idle source,因為idle source的優先順序更低,所有GMainLoop就不會把idle source新增到poll輪詢的監控集合中,也就是說,直到GTimeoutSource從GMainLoop中移除,idle source的callback函式,才有機會被呼叫。



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  print?
  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  print?
  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;