GLib 主迴圈

最後更新: 2026-05-24 01:10:27 +0800 +0800
tags: Glib | Gtk
原文以英文撰寫。您正在閱讀AI翻譯版本。

GLib 的事件迴圈將事件描述與事件分派分開。GSource 描述工作何時就緒,GMainContext 擁有並排程一組來源,而 GMainLoop 反覆疊代一個內容。保持這些角色分離,可以明確表示回呼放置、取消與生命週期規則。

來源、內容與迴圈 #

主內容的一次疊代有四個概念階段:

  1. 準備: 來源回報是否已經就緒,以及內容可以休眠多久。
  2. 查詢與輪詢: 內容建立檔案描述元集合,並等待事件或逾時喚醒。
  3. 檢查: 來源檢查輪詢結果並決定是否就緒。
  4. 分派: 就緒來源呼叫其回呼。

g_main_context_iteration() 執行一次疊代。較低階的 g_main_context_prepare()、g_main_context_query()、g_main_context_check() 與 g_main_context_dispatch() 呼叫,存在於與另一個事件迴圈整合的情境,但應用程式必須保留其順序與內容取得規則。

GMainLoop 在該機制外加入執行旗標:

c
GMainContext *context = g_main_context_new();
GMainLoop *loop = g_main_loop_new(context, FALSE);

g_main_loop_run(loop);
/* A callback eventually calls g_main_loop_quit(loop). */

g_main_loop_unref(loop);
g_main_context_unref(context);

g_main_loop_quit() 會在目前分派返回後停止迴圈。它不會銷毀內容或其來源。多個迴圈可以疊代同一內容,包括巢狀迴圈,因此巢狀執行允許重新進入附加至該內容的每個來源。GTK 應用程式應優先使用非同步 API,而不是巢狀迴圈,因為看似同步的程式碼可能會分派無關的 UI 回呼。

就緒、優先權與所有權 #

每個來源都有整數優先權。數字越小表示優先權越高:G_PRIORITY_HIGH 會在 G_PRIORITY_DEFAULT 之前執行,而 G_PRIORITY_DEFAULT_IDLE 的優先權較低。當不同優先權的來源就緒時,GLib 會先分派較高優先權的工作。因此,持續就緒的高優先權來源可能使低優先權來源飢餓。程式碼不得依賴同一優先權來源之間未記錄的順序。

附加來源會將它關聯至最多一個內容:

c
GSource *source = g_timeout_source_new(250);

g_source_set_priority(source, G_PRIORITY_DEFAULT);
g_source_set_callback(source, refresh_cache, cache, NULL);
guint source_id = g_source_attach(source, context);
g_source_unref(source); /* The context now keeps the source alive. */

回呼回傳 G_SOURCE_CONTINUE 以保持排程,或回傳 G_SOURCE_REMOVE 以在分派後銷毀來源。g_source_destroy() 是直接的取消操作,可以從另一執行緒呼叫。來源 ID 僅對內容有效,最終可能重用;當程式碼擁有非預設內容時,保留 GSource * 較不含糊。

附加本身不會轉移應用程式資料的所有權。當回呼資料需要清理時,將 GDestroyNotify 傳給 g_source_set_callback() 或 *_add_full() 輔助函式。當回呼被替換或來源完成時,通知函式會執行,而且不得假設特定執行緒。

內建與自訂來源 #

GLib 為常見的就緒條件提供來源建構器:

  • g_timeout_source_new() 與 g_timeout_source_new_seconds() 會在一段間隔後就緒。重複逾時會根據 GLib 的單調時間計算排程下一個間隔;它們不是精確的週期性即時計時器。
  • g_idle_source_new() 在沒有較高優先權工作就緒時就緒。回傳 G_SOURCE_CONTINUE 的閒置回呼必須快速讓出控制權。
  • g_unix_fd_source_new() 會針對 GIOCondition 監視 Unix 檔案描述元。子程序監視與 Unix 訊號來源遵循相同的附加與分派模型。
  • g_io_create_watch() 會調整 GIOChannel,但新的 Unix 程式碼通常可以直接監視非阻塞檔案描述元。

便利函式 g_timeout_add()、g_idle_add() 與 g_child_watch_add() 會附加至全域預設內容,而不是目前執行緒預設的內容。具備內容意識的函式庫程式碼應建立來源並明確附加:

c
GSource *source = g_idle_source_new();

g_source_set_callback(source, publish_result, result, result_free);
g_source_attach(source, target_context);
g_source_unref(source);

自訂來源將 GSource 嵌入為第一個欄位,並提供 GSourceFuncs:

c
typedef struct {
  GSource source;
  gboolean pending;
} WorkSource;

static gboolean
work_prepare(GSource *source, gint *timeout_ms)
{
  WorkSource *work = (WorkSource *) source;
  *timeout_ms = -1;
  return work->pending;
}

static gboolean
work_check(GSource *source)
{
  return ((WorkSource *) source)->pending;
}

static gboolean
work_dispatch(GSource *source, GSourceFunc callback, gpointer data)
{
  WorkSource *work = (WorkSource *) source;
  work->pending = FALSE;
  return callback(data);
}

static GSourceFuncs work_source_funcs = {
  work_prepare, work_check, work_dispatch, NULL
};

使用 g_source_new(&work_source_funcs, sizeof(WorkSource)) 建立實例。等待核心事件的來源應註冊輪詢描述元,而不是反覆回報自己就緒;否則會將迴圈變成忙碌等待。如果另一執行緒變更自訂來源狀態,請保護該狀態,並呼叫 g_main_context_wakeup(),使阻塞中的輪詢觀察到變更。

執行緒預設內容 #

g_main_context_default() 回傳的全域預設內容,與每執行緒的執行緒預設內容堆疊不同。GIO 非同步操作通常會擷取呼叫執行緒的執行緒預設內容,並在該處傳遞完成通知。

c
g_main_context_push_thread_default(worker_context);
start_async_operation();
g_main_context_pop_thread_default(worker_context);

每次 push 都必須在同一執行緒中有相對應的 pop。堆疊為空時,g_main_context_get_thread_default() 回傳 NULL,而 g_main_context_ref_thread_default() 回傳有參照的執行緒預設內容或全域預設內容。函式庫應記錄每個回呼與訊號傳遞的內容,而不是默默假設全域預設內容。

一次只有一個執行緒可以取得內容,但所有權可以在疊代之間移動。若要將工作傳送至通常由另一執行緒執行的內容,使用 g_main_context_invoke_full():

c
g_main_context_invoke_full(ui_context,
                           G_PRIORITY_DEFAULT,
                           apply_worker_result,
                           result,
                           result_free);

該函式會在 ui_context 擁有期間執行。如果呼叫端已擁有內容,呼叫可能直接執行。當內容是呼叫端的執行緒預設內容且呼叫端可以取得它時,也可能直接執行;因此它不是無條件的非同步佇列操作。需要執行緒親和性的設計,應讓目標內容由指定執行緒持續執行並擁有,然後使用 g_main_context_is_owner() 斷言預期內容。若需求是背景執行,而不是特定工作執行緒,請使用 GTask。

回呼生命週期 #

附加的來源可能比其回呼資料代表的物件存活更久。GLib 記錄兩種主要所有權策略:

  1. 儲存來源,並在擁有者釋放時銷毀它。這會將回呼生命週期限制在擁有者內。
  2. 為來源提供強參照,並使用 GDestroyNotify 釋放它。這可防止使用後釋放,但從未執行或從未銷毀的來源可能讓擁有者無限期存活。

當操作可能與其目標一同正常消失時,弱參照很有用:

c
typedef struct {
  GWeakRef target;
} PendingUpdate;

static gboolean
apply_update(gpointer user_data)
{
  PendingUpdate *pending = user_data;
  g_autoptr(GObject) target = g_weak_ref_get(&pending->target);

  if (target == NULL)
    return G_SOURCE_REMOVE;

  /* Update target while the temporary strong reference is held. */
  return G_SOURCE_REMOVE;
}

static void
pending_update_free(gpointer data)
{
  PendingUpdate *pending = data;
  g_weak_ref_clear(&pending->target);
  g_free(pending);
}

static void
schedule_update(GObject *target, GMainContext *target_context)
{
  PendingUpdate *pending = g_new0(PendingUpdate, 1);
  GSource *source = g_idle_source_new();

  g_weak_ref_init(&pending->target, target);
  g_source_set_callback(source, apply_update, pending, pending_update_free);
  g_source_attach(source, target_context);
  g_source_unref(source);
}

GWeakRef 具備執行緒安全性:g_weak_ref_get() 會以原子方式取得強參照,或回傳 NULL。使用 g_object_add_weak_pointer() 安裝的原始弱指標,不提供該跨執行緒保證。弱參照解決的是物件有效性,而不是同步;可變狀態仍需要限制在單一內容中,或使用明確鎖定。

上游參考資料 #

評論

在 GitHub 上閱讀或評論(發表評論需要登入)