From previous entries, you may have noticed that I find “cross domain” issues interesting – like ones that span programming language runtimes and operating systems simultaneously. An excellent example of this is the generic concept of “cancelling” computation.
What do I mean by cancelling computation? Let’s first define “computation”. Most people using computers are interacting with an operating system, which runs a set of processes. Here, a process is an example of computation. But in more complex software, it often happens that a single process inside has multiple related independent “tasks” (these tasks could technically be structured as threads, or coroutines inside a single thread). It’s also in some domains quite common for an application to be composed of multiple processes, where child processes communicate back with the parent. The recent Linux addition of cgroups finally give a way for the operating system kernel to logically manage a group of multiple processes as one unit.
Now that we’ve looked at different kinds of “computation” – we can say “cancelling” that computation means that the computation stops before it would have ordinarily completed, and furthermore that only that computation stops. The latter part of this definition is important – I could of course cancel my browser process(s) by pulling the battery from my phone or halting the operating system kernel, but that’s not a really useful implementation of cancellation.
To make all of this less abstract, let’s talk about a simple case – you are logged into a Unix shell, and type “sleep 30″. While that’s running, your shell will be blocked. You can press Control-C, and this results in just that “sleep” process being “cancelled” by the definition above. Under the covers, Control-C results in a SIGINT being generated for that process, and because it didn’t install a handler for SIGINT, the default action defers to having the kernel exit the process.
If you’re using systemd for your init program, the operation "systemctl stop foo" will (by default) use the Linux kernel's cgroup mechanism to stop all processes that were ever invoked from the original foo process.
The above two cases are pretty simple and reliable; they're managed by the operating system kernel. But what about the case where a single process is composed of a set of logical "tasks"? This is where things get interesting, and where this blog post starts to relate to GNOME: we're going to talk about the GCancellable.
If you've used modern GNOME APIs very much, you've almost certainly seen the GCancellable parameter on nearly every method that involves I/O, or more generally, an operation that one might want to cancel. Let's look at a specific example, g_input_stream_read_async. Let's further suppose that that GInputStream is the read half of a GSocketConnection - You are writing an application which reads data from the network, such as a Bittorrent client, a web browser, or a ssh client. You want to have a "[X] Stop" button, which just stops the data copying; it should not make your entire application's UI disappear. Sounds obvious when stated this way, but the operating system kernel doesn't help us here - userspace code needs to be involved.
On Linux kernel based systems, a GCancellable is really just a wrapper for an eventfd(), which is just an optimized version of a standard Unix pipe() for cases where you just want notification of events on a 64 bit counter, not a general byte stream.
Under the hood, when your application calls g_input_stream_read_async(stream, ..., cancellable, ...), GLib will ensure your application drops into a poll() both the socket fd and the eventfd, awaiting input. Calling g_cancellable_cancel will cleanly break your application out of the poll(), and manifest as a G_IO_ERROR_CANCELLED error.
I originally wanted to have a comparison with Java, but it would make this blog entry far too long. Suffice to say it's been an evolutionary learning experience for them, moving away from threads for everything.
The GCancellable model has been very successful for GLib, and its consuming applications such as GNOME (both by language bindings and pure C/C++ applications) - the application programmer can write reliable cancellable asynchronous operations, and under the hood GLib makes efficient use of available operating system primitives.