I just integrated GLib event loop into Boost.Asio, so I'll share some notes before it vanishes from my memory.
There are several approaches to integrate GLib and Boost.Asio. First and easiest option is to spawn a new thread to run g_main_loop_run()
. The new thread will block on this call until you call g_main_loop_quit()
. However the documentation only warranties thread-safety to functions receiving GMainContext
so you better call this function from the GLib thread itself. You can submit jobs to be executed in the GLib thread by calling g_main_context_invoke()
.
Second approach is to control each iteration tick done by g_main_loop_run()
. This can be done by calling g_main_context_iteration()
. By controlling the polling, you have the option to call this function from any thread you want from time to time. This is a good option if you're for instance coding a SDL game where polling really is the appropriate answer.
An open question arises if you rely on polling: how frequently should I poll? If you poll too often you waste CPU, but if you poll to sparsely then you introduce delay/lag. That's where the third integration option comes in: break the g_main_context_iteration()
call into a sequence of calls to g_main_context_prepare()
, g_main_context_query()
, g_main_context_check()
and g_main_context_dispatch()
. But do create GMainContext
with g_main_context_new_with_flags(G_MAIN_CONTEXT_FLAGS_OWNERLESS_POLLING)
or else you'll run in some races explained at https://gitlab.gnome.org/GNOME/glib/-/commit/e26a8a59813ce651c881fe223e7d1a5034f2f816.
The code for the integration I just wrote is online and you can find at https://gitlab.com/emilua/glib/-/blob/bc2b4236aa7f296a08739f23522809817229792f/src/service.cpp. This code also takes Boost.Asio strands into consideration, so you should be able to call io_context.run()
from multiple threads and this code will still work. If you don't need this constraint, the code can be simplified a lot.
There are just a few tricks to keep in mind when you write this kind of code:
- Pay attention to stack overflows. Is it possible for a handler to trigger detection of another ready source that will in turn call the second handler in a nesting fashion? Avoid that.
- Dispatch semantics are not always safe. Code is rarely written under the assumption that arbitrary handlers can be executed when it calls a function to schedule some IO. Just do post semantics by default and at most offer dispatch semantics as an option.
- Be careful of starvation and unfairness. Always yield the “event loop tick” to other services now and then.
And GLib-specific rules:
GMainContext
is thread-safe.
- Code using GLib might not take an explicit reference/parameter to a
GMainContext
but instead directly reference the global one or the thread default one. If this code uses the thread default context, then the solution is simple (just set the thread default before dispatching the handlers). If this code uses the global one, then your application just acquired a new restriction of not creating multiple GMainContext
objects and always refer to the same one as well.
- GLib has no concept of pending work. It expects its loop to be running forever. The code I linked earlier has a solution for that.
- Do not reuse “fd watchers” from previous tick iterations in an attempt to save syscalls. The interest set is not stateful and GLib doesn't need to inform you it has closed a fd that was used in the previous iteration. If that event happens, another thread from the program could open a fd that has the same number and your fd watcher just became a time bomb. The amount of danger will depend on the “fd watcher” implementation (I used epoll for instance) and interactions specific to your program.