-
Pl
chevron_right
Christian Hergert: Asynchronous State Machines with Fibers
news.movim.eu / PlanetGnome • 10:48 • 2 minutes
Writing state machines gets a bit of a bad reputation because they are often implemented in complex manners which are specific to the problem domain. I think that makes people shy away from writing them when they are truly beneficial, including myself.
Where they often go awry is when you have some sort of work that needs to be done asynchronously. This is exceedingly common in UI programming like GTK applications but just as easily found in daemons.
Because of this, I see people explicitly avoiding the state machine, or worse, implicitly avoiding its correctness by open-coding a solution across a dozen callbacks.
With
DexLimiter
and
DexFiber
I find I can write these state machines better.
You can use the limiter with a max-concurrency of 1 to get an “asynchronous Mutex” of sorts. No lock management necessary.
static void
password_daemon_init (PasswordDaemon *daemon)
{
daemon->limiter = dex_limiter_new (1);
}
Imagine, if you will, that a limiter is a mutex plus a callback/closure which fires as soon as a slot is free. That means we need a little state to send to our transition callback.
/* Define our closure state for a transition */
DEX_DEFINE_CLOSURE_TYPE (StateTransition, state_transition,
DEX_DEFINE_CLOSURE_OBJECT (PasswordDaemon, daemon),
DEX_DEFINE_CLOSURE_VALUE (PasswordDaemonMode, target))
That is a nice wrapper around defining a
struct
with a
new
and
free
function.
Now we can request a transition of the state machine. Since our
DexLimiter
is an asynchronous mutex (with a single runnable slot), the fiber will not be spawned until it is the highest priority.
DexFuture *
password_daemon_transition (PasswordDaemon *daemon,
PasswordDaemonMode mode)
{
StateTransition *transition;
transition = state_transition_new ();
transition->daemon = g_object_ref (daemon);
transition->target = model;
return dex_limiter_run (daemon->limiter, NULL, 0,
password_daemon_transition_fiber,
transition,
state_transition_free);
}
That makes our transition code very clean when you combine the fiber with
g_autoptr()
and
dex_await()
to await the completion of futures. So a state machine might look like the following:
static DexFuture *
password_daemon_transition_fiber (gpointer user_data)
{
TransitionState *state = user_data;
GError *error = NULL;
switch (state->target)
{
case PASSWORD_DAEMON_MODE_HANDOFF:
if (state->daemon->mode != PASSWORD_DAEMON_MODE_INITIAL)
return invalid_transition (state->daemon->mode,
state->target);
if (!password_daemon_enter_handoff (self, &error))
return dex_future_new_for_error (&error);
break;
case PASSWORD_DAEMON_MODE_LOCKED:
...
case PASSWORD_DAEMON_MODE_UNLOCKED:
...
}
return dex_future_new_enum (state->daemon->mode);
}
static gboolean
password_daemon_enter_handoff (PasswordDaemon *daemon,
GError **error)
{
GSocket *control;
if (!(control = dex_await_object (create_socket (), error)))
return FALSE;
...
}
What I find nice about this is
enter
/
leave
transition components can be customized for the state machine transition. That leaves room for transitions between states which require specialization for correctness.
This is much cleaner than ad-hoc callbacks chained together because you can await in the transition fiber for asynchronous work to complete and the state machine itself is preserved. No shoving temporary state in your class instance. No testing hell to see if you caught all the failure cases. No pain with sequencing or order of main loop processing.
Hopefully that shows you can use libdex to write more correct and cleaner state machines by keeping the majority of the implementation in one place.