Glib之主事件循環


介紹

GLib和GTK+應用的主事件循環管理着所有事件源。這些事件的來源有很多種比如文件描述符(文件、管道或套接字)或超時。新類型的事件源可以通過g_source_attach()函數添加。
為了讓多組獨立事件源能夠在不同的線程中被處理,每個事件源都會關聯一個GMainContext。一個線程只能運行一個GMainContext,但是在其他線程中能夠對事件源進行添加和刪除操作。
每個事件源都被賦予了優先級。默認的優先級是G_PRIORITY_DEFAULT(0)。值越小優先級越高,優先級高的事件源優先處理。
Idle函數在沒有更高優先級的事件被處理的時候才會執行。
GMainLoop數據類型代表了一個主事件循環。通過g_main_loop_new()來創建GMainLoop對象。在添加完初始事件源后執行g_main_loop_run(),主循環將持續不斷的檢查每個事件源產生的新事件,然后分發它們,直到處理來自某個事件源的事件的時候觸發了g_main_loop_quit()調用退出主循環為止。
GMainLoop實例能夠被遞歸創建。在GTK+應用中經常使用這種方式來顯示模態對話框。注意如果一個事件源被添加到一個GMainContext,那么它將被所有關聯這個GMainContext的主線程檢查和分發。
GTK+對這些函數做了些封裝,例如gtk_main、gtk_mian_quit和gtk_events_pending。

自定義事件類型

GMainLoop一個不常用的特性就是能夠創建一個新的事件源類型,然后當做內置事件源的擴展來使用。一個新的事件源類型通常用來處理GDK事件。通過繼承GSource結構來創建一個新的事件源類型。繼承產生的新事件源類型表示GSource結構作為新事件源類型的第一個元素然后其他元素緊跟其后。使用g_source_new函數來創建新的事件源類型實例,函數的參數就是新的事件源類型大小。GSourceFuncs決定新的事件源類型的行為。
新的事件源有兩種基本方式與GMainContext交互。它們GSourceFuncs中的准備函數能夠設置睡眠事件,用來決定主循環下次檢測它們的時間。此外事件源也可以使用g_source_add_poll()函數添加文件描述符到GMainContext進行檢測。

自定義主循環迭代

執行g_main_context_iteration()函數可以完成GMainContext的單次迭代。在一些情況下,我們可能想獲取主循環更多的底層控制細節,我們可以調用g_main_context_iteration()里的組件函數:g_main_context_prepare()g_main_context_prepare ()g_main_context_query()g_main_context_check()g_main_context_dispatch()

Main Context狀態

在UNIX系統上,GLib的主循環和fork()是不兼容的。

事件源內存管理

有兩種可選的方式來管理傳遞給GSource回調函數用戶數據的內存。用戶數據就是在調用g_timeout_add()g_timeout_add_full()g_idle_add()傳入的參數。這些數據通常被timeout或idle回調函數所擁有,比如一個構件或一個網路協議的實現。有些時候這些回調函數會在數據被銷毀的后背調用,因為使用了已經被釋放的內存,所以這會導致一個錯誤。

  • 第一種推薦的方法就是保存g_timeout_add()g_source_attach()返回的事件源ID,然后在其維持的用戶數據被釋放后顯示的將其從GMainContext移除。這樣就能保證調用這些回調函數的時候,這些用戶數據依然有效。
  • 第二種就是保存這些回調函數中的用戶數據對象的引用,然后在GDestroyNotify回調函數中釋放它。這樣就能確保數據對象在事件源最后一次調用然后被釋放前一直有效。GDestroyNotify回調函數是GSource函數的一個變體的入參,它在事件源被釋放時調用。
    第二條途徑有必要提醒下,如果在事件源還沒被調用前主循環就結束的情況下,用戶數據對象被維持狀態是不確定的。

代碼

數據結構

struct _GSourcePrivate
{
  GSList *child_sources;
  GSource *parent_source;

  gint64 ready_time;

  /* This is currently only used on UNIX, but we always declare it (and
   * let it remain empty on Windows) to avoid #ifdef all over the place.
   */
  GSList *fds;
};

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

  const GSourceFuncs *source_funcs;
  guint ref_count;

  GMainContext *context;

  gint priority;
  guint flags;
  guint source_id;

  GSList *poll_fds;

  GSource *prev;
  GSource *next;

  char    *name;

  GSourcePrivate *priv;
};

struct _GPollRec
{
  GPollFD *fd;
  GPollRec *prev;
  GPollRec *next;
  gint priority;
};

struct _GMainContext
{
  /* The following lock is used for both the list of sources
   * and the list of poll records
   */
  GMutex mutex;
  GCond cond;
  GThread *owner;
  guint owner_count;
  GSList *waiters;

  gint ref_count;

  GHashTable *sources;              /* guint -> GSource */

  GPtrArray *pending_dispatches;
  gint timeout;            /* Timeout for current iteration */

  guint next_id;
  GList *source_lists;
  gint in_check_or_prepare;

  GPollRec *poll_records;
  guint n_poll_records;
  GPollFD *cached_poll_array;
  guint cached_poll_array_size;

  GWakeup *wakeup;

  GPollFD wake_up_rec;

/* Flag indicating whether the set of fd's changed during a poll */
  gboolean poll_changed;

  GPollFunc poll_func;

  gint64   time;
  gboolean time_is_fresh;
};

struct _GMainLoop
{
  GMainContext *context;
  gboolean is_running;
  gint ref_count;
};
函數 說明
g_main_context_add_poll_unlocked 在g_source_add_poll和g_source_add_unix_fd中被調用,首先將需要監聽的文件描述符保存到_GSource->poll_fds或_GSource->priv->fds中,然后再創建個_GPollRec對象,接着將其保存到_GMainContext->poll_records中,最后設置context->poll_changed為True,這個值會影響當前主循環的g_main_context_check
g_source_attach_unlocked 在g_source_attach和g_source_add_child_source中被調用,將_GSource保存到_GMainContext->sources,並將_GSource保存的文件描述符通過g_main_context_add_poll_unlocked函數保存到_GMainContext,最后返回_GSource->source_id
g_main_context_prepare 遍歷_GMainContext擁有的事件源,調用事件源的_GSourceFuncs->prepare函數計算下次輪訓間隔,並檢測事件源是否已經就緒
g_main_context_query 從_GMainContext擁有的事件源中篩選出需要進行poll的事件源,並設置context->poll_changed為False(只要不是在poll期間對事件源進行添加或刪除操作即可)
g_main_context_poll 使用poll函數監聽g_main_context_query篩選出的事件源
g_main_context_check 遍歷g_main_context_query篩選出的事件源,調用事件源的_GSourceFuncs->check函數檢測事件源是否已經就緒,如果就緒則將事件源添加到_GMainContext->pending_dispatches中。如果context->poll_changed為True(說明在poll的時候有新的事件源加入或移除),則跳過后面的分發(pending_dispatches為空),重新執行g_main_context_iterate
g_main_context_dispatch 遍歷_GMainContext->pending_dispatches中的事件源,並調用事件源的_GSourceFuncs->dispatch函數進行分發

idle事件源

#define G_PRIORITY_DEFAULT_IDLE     200

GSourceFuncs g_idle_funcs =
{
  g_idle_prepare,
  g_idle_check,
  g_idle_dispatch,
  NULL
};

/* Idle functions */

static gboolean g_idle_prepare(GSource *source, gint *timeout)
{
  *timeout = 0;

  return TRUE;
}

static gboolean g_idle_check(GSource *source)
{
  return TRUE;
}

static gboolean g_idle_dispatch (GSource *source, GSourceFunc callback, gpointer user_data)
{
  gboolean again;

  if (!callback)
    {
      g_warning ("Idle source dispatched without callback\n"
         "You must call g_source_set_callback().");
      return FALSE;
    }

  again = callback (user_data);

  TRACE (GLIB_IDLE_DISPATCH (source, source->context, callback, user_data, again));

  return again;
}
函數 說明
g_idle_add 創建一個idle事件源,然后添加到GMainContext

timeout事件源

#define G_PRIORITY_DEFAULT          0

GSourceFuncs g_timeout_funcs =
{
  NULL, /* prepare */
  NULL, /* check */
  g_timeout_dispatch,
  NULL
};

static gboolean g_timeout_dispatch (GSource *source, GSourceFunc callback, gpointer user_data)
{
  GTimeoutSource *timeout_source = (GTimeoutSource *)source;
  gboolean again;

  if (!callback)
    {
      g_warning ("Timeout source dispatched without callback\n"
                 "You must call g_source_set_callback().");
      return FALSE;
    }

  again = callback (user_data);

  TRACE (GLIB_TIMEOUT_DISPATCH (source, source->context, callback, user_data, again));

  if (again)
    g_timeout_set_expiration (timeout_source, g_source_get_time (source));

  return again;
}
函數 說明
g_timeout_add 創建一個timeout事件源,然后添加到GMainContext。timeout事件源沒有自己的prepare和check函數,是因為在g_main_context_prepare和g_main_context_check 中都會獲取下當前系統時間,然后和timeout事件源的對比,如果時間已經過了,則將事件源的狀態修改為就緒狀態,所以就不需要自己來寫了

GIOChannel事件源

struct _GIOChannel
{
  /*< private >*/
  gint ref_count;
  GIOFuncs *funcs;

  gchar *encoding;
  GIConv read_cd;
  GIConv write_cd;
  gchar *line_term; /* String which indicates the end of a line of text */
  guint line_term_len; /* So we can have null in the line term */

  gsize buf_size;
  GString *read_buf; /* Raw data from the channel */
  GString *encoded_read_buf;    /* Channel data converted to UTF-8 */
  GString *write_buf; /* Data ready to be written to the file */
  gchar partial_write_buf[6];    /* UTF-8 partial characters, null terminated */

  /* Group the flags together, immediately after partial_write_buf, to save memory */

  guint use_buffer     : 1;    /* The encoding uses the buffers */
  guint do_encode      : 1;    /* The encoding uses the GIConv coverters */
  guint close_on_unref : 1;    /* Close the channel on final unref */
  guint is_readable    : 1;    /* Cached GIOFlag */
  guint is_writeable   : 1;    /* ditto */
  guint is_seekable    : 1;    /* ditto */

  gpointer reserved1;
  gpointer reserved2;
};

struct _GIOFuncs
{
  GIOStatus (*io_read)(GIOChannel *channel, gchar *buf, gsize count, gsize *bytes_read, GError **err);
  GIOStatus (*io_write)(GIOChannel *channel, const gchar *buf, gsize count, gsize *bytes_written, GError **err);
  GIOStatus (*io_seek)(GIOChannel *channel, gint64 offset, GSeekType type, GError **err);
  GIOStatus (*io_close)(GIOChannel *channel, GError **err);
  GSource* (*io_create_watch)(GIOChannel *channel, GIOCondition  condition);
  void (*io_free)(GIOChannel *channel);
  GIOStatus (*io_set_flags)(GIOChannel *channel, GIOFlags flags, GError **err);
  GIOFlags (*io_get_flags)(GIOChannel *channel);
};

GSourceFuncs g_io_watch_funcs = {
  g_io_unix_prepare,
  g_io_unix_check,
  g_io_unix_dispatch,
  g_io_unix_finalize
};

static GIOFuncs unix_channel_funcs = {
  g_io_unix_read,
  g_io_unix_write,
  g_io_unix_seek,
  g_io_unix_close,
  g_io_unix_create_watch,
  g_io_unix_free,
  g_io_unix_set_flags,
  g_io_unix_get_flags,
};
函數 說明
g_io_channel_new_file 獲取並保存文件句柄,然后使用unix_channel_funcs初始化_GIOChannel->funcs
g_io_create_watch 使用g_io_unix_create_watch函數將channel保存的文件句柄轉換為事件源,事件源函數為g_io_watch_funcs
g_io_add_watch 使用g_io_create_watch創建事件源,然后附加到當前主循環的GMainContext
g_io_unix_prepare 獲取channel的輸入輸出buffer的數據狀態,並與需要監聽的channel事件進行對比
g_io_unix_check 獲取channel的輸入輸出buffer的數據狀態,並與channel實際觸發的事件進行對比


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM