GLibのイベントループは、イベントの記述とイベントのディスパッチを分離します。
GSource は処理の準備が整う条件を記述し、GMainContext はソースの集合を
所有してスケジュールし、GMainLoop は一つのコンテキストを繰り返し反復します。
これらの役割を分離することで、コールバックの配置、キャンセル、および
ライフタイムの規則が明確になります。
ソース、コンテキスト、ループ #
メインコンテキストの1回の反復には、概念上、次の四つのフェーズがあります。
- 準備: ソースは、すでに準備が整っているか、およびコンテキストが スリープできる時間を報告します。
- 照会とポーリング: コンテキストはファイルディスクリプターの集合を作成し、 イベントまたはタイムアウトによって起こされるまで待機します。
- 確認: ソースはポーリング結果を調べ、準備が整ったかどうかを判断します。
- ディスパッチ: 準備が整ったソースがコールバックを呼び出します。
g_main_context_iteration() は1回の反復を実行します。より低水準の
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アプリケーションでは、
同期処理に見えるコードが無関係なUIコールバックをディスパッチする可能性が
あるため、ネストしたループより非同期APIを優先すべきです。
準備状態、優先度、所有権 #
すべてのソースには整数の優先度があります。小さい数値ほど優先度が高く、
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() ヘルパーに渡します。notify関数は、
コールバックが置き換えられたとき、またはソースがファイナライズされたときに
実行され、特定のスレッドで実行されると仮定してはなりません。
組み込みソースとカスタムソース #
GLibは、一般的な準備条件に対応するソースコンストラクターを提供します。
g_timeout_source_new()とg_timeout_source_new_seconds()は、 指定した時間の経過後に準備済みになります。繰り返しタイムアウトは、 GLibの単調時間計算に基づいて次の間隔をスケジュールします。正確な周期の リアルタイムタイマーではありません。g_idle_source_new()は、優先度がより高い処理が準備済みでないときに 準備済みになります。G_SOURCE_CONTINUEを返すアイドルコールバックは、 短時間で制御を返す必要があります。g_unix_fd_source_new()は、UnixファイルディスクリプターのGIOConditionを監視します。子プロセス監視ソースと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は、主に次の二つの所有権戦略を文書化しています。
- ソースを保存し、所有者がdisposeされるときに破棄する。これにより、 コールバックのライフタイムが所有者に制限されます。
- ソースに強参照を渡し、
GDestroyNotifyで解放する。これにより use-after-freeを防げますが、実行も破棄もされないソースが所有者を 無期限に存続させる可能性があります。
対象とともに操作が消えても問題ない場合は、弱参照が有用です。
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 で閲覧・コメントする(投稿にはログインが必要です)