Before you start#
Read userspace process and thread foundations and Wayland surfaces and events. This chapter examines GLib 2.82.2 and GTK 4.16.3 as separate historical source baselines. The practice program uses GLib only and opens no graphical session.
Learning goals#
Explain how a main context chooses and dispatches sources, distinguish global and thread-default contexts, and track GObject references through disposal. Follow an asynchronous task result back to its captured context without assuming cancellation removes a queued callback. Relate GTK 4 widget state to layout and rendering.
Source walkthrough#
- WorkerMain context
Complete task with result
- Main contextUI owner
Dispatch callback in captured context
- UI ownerUI owner
Resolve weak reference before using widget
- UI ownerMain context
Release result and callback state
Time runs downward. Dashed arrows show returns.
Read the text version
- Worker → Main context: Complete task with result
- Main context → UI owner: Dispatch callback in captured context
- UI owner → UI owner: Resolve weak reference before using widget
- UI owner → Main context: Release result and callback state
A GMainContext is a collection of event sources and the machinery for checking and dispatching them. A GMainLoop repeatedly iterates a context until asked to quit; the loop and context are distinct objects. A source can represent a timer, idle callback, file descriptor readiness or another event. Each source has callback data, a priority, and lifecycle rules. Attaching it to a context makes it eligible for that context's iteration. Destroying a source prevents future dispatch as specified by its API, but code already executing in a callback still needs its own valid data and synchronization.
The global default context is returned by g_main_context_default; passing NULL to several APIs selects it. A thread-default context is a separate per-thread association set by pushing a context. It lets asynchronous operations capture a context for later callbacks; it does not automatically start an event loop, and it is not necessarily the global default. Push and pop must be balanced in the appropriate thread. A context can be acquired by one thread at a time for iteration, and ownership is recursive for the owning thread. Another thread can attach sources and wake the context through supported APIs, but that does not make the application's data structures safe for concurrent mutation.
An iteration conceptually prepares sources, determines a timeout and poll descriptors, waits, checks which sources are ready, and dispatches eligible callbacks. Source priorities influence which ready sources are dispatched; lower numeric priority values are higher priority. A constantly ready high-priority source can delay lower-priority work, so an idle callback is not a guaranteed deadline. A callback may return a value indicating whether its source remains installed, according to that source's API. Source destroy notification provides a place to release callback data, but the application must arrange exactly one ownership path and avoid using that data after destruction.
- Prepare
g_main_context_prepare collects sources and a timeout.
- Poll
The internal poll waits on registered descriptors and may block.
- Check
g_main_context_check finds which sources are ready.
- Dispatch
g_main_context_dispatch runs ready callbacks in priority order.
From GLib 2.82.2 glib/gmain.c, g_main_context_dispatch, LGPL-2.1-or-later as stated in the file; function signature and following implementation omitted:
LOCK_CONTEXT (context);
g_main_context_dispatch_unlocked (context);
UNLOCK_CONTEXT (context);
The first statement acquires the context's internal lock, the second enters its dispatch machinery, and the third releases the lock. The surrounding code selects the global default when the argument is NULL. This excerpt does not mean user callbacks execute forever while an application can rely on the context lock to protect unrelated state: the dispatch implementation manages its own locking around callbacks. It is a library-internal lock, not a substitute for application ownership rules. The full main-loop source describes source handling and context iteration.
Reentrancy and nested loops
A callback can cause more work to be scheduled. It may also enter a nested loop, allowing other callbacks to run before the original callback returns. This is reentrancy: an object's state may change while an outer operation is suspended on the same thread. For example, a callback begins updating a document, opens a nested loop, and a second callback closes the document. When the first callback resumes, its previously borrowed document pointer may no longer be valid. Avoid depending on a non-reentrant sequence across a nested loop; retain a suitable reference, re-check state, or structure the operation as an asynchronous state machine.
A source's priority and readiness are different concepts. A timeout becoming ready means its target time has passed according to the relevant clock, not that its callback has already run. Time spent in another callback can delay it. A file descriptor becoming readable likewise means an operation may make progress; it does not guarantee an entire application message is available. A source's callback should handle partial reads, closure and error conditions according to its underlying API. Calling a long blocking operation in a UI context prevents that context from processing input, rendering and task completions until the operation returns.
g_main_context_invoke and related mechanisms can arrange work in a context, but callers must understand the API's possible immediate invocation when ownership conditions permit. Do not assume every invocation necessarily creates a delayed queue entry. Passing a pointer to stack data is unsafe when the callback might outlive the stack frame. Pass owned data with a destroy notifier, or a separately referenced object, and define who releases it on scheduling failure or cancellation. When shutting down, stop producing new work, remove sources where appropriate, drain or safely abandon pending callbacks, and keep the context alive until its users have stopped.
GObject references and signals
GObject supplies a runtime type system, reference counting, properties and signals. An instance has a type and class; a class contains virtual methods and property/signal definitions. A property can validate or transform values through its specification and setter. Not every property is writable after construction, and emitting a notification is not necessarily an atomic snapshot of several properties. A signal connects an emitter to callbacks through closures. Callback data and closures have their own lifetime rules, and disconnecting a handler prevents future eligible invocations but must not be used as proof that an already executing callback has finished.
A newly created GInitiallyUnowned object can carry a floating reference. A container or other owner commonly sinks it: sinking a floating reference converts that reference into an ordinary owned reference; sinking a non-floating object instead takes another reference. This is why blindly unrefing after handing a floating widget to an owner can destroy the widget too early, while blindly adding references can leak it. Consult the exact transfer annotations for each API. An ordinary reference keeps the object allocated but does not guarantee that the object is logically usable after disposal or that GTK operations are legal from the current thread.
From GLib 2.82.2 gobject/gobject.c, g_object_ref_sink, LGPL-2.1-or-later per the file header:
gpointer
(g_object_ref_sink) (gpointer _object)
{
GObject *object = _object;
gboolean was_floating;
g_return_val_if_fail (G_IS_OBJECT (object), object);
g_return_val_if_fail (g_atomic_int_get (&object->ref_count) >= 1, object);
g_object_ref (object);
was_floating = floating_flag_handler (object, -1);
if (was_floating)
g_object_unref (object);
return object;
}
The function takes a reference, clears a floating reference through the internal handler, and drops the extra reference only when the object was floating. A floating object therefore ends with one owned reference, while a non-floating object gains one. The excerpt does not show which widget APIs transfer a floating reference, so each call's annotation still decides ownership.
From GLib 2.82.2 gobject/gobject.c, g_object_run_dispose, LGPL-2.1-or-later as stated in the file; validation and surrounding code are omitted:
G_OBJECT_GET_CLASS (object)->dispose (object);
This dispatches the class's dispose method for the object. It does not itself free the object's allocation or mean that all references have disappeared. The source holds a temporary reference around this operation; callers must follow the documented API rather than invoke a class method on a potentially dead pointer. Disposal breaks references to other objects and may be called more than once; a correct implementation makes it idempotent and chains to its parent. Finalization occurs when the object is finally destroyed and releases remaining non-object resources. The exact timing depends on references and on explicit disposal; a cycle of strong references can prevent the last unref and needs deliberate breaking.
Signals introduce another ownership edge. A closure can retain callback data, and that data might retain the emitter or another object. A cycle can therefore persist even when the screen no longer shows a widget. Use an appropriate disconnect or weak-reference strategy and a closure destroy notifier where required. A weak reference does not itself keep an object alive; to use an object safely, obtain a strong reference through the appropriate weak-reference API and handle the possibility that the object has already gone away. A raw pointer checked for non-NULL is not a lifetime guarantee across threads.
GTask and the worker boundary
GTask represents an asynchronous operation and implements the async-result convention. Creating a task captures the relevant thread-default main context for completion. A worker function can perform blocking work away from the UI thread, then return a result or error through the task API. Completion is delivered according to the task's documented context and scheduling rules; the context must be iterated for callbacks to run. The task source object, task data, result and callback data each have transfer and destruction contracts. A destroy notifier for a returned pointer handles cleanup if the result is never propagated to the caller.
A GCancellable requests cancellation; it is not a thread join and does not guarantee that already queued callbacks vanish. The worker and completion callback must handle success, error and cancellation consistently, including the race where work completes just before cancellation is observed. A callback can run after a window was closed. Passing a raw widget pointer as callback data and assuming cancellation makes it safe is incorrect. Instead, hold the lifetime required for the operation, use a weak reference for the optional UI target, and resolve that reference in the completion callback on the correct context. Release the result and callback data even when the target is gone. See GLib 2.82.2 gio/gtask.c for the implementation and completion contracts.
From GLib 2.82.2 gio/gtask.c, g_task_return, LGPL-2.1-or-later per the file header (surrounding code omitted):
source = g_main_current_source ();
if (source && g_source_get_context (source) == task->context)
{
/* Second, we can only complete immediately if this is not the
* same iteration of the main loop that the task was created in.
*/
if (g_source_get_time (source) > task->creation_time)
{
/* Finally, if the task has been cancelled, we shouldn't
* return synchronously from inside the
* GCancellable::cancelled handler. It's easier to run
* another iteration of the main loop than tracking how the
* cancellation was handled.
*/
if (!g_cancellable_is_cancelled (task->cancellable))
{
g_task_return_now (task);
g_object_unref (task);
return;
}
}
The synchronous completion branch runs only when the current source belongs to the task's context, that source was dispatched after the task was created, and the task is not cancelled. Otherwise the code leaves the result for the idle source that g_task_attach_source queues. The excerpt does not show that queued path or the worker, so it cannot establish that a callback runs before a window is closed.
Consider this concrete timeline. At time 0 a window starts a task and records a weak reference to its view. At time 1 the worker produces a result and requests completion on the captured main context. At time 2 the user closes the window before the callback runs; the owner requests cancellation, disconnects its own signals and drops its view reference. At time 3 the queued completion callback runs. It propagates or frees the task result according to the API, attempts to obtain a strong reference from the weak reference, finds no live view and skips the UI update. Finally it clears the weak reference and callback data. The important fact is not whether cancellation wins the race: both outcomes have a valid cleanup route.
- Worker threadGTask
return the result from the worker
- GTaskCaptured main context
queue complete_in_idle_cb
- Captured main contextGTask
g_task_return_now runs the callback
- GTaskUI owner (weak ref)
callback resolves the weak reference
- UI owner (weak ref)GTask
Return. no live owner: skip the UI update
Time runs downward. Dashed arrows show returns.
Read the text version
- Worker thread → GTask: return the result from the worker
- GTask → Captured main context: queue complete_in_idle_cb
- Captured main context → GTask: g_task_return_now runs the callback
- GTask → UI owner (weak ref): callback resolves the weak reference
- UI owner (weak ref) → GTask (return): no live owner: skip the UI update
GTK 4 widget and rendering lifecycle
GTK 4 widgets are GObjects organized into a parent-child hierarchy. Parenting and reference transfer determine ownership; widgets can hold a floating reference at creation, and the relevant API says whether a container takes ownership. A widget's allocation and a widget being visible are different states. Unparenting removes a relationship and can allow destruction if no other reference remains. A callback retaining a widget strongly can unintentionally keep an otherwise closed UI alive, while a raw pointer can become stale. Destruction and task completion must be coordinated at the application layer.
GTK performs measurement and allocation to determine geometry. Changes to properties, style or content can invalidate layout or rendering and schedule another update. GTK 4 builds render nodes through its snapshot machinery and GSK renderer; it is not the GTK 3 immediate draw callback model. A renderer and display backend are selected based on build, platform and runtime configuration; Vulkan is not a universal default or guaranteed capability. GTK 4.16.3 gtk/gtkwidget.c is a starting point for the widget lifecycle and snapshot path.
GTK objects and widget operations generally belong to the application's designated UI/main thread. Reference counting alone does not grant permission to mutate widgets from a worker. A worker can create ordinary data, then pass an owned immutable result to the main context, where the callback checks the target and applies it. If the result contains objects with their own thread restrictions, those must be respected too. A context can be iterated by different threads at different times under its ownership rules, but a GTK application should not infer from that general GLib capability that moving its UI loop between threads is safe.
Shutdown and source cleanup
Quitting a main loop stops that loop's repeated iteration; it does not automatically destroy every source, cancel every worker or release every application reference. A worker that posts completion after the last iteration can leave a callback pending. An application needs an explicit shutdown order: stop new submissions, request cancellation, wait or otherwise arrange for workers to finish without blocking a thread they require for completion, remove sources and disconnect handlers, then release contexts and objects when no user can access them. The exact order varies with API and thread design.
A timeout callback can remove itself by returning the documented removal value. If another owner also tries to remove the source, it must avoid using an obsolete source ID or pointer. IDs can be reused, so retaining an old integer and removing it later can affect an unrelated source. Keep source ownership local, clear stored handles when removal occurs, and use context-aware APIs for sources attached outside the global default. A destroy notifier should release its owned callback data exactly once even if the callback is never called.
Practice#
This small program uses a private main context and a one-shot idle source. It creates no GTK widgets and connects to no session service. A C compiler and GLib development files are required; otherwise use the supplied task timeline as the source-only alternative.
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT
cat > "$tmpdir/main.c" <<'C'
#include <glib.h>
#include <stdio.h>
static gboolean once(gpointer data) {
int *count = data;
++*count;
printf("callback count=%d\n", *count);
return G_SOURCE_REMOVE;
}
int main(void) {
int count = 0;
GMainContext *context = g_main_context_new();
GSource *source = g_idle_source_new();
g_source_set_callback(source, once, &count, NULL);
g_source_attach(source, context);
g_source_unref(source);
g_main_context_iteration(context, TRUE);
g_main_context_unref(context);
return count == 1 ? 0 : 1;
}
C
if command -v cc >/dev/null && pkg-config --exists glib-2.0; then
cc "$tmpdir/main.c" -o "$tmpdir/main" $(pkg-config --cflags --libs glib-2.0) && "$tmpdir/main"
else
printf '%s\n' 'GLib development files or compiler unavailable'
fi
rm -rf "$tmpdir"
trap - EXIT
Expected output is callback count=1. The stack counter remains valid because the context is iterated before main returns; moving the callback to a worker without extending that lifetime would be a counterexample. This program checks local GLib behavior, not the pinned version, GTK rendering, or task cancellation. If dependencies are absent, use the time 0–3 task example above: identify the owner of the result, the weak view reference and the code that releases each when the view is gone. Deliver the event timeline and its cleanup table.
Knowledge check#
- How do the global default and thread-default contexts differ?
- Why does a nested loop threaten an outer callback's assumptions?
- What does sinking a floating reference do?
- Why can a task callback run after cancellation and widget destruction?
- Does quitting a main loop automatically join worker threads?
Answers#
- The global default is a shared fallback context; a thread-default context is explicitly associated with a thread and can be captured by asynchronous operations. Neither association starts a loop by itself.
- It can dispatch other callbacks before the outer callback resumes, allowing state or object lifetimes to change while the outer code is suspended.
- It converts a floating reference to an ordinary owned reference. If the reference is already non-floating, the operation takes another reference instead.
- Cancellation is a request and completion may already be queued. The callback must handle cancellation or result ownership and obtain a valid target through a weak-reference protocol.
- No. The application needs an explicit worker and source shutdown plan that avoids waiting on a context needed by those workers.
Checkpoint#
Submit the time 0–3 task timeline and a reference table for task data, result, weak target and callback data. Pass when both cancellation outcomes release their resources and no worker touches the widget. If not, reread the task and object source paths and retry the timeline. This is practice, not certification.
Sources#
Read GLib main-loop implementation, GObject lifecycle, GTask implementation, and GTK 4 widget implementation. The installed runtime and graphical backend may differ from these pinned source versions.