GLib 的事件迴圈將事件描述與事件分派分開。GSource 描述工作何時就緒,GMainContext 擁有並排程一組來源,而 GMainLoop 反覆疊代一個內容。保持這些角色分離,可以明確表示回呼放置、取消與生命週期規則。
來源、內容與迴圈 #
主內容的一次疊代有四個概念階段:
- 準備: 來源回報是否已經就緒,以及內容可以休眠多久。
- 查詢與輪詢: 內容建立檔案描述元集合,並等待事件或逾時喚醒。
- 檢查: 來源檢查輪詢結果並決定是否就緒。
- 分派: 就緒來源呼叫其回呼。
g_main_context_iteration() 執行一次疊代。較低階的 g_main_context_prepare()、g_main_context_query()、g_main_context_check() 與 g_main_context_dispatch() 呼叫,存在於與另一個事件迴圈整合的情境,但應用程式必須保留其順序與內容取得規則。
GMainLoop 在該機制外加入執行旗標:
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 會先分派較高優先權的工作。因此,持續就緒的高優先權來源可能使低優先權來源飢餓。程式碼不得依賴同一優先權來源之間未記錄的順序。
附加來源會將它關聯至最多一個內容:
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() 會附加至全域預設內容,而不是目前執行緒預設的內容。具備內容意識的函式庫程式碼應建立來源並明確附加:
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:
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 非同步操作通常會擷取呼叫執行緒的執行緒預設內容,並在該處傳遞完成通知。
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():
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 記錄兩種主要所有權策略:
- 儲存來源,並在擁有者釋放時銷毀它。這會將回呼生命週期限制在擁有者內。
- 為來源提供強參照,並使用
GDestroyNotify釋放它。這可防止使用後釋放,但從未執行或從未銷毀的來源可能讓擁有者無限期存活。
當操作可能與其目標一同正常消失時,弱參照很有用:
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 上閱讀或評論(發表評論需要登入)