-
Pl
chevron_right
Christian Hergert: Stackless Coroutines in Libdex
news.movim.eu / PlanetGnome • 13 hours ago • 3 minutes
Fibers are always a nice way to keep your async C code clean while using Libdex. However, occasionally you may want a lighter option which doesn’t require a stack or saving registers for work doing little more than coordinating futures.
I’ve added Stackless Coroutines for this which still allows writing future-coordinating code. Though this will suspend/resume your coroutine by re-entering the function and jumping to the next position. Your threads stack is reused. State is saved in your closure state.
This isn’t a new concept. It is really old just like fibers. What is useful is that this style of continuation passing may still be represented as a DexFuture and therefore composed like the others.
You can place these stackless coroutines in DexTaskGroup alongside fibers, threadpool work, and others. Cancellation will propagate to a clean exit point of the coroutine just like it would with a fiber.
Overhead is a bit lower than fibers in synthetic benchmarks depending on use. I was actually impressed our fiber implementation performed as well as it did head-to-head.
To make building your coroutine continuation easier, libdex provides a handy macro to create your
typedef struct
,
_new()
, and
_free()
helpers in a single macro expansion using
DEX_DEFINE_CLOSURE_TYPE()
.
You use it like this:
DEX_DEFINE_CLOSURE_TYPE (MyTaskState, my_task_state, DEX_DEFINE_CLOSURE_VALUE (gsize, bytes), DEX_DEFINE_CLOSURE_POINTER (GBytes *, bytes_obj, g_bytes_unref), DEX_DEFINE_CLOSURE_OBJECT (GSocketConnection, conn))
Coroutines cannot use the exact syntax that fibers do for awaiting, which is a bummer, but a side-effect of trying to make something that works across Linux, Windows, FreeBSD, Solaris, macOS, etc. Particularly because the implementation must use
switch
/
case
to stay portable without address-of-label support on
MSVC
nor
clang-cl.exe
.
So awaiting is a bit more clear you’re suspending/resuming the stackless coroutine.
DEX_DEFINE_CLOSURE_TYPE (LoadState, load_state,
DEX_DEFINE_CLOSURE_OBJECT (GFile, file),
DEX_DEFINE_CLOSURE_OBJECT (GFileInputStream, input),
DEX_DEFINE_CLOSURE_OBJECT (GFileInfo, info),
DEX_DEFINE_CLOSURE_VALUE (int, io_priority))
static DexFuture *
do_something (DexCoroutineContext *context,
gpointer user_data)
{
LoadState *state = user_data;
g_autoptr(GError) error = NULL;
DEX_COROUTINE_BEGIN (context);
DEX_COROUTINE_SUSPEND_OBJECT (
&state->input, &error,
dex_file_read (state->file, state->io_priority));
if (error != NULL)
return dex_future_new_for_error (g_steal_pointer (&error));
DEX_COROUTINE_SUSPEND_OBJECT (
&state->info, &error,
dex_file_input_stream_query_info (
state->input,
G_FILE_ATTRIBUTE_STANDARD_SIZE,
state->io_priority));
if (error != NULL)
return dex_future_new_for_error (g_steal_pointer (&error));
/* maybe do something useful here */
return dex_future_new_int64 (g_file_info_get_size (state->info));
DEX_COROUTINE_END;
}
You do need to be careful about placing things on the stack, because they wont be there on the other side of that
DEX_COROUTINE_SUSPEND_*
macro expansion. That is because when the scheduler jumps back into your stackless coroutine, it will use a
switch
/
case
to jump to the next bit of code. Don’t fear though, just add your state to your continuation which we’ve established is easy to do now.
If you don’t like these macros, you can do things the manual way using
dex_coroutine_context_suspend()
and
dex_coroutine_context_resume()
who’s APIs are not terrible either. They do require you make up your own program-counter regime though which for the macro case is basically just
__COUNTER__
.
You can spawn your coroutine using
dex_scheduler_spawn_coroutine()
or as part of a work-queue in
DexLimiter
with
dex_limiter_run_coroutine()
.
dex_scheduler_spawn_coroutine ( dex_thread_pool_scheduler_get_default (), my_coroutine, my_coroutine_new (), (GDestroyNotify) my_coroutine_free);
I hope you've enjoyed this attempt to make another 1970s technology useful in a modern world.