Choosing Between Vala async/yield, GIO Callbacks, and GLib Threads for Non‑Blocking Work
A decision guide for Vala developers: pick the right async technique based on I/O vs CPU work, error handling needs, and composition complexity.
12 Jul 2025, 08:18 UTC

Problem: Keep the GLib main loop responsive while doing work
In a typical GNOME‑style Vala application the MainLoop (or MainContext) dispatches UI events, timeouts, and I/O notifications. If a function blocks this loop—for example by performing a long file read or a CPU‑intensive calculation—the whole application freezes until the work finishes. The goal is to move the waiting part off the loop while preserving a simple error‑propagation path and keeping UI updates safe.
Decision and constraints
You must decide which mechanism to use for non‑blocking work. The constraints are:
- The main loop must keep running; UI callbacks and timeouts should continue to fire.
- I/O‑bound waits (file, network, subprocess) differ from CPU‑bound work: the former can be expressed as cooperative yields, the latter needs a separate thread.
- Error handling and cancellation must be designed up front; Vala’s
throwsandGLib.Cancellableare the idiomatic ways. - The chosen approach should compose well when you need to chain several asynchronous steps.
Option A – Vala async methods with yield
Mark a method async and suspend it with yield on a GIO‑based asynchronous operation (e.g. File.load_contents_async). The Vala compiler generates a name.begin(...) starter and a matching name.end(res) that you call in the completion callback. Inside another async method you can simply write yield op(args, out result). Errors declared with throws are caught by an ordinary try/catch around the end call.
Option B – Plain GIO callbacks
Call the GIO async API directly and supply a lambda (or anonymous method) for the completion handler. Example:
file.load_contents_async.begin(cancellable, (src, res) => {
try {
file.load_contents_end(res, out data, out etag);
process(data);
} catch (Error e) {
show_error(e);
}
});
This works well for a single shot, but chaining several operations nests callbacks and scatters error handling.
Option C – GLib threads
For CPU‑bound work or when you must call a blocking library that does not cooperate with the main loop, spawn a GLib.Thread. After the thread finishes, marshal results back to the main thread with Idle.add (or GLib.MainContext.invoke) to update UI. You must protect shared data with locks or confine objects to a single thread, and be careful about GObject reference counts across threads.
Compact comparison
| Criterion | Async/yield | Plain callbacks | GLib threads |
|---|---|---|---|
| Best for | I/O‑bound sequences that need composition | One‑shot fire‑and‑forget operations | CPU‑bound work or blocking libraries |
| Blocks main loop? | No (cooperative yield) | No | Only if you misuse the thread (e.g. do heavy work in the callback) |
| Error propagation | try/catch around .end() |
Inside the handler | Must marshal Error back to main thread |
| Complexity | Medium (async spreads through call graph) | Low for single step, high for chains | High (locking, thread lifetime, GObject safety) |
| Dependencies | Vala compiler + GIO | Same as async | GLib thread API |
Trade‑off summary
The async/yield approach keeps all logic on the main thread, so UI updates are naturally thread‑safe and you avoid explicit locking. Cancellation flows through GLib.Cancellable without extra code. The downside is that the async modifier is viral: any caller that wants to use the result must also be async (or wrap the call in a begin/end pair). Plain callbacks avoid this virality but become unwieldy when you need to sequence multiple steps. Threads give you true parallelism for CPU‑heavy tasks, but you must manage shared state and ensure that any GObject touched from the worker thread is either immutable or properly referenced.
Concrete validated pattern
The following snippet shows a reusable async method that reads a file, processes its contents, and propagates errors or cancellation. It is intended to be launched from a UI handler (e.g. a button click) while a MainLoop is running.
async void load_file(File file, Cancellable c) throws Error {
uint8[] data;
string etag;
// Yield suspends this method until the async operation finishes.
yield file.load_contents_async(c, out data, out etag);
// At this point we are back on the main thread.
process(data);
}
// UI handler example
void on_button_clicked(Button source) {
File f = File.new_for_path("/home/user/example.txt");
Cancellable c = new Cancellable();
load_file.begin(f, c, (obj, res) => {
try {
load_file.end(res);
show_success();
} catch (Error e) {
show_error(e.message);
}
});
}
To validate the pattern:
- Save the code to
demo.vala. - Compile with
valac --pkg gio-2.0 demo.vala(requires Vala ≥ 0.48 and GLib 2.68 or newer). - Run the resulting binary; it will start a
MainLoopimplicitly viaGtk.Applicationor manually withnew MainLoop(). - Test three paths:
- Success: point
fat an existing readable file. - Error: point at a non‑existent path; the
Errorshould be caught in the handler and displayed viashow_error. - Cancellation: fire the
Cancellable(e.g. via a timeout) while a large file is being read; the operation should fail withIOError.CANCELLED.
- Success: point
- During a large read, add a
Timeout.add(100, () => { stdout.printf("tick\n"); return true; })to verify the main loop keeps iterating.
Limitations and practical checks
Even though the pattern is safe for I/O‑bound work, keep these caveats in mind:
- The continuation only runs while a
GLib.MainContextis iterating. If you callload_file.beginbefore starting aMainLoop, the callback will never be invoked. - Never ignore the
.end()call; doing so swallows anyErrorthat the async operation may have produced. - The generated C code uses a state‑machine that may differ between valac releases. Rely only on the Vala‑level contract (the
begin/endpair and thethrowsdeclaration), not on the internal implementation. asyncmethods cannot be used for constructors, static methods, or certain property accessors. If you need asynchronous initialization, consider a factory method that returns aFuture-like object or use plain callbacks.- If you accidentally place a CPU‑intensive loop inside an
asyncmethod (e.g. parsing a huge JSON string without yielding), you will block the main loop despite theasynckeyword. Offload such work to a thread.
To check that the loop is not blocked during a long operation, run the program with the ticker described above and observe that tick lines continue to appear while the file read is in progress. If the ticker stalls, you have introduced a blocking call.
Rollback
This guide does not modify system state; it only shows how to structure code. Therefore no rollback steps are required.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.