• 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.