How to integrate Boost.Asio main loop in GUI framework like Qt4 or GTK
Asked Answered
O

5

29

Is there any way to integrate Boost.Asio with Qt4 (preferred) or GTK main loop? GTK provides poll(2) like API so technically is should be possible. Qt provides its own networking layer, however I prefer to use existing code written for Boost.Asio. I want to integrate them without using an additional thread.

Is there any reference how to do this for Qt4 (preferred) or GTKmm?

Thanks.

Edit

I want to clearify several things to make the answer easier. Both Qt and GTKmm provide "select like" functionality:

So, the question is, how to integrate existing "selectors/pollers" as reactor to Boost.Asio io_service. Today, Boost.Asio can use select, kqueue, epoll, /dev/poll and iocp as reactor/proactor service. I want to integrate it to the main-loop of GUI framework.

Any suggestions and solutions (better) are welcome.

Overcareful answered 16/6, 2009 at 11:57 Comment(1)
Any update on a good solution here? I've just hit the same issue...Edp
L
10

It's rather old question but for those who are reading it now I would like to share my code which is an implementation of QAbstractEventDispatcher for boost::asio.

All you need is to add the following line before creating QApplication (usually it's in main()).

QApplication::setEventDispatcher(new QAsioEventDispatcher(my_io_service));

It will cause, that io_service is being run together with qt application in one thread without additional latency and performance drop (like in solution with calling io_service::poll() "from time to time").

Unfortunately, my solution is for posix systems only, since it use asio::posix::stream_descriptor . Windows support may need completely different approach or quite similar - I don't really know.

Livialivid answered 14/12, 2014 at 22:26 Comment(7)
Nice... Thanks for sharing. And I don't think you can't do it for Windows because windows has very-very different approach. Also asio does not really support reactor style operations on Windows - it falls back to "select" on different thread because IOCP does not support "select" like operations.Overcareful
Unfortunately, qt developers do not recommend such a solution: qtcentre.org/threads/… .Livialivid
@Livialivid Are you saying the Qt developers don't recommend a solution like the one you've implemented here? I'm not quite seeing that from the thread you linked to...Wasp
Also, it appears that a QTimer with an interval of 0 will simply cause the appropriate signal to be triggered once every time the event loop is processed; what is the advantage of re-implementing QAbstractEventDispatcher rather than simply connecting a call to io_service::poll to a 0-interval QTimer?Wasp
@KyleStrand A QTimer with interval of 0 will simply cause 100% CPU usage (of one core).Livialivid
Huh. That must mean that it actually affects the frequency with which events are processed, then....? That doesn't seem to be indicated by the documentation, but I guess it makes sense.Wasp
@Livialivid What if you called QThread::yieldCurrentThread() after each io_service::poll()?Wasp
B
17

Simple: Build a QT slot that calls the io_service::poll_one() belonging to the gui. Connect that slot to QT's tick signal.

In Depth: Luckily for you Boost.Asio is very well designed. There are many options on how to provide a thread of execution to the underlying asynchronous internals. People have already mention using io_service::run(), a blocking call with many disadvantages.

You are only allowed to access gui widgets from a single thread. External threads generally need to post events to the gui if they want to do mutate any widget. This is very similar to how Asio works.

The naive approach is to just dedicate one thread (or timer) to running io_service::run() and have the Asio completion handler post a gui signal. This will work.

Instead you can use the guarantee that completion handlers will only be called in the thread of execution of the io_service caller. Don't have the gui thread call io_service::run() as it is blocking and could hang the gui. Instead use io_service::poll() or io_service::poll_one(). This will cause any pending Asio completion handlers to be called from the gui thread. Since the handlers are running in the gui thread they are free to modify the widgets.

Now you need make sure the io_service gets a chance to run regularly. I recommend having a repeating gui signal call poll_one() a few times. I believe QT has a tick signal that would do the trick. You could of course roll your own QT signal for more control.

Backchat answered 21/12, 2009 at 18:58 Comment(4)
What "tick" signal? It sounds like this would have ASIO do busy waits which will lead to high CPU usage.Analysand
It's been awhile but it looks like the tick signal is unique to PyQT4. For a C++ solution, a QTimer would work, or any other signal that regularly fires.Backchat
@cheez, the reason to call poll_one instead of poll is to avoid avoid long pauses that could occur if lots of network events flood in. I suggest the simple solution of calling poll_one a few times to avoid the problem where network events are more frequent than poll_one is called. An obvious optimization is to break out of the loop early if poll_one returns 0 (avoid the busy loop). Another more complex solution would be to loop on poll_one until 0 is return or a time threshold is passed (say 100ms).Backchat
@nonchalant Specifically, it appears that a QTimer with an interval of 0 will fire every time the Qt Event Loop is processed.Wasp
L
10

It's rather old question but for those who are reading it now I would like to share my code which is an implementation of QAbstractEventDispatcher for boost::asio.

All you need is to add the following line before creating QApplication (usually it's in main()).

QApplication::setEventDispatcher(new QAsioEventDispatcher(my_io_service));

It will cause, that io_service is being run together with qt application in one thread without additional latency and performance drop (like in solution with calling io_service::poll() "from time to time").

Unfortunately, my solution is for posix systems only, since it use asio::posix::stream_descriptor . Windows support may need completely different approach or quite similar - I don't really know.

Livialivid answered 14/12, 2014 at 22:26 Comment(7)
Nice... Thanks for sharing. And I don't think you can't do it for Windows because windows has very-very different approach. Also asio does not really support reactor style operations on Windows - it falls back to "select" on different thread because IOCP does not support "select" like operations.Overcareful
Unfortunately, qt developers do not recommend such a solution: qtcentre.org/threads/… .Livialivid
@Livialivid Are you saying the Qt developers don't recommend a solution like the one you've implemented here? I'm not quite seeing that from the thread you linked to...Wasp
Also, it appears that a QTimer with an interval of 0 will simply cause the appropriate signal to be triggered once every time the event loop is processed; what is the advantage of re-implementing QAbstractEventDispatcher rather than simply connecting a call to io_service::poll to a 0-interval QTimer?Wasp
@KyleStrand A QTimer with interval of 0 will simply cause 100% CPU usage (of one core).Livialivid
Huh. That must mean that it actually affects the frequency with which events are processed, then....? That doesn't seem to be indicated by the documentation, but I guess it makes sense.Wasp
@Livialivid What if you called QThread::yieldCurrentThread() after each io_service::poll()?Wasp
H
6

If I understand your question correctly, you have code written for Boost.Asio . You would like to use that code inside a GUI application.

What is not clear in your question is if you want to wrap the Qt/Gtk network layers through asynio for your code to work, if you are just looking for a solution for having both a gui event loop and asynio together.

I will assume the second case.

Both Qt and Gtk have methods to integrate foreign events in their event loop. See for example qtgtk where the Qt event loop is plugged into Gtk.

In the specific case of Qt, if you want to generate events for Qt, you can use the following class: QAbstractEventDispatcher.

After a quick look at boost asio, I think you need to do the following:

  • have a recurring QTimer with duration zero that calls io_service::run() all the time. That way, boost::asio will call your completion handler as soon as your asynchronous operation is completed.
  • in your completion handler, two options:
    • if your completion operation is a long one, separated from the GUI, do your business and make sure to call qApp.processEvents() regularly to keep the GUI responsive
    • if you just want to communicate back with the gui:
      1. define a custom QEvent type
      2. subscribe to this event
      3. post your event to Qt event loop using QCoreApplication::postEvent().
Hovis answered 19/6, 2009 at 11:0 Comment(4)
The solution you suggest requires some kind of busy or short timeouts wait, that is suboptimal. I rather want merge both even loop and not run-qt/run-asio/run-qt/run-asio, especially when I wait for one of: user input/input from the network that can occure simultaniously.Overcareful
There Ain't No Free Lunch. If I understood correctly, asio has an essential part which is calling the ::run() function, which is blocking. If that function is blocking, either you call it from a background thread, or it might block your application. You mention event loop integration but from quickly browsing, I haven't found any event loop in asio. Asynchronous calling does not necessarily go with an event loop.Hovis
You can also do it the other way round: use Qt for everything, and just "convert" qt network events into a format expected by asio. In this way, you would not use asio, but would have to call the legacy code in the same asio used to do.Hovis
Look also at: qtcentre.org/threads/…Livialivid
J
2

Genuinely integrating the main loops is possible. It's just a big pain (and I have yet to actually try it).

Running io_service::run() on a separate thread is probably the way to go.

Juryrig answered 23/12, 2009 at 4:18 Comment(0)
V
1

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.
Voiced answered 18/2, 2021 at 22:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.