With GNOME 48 I released libdex 0.10 on the march towards a 1.0. One of the major improved features there was around fiber cancellation.
I’m not going to go into detail about the differences between threads and fibers as wikipedia or your local CS department can probably help you there. But what I will say is that combining __attribute__((cleanup))
(e.g. g_autoptr()
) with futures and fibers makes such a nicer experience when writing C.
Thread cancellation is a rather non-portable part of the threading stack across platforms. Some POSIX platforms support it, some don’t. Having safe places to cancel can be a real challenge even if you are depending on a threading implementation that can do it.
With fibers, we have a natural cancellation point due to the cooperative nature of scheduling. All (well behaved) fibers are either making progress or awaiting completion of a future. We use the natural await()
points to implement cancellation. If everything that was awaiting the future of the fiber has been cancelled, then the fiber can naturally cancel too. The next time it awaits that will just happen and natural exit paths will occur.
When you don’t want cancellation to propagate, you still use dex_future_disown()
like always (as the fiber itself is a future).
Just to give a quick example of how fibers and futures makes writing C code nicer, here is an excerpt from libfoundry that asynchronously implements the necessary phases to build/run your project with a specific tool, possibly on a non-local system. In the GNOME Builder IDE, this is a series of async callbacks that is extremely difficult to read/debug. But with Foundry using libdex, it’s just a few lines of code and every bit as non-blocking.
From foundry-run-manager.c.
g_autoptr(FoundryDeployStrategy) deploy_strategy = NULL; g_autoptr(FoundryBuildProgress) progress = NULL; g_autoptr(GSubprocess) subprocess = NULL; GError *error = NULL; if (!(deploy_strategy = dex_await_object (foundry_deploy_strategy_new (state->pipeline), &error)) || !dex_await (foundry_deploy_strategy_deploy (deploy_strategy, state->build_pty_fd, state->cancellable), &error) || !dex_await (foundry_deploy_strategy_prepare (deploy_strategy, state->launcher, state->pipeline, state->build_pty_fd, state->cancellable), &error) || !dex_await (foundry_run_tool_prepare (state->run_tool, state->pipeline, state->command, state->launcher, state->run_pty_fd), &error) || !(subprocess = foundry_process_launcher_spawn (state->launcher, &error))) return dex_future_new_for_error (error);
At each dex_await*()
function call the fiber is suspended and we return to the main loop for additional processing.
In a better world we’d be able to do these without fibers and instead do stackless coroutines. But maybe with a little compiler help we can have that too.