如何将 Boost.Asio 主循环集成到像 Qt4 或 GTK 这样的 GUI 框架中?

29
有没有办法将Boost.Asio与Qt4(首选)或GTK主循环集成? GTK提供类似于poll(2)的API,因此技术上应该是可能的。Qt提供了自己的网络层,但我更喜欢使用已经编写为Boost.Asio的现有代码。 我想将它们集成在一起,而不使用附加线程
是否有参考资料可以用于Qt4(首选)或GTKmm?
谢谢。 编辑 我想澄清几件事情,以使答案更容易理解。 Qt和GTKmm都提供“select-like”功能: 因此,问题是如何将现有“选择器/轮询器”作为反应堆集成到Boost.Asio io_service中。 今天,Boost.Asio可以使用select、kqueue、epoll、/ dev / poll和iocp作为反应堆/ proactor服务。 我希望将其集成到GUI框架的主循环中。
欢迎提出任何建议和解决方案(最好)。

这个问题有好的解决方案了吗?我也遇到了同样的问题... - Macke
5个回答

17

简单: 构建一个QT slot,调用归属于gui的io_service::poll_one()。将该slot连接到QT的tick信号。

详细: 幸运的是,Boost.Asio设计得非常好。提供了许多选项来为底层异步内部提供执行线程。人们已经提到使用io_service::run(),这是一个带有许多缺点的阻塞调用。

你只允许从单个线程访问gui小部件。外部线程通常需要向gui发送事件,如果它们想要改变任何小部件。这与Asio的工作方式非常相似。

幼稚的方法就是只分配一个线程(或计时器)去运行io_service::run(),然后让Asio完成处理程序发布一个gui信号。这样是可以工作的。

相反,您可以利用完成处理程序仅在io_service调用者的执行线程中被调用的保证。不要让gui线程调用io_service::run(),因为它会阻塞并可能挂起gui。而是使用io_service::poll()io_service::poll_one()。这将导致任何待处理的Asio完成处理程序从gui线程中调用。由于处理程序在gui线程中运行,它们可以自由地修改小部件。

现在你需要确保io_service有机会定期运行。我建议用一个重复的gui信号调用poll_one()几次。我相信QT有一个tick信号可以做到这一点。当然,您也可以为了更好的控制而自己编写QT信号。


3
“tick”信号是什么?听起来这会让ASIO进行繁忙等待,从而导致高CPU使用率。 - Sohail
1
已经有一段时间了,但看起来tick信号在PyQT4中是独特的。对于C++解决方案,可以使用QTimer或定期触发的任何其他信号。 - deft_code
@cheez,调用poll_one而不是poll的原因是为了避免大量网络事件涌入时可能出现的长时间暂停。我建议简单的解决方案是多次调用poll_one以避免网络事件比poll_one调用频繁的问题。一个明显的优化是如果poll_one返回0,则尽早退出循环(避免忙等待)。另一个更复杂的解决方案是在poll_one上循环,直到返回0或超过时间阈值(比如100毫秒)。 - deft_code
@nonchalant 特别是,似乎一个间隔为0的QTimer会在每次Qt事件循环被处理时触发。 - Kyle Strand

10

这是一个比较老的问题,但对于现在正在阅读它的人,我想分享 我的代码,它是一个基于 boost::asio 实现的 QAbstractEventDispatcher。

你只需要在创建 QApplication 之前添加以下行(通常在 main() 中)即可。

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

我的解决方案可以让 io_service 在一个线程中与 Qt 应用程序一起运行,而不会出现额外的延迟和性能下降(例如调用 io_service::poll() “不时地” 解决方案中出现的情况)。

不幸的是,我的解决方案仅适用于 posix 系统,因为它使用了 asio::posix::stream_descriptor。Windows 支持可能需要完全不同的方法或非常相似的方法-我并不太清楚。


2
不幸的是,qt开发人员不建议使用这种解决方案:http://www.qtcentre.org/threads/53229-How-to-integrate-asio-io_service-with-Qt-event-loop。 - peper0
@peper0,你是在说Qt开发人员不推荐像你在这里实现的解决方案吗?我从你链接的帖子中没有完全看出来... - Kyle Strand
此外,似乎使用间隔为 0QTimer 只会在每次事件循环被处理时触发相应的信号;那么重新实现 QAbstractEventDispatcher 的优点是什么,而不是将对 io_service::poll 的调用连接到一个 0-间隔的 QTimer 上? - Kyle Strand
哦,那肯定意味着它实际上会影响事件处理的频率了……?文档似乎没有表明这一点,但我想这是有道理的。 - Kyle Strand
如果在每个io_service::poll()之后调用QThread::yieldCurrentThread()会怎样呢? @peper0 - Kyle Strand
显示剩余2条评论

6
如果我理解您的问题正确,您编写了Boost.Asio代码。您想在GUI应用程序中使用该代码。
您的问题中不清楚的是,您是否希望通过asynio包装Qt/Gtk网络层使您的代码工作,还是只是寻找同时拥有GUI事件循环和asynio的解决方案。
我将假设是第二种情况。
Qt和Gtk都有方法来集成外部事件到它们的事件循环中。例如,可以参考qtgtk,其中Qt事件循环被插入到Gtk中。
在特定的Qt情况下,如果您想为Qt生成事件,可以使用以下类:QAbstractEventDispatcher
经过快速查看boost asio,我认为您需要执行以下操作:
  • 有一个重复的QTimer,持续时间为零,调用io_service::run(),这样,boost::asio将在异步操作完成后立即调用完成处理程序。
  • 在您的完成处理程序中,有两个选项:
    • 如果您的完成操作是长期的,并与GUI分离,请处理自己,并确保定期调用qApp.processEvents()以保持GUI响应
    • 如果您只想与GUI通信:
      1. 定义自定义QEvent类型
      2. 订阅此事件
      3. 使用QCoreApplication::postEvent()将您的事件发布到Qt事件循环中。

2
你提出的解决方案需要一种繁忙或短暂的超时等待,这是次优的。我更希望合并循环并且不运行Qt/Asio/Qt/Asio,尤其是当我等待以下之一时:用户输入/来自网络的输入可以同时发生。 - Artyom
1
没有免费的午餐。如果我理解正确,asio有一个重要部分是调用::run()函数,这个函数是阻塞的。如果这个函数是阻塞的,你要么从后台线程调用它,要么它可能会阻塞你的应用程序。你提到了事件循环集成,但是快速浏览后,我没有在asio中找到任何事件循环。异步调用不一定需要与事件循环配合使用。 - Philippe F
你也可以采用另一种方式:使用Qt来完成所有操作,然后将Qt网络事件“转换”为asio所期望的格式。这样,你就不需要使用asio,但是必须在与asio相同的方式下调用遗留代码。 - Philippe F
请查看以下链接:http://www.qtcentre.org/threads/53229-How-to-integrate-asio-io_service-with-Qt-event-loop - peper0

2

真正地整合主循环是可能的。只是这很麻烦(而且我还没有尝试过)。

在单独的线程上运行io_service::run()可能是最好的方法。


1
我刚刚将GLib事件循环集成到Boost.Asio中,因此在我的记忆中消失之前,我将分享一些笔记。
有几种方法可以集成GLib和Boost.Asio。第一种最简单的选择是生成一个新线程来运行g_main_loop_run()。新线程将在此调用上阻塞,直到您调用g_main_loop_quit()。但是,文档仅保证对于接收GMainContext的函数具有线程安全性,因此最好从GLib线程本身调用此函数。您可以通过调用g_main_context_invoke()来提交要在GLib线程中执行的作业。
第二种方法是控制由g_main_loop_run()完成的每个迭代间隔。这可以通过调用g_main_context_iteration()来完成。通过控制轮询,您可以随时从任何线程调用此函数。如果您正在编写SDL游戏等需要轮询的应用程序,则这是一个不错的选择。
如果您依赖于轮询,就会出现一个开放性问题:我应该多频繁地进行轮询?如果您轮询得太频繁,则会浪费CPU资源,但如果您轮询得太少,则会引入延迟/滞后。这就是第三种集成选项的作用:将 g_main_context_iteration() 调用拆分为一系列调用 g_main_context_prepare()g_main_context_query()g_main_context_check()g_main_context_dispatch()。但请使用 g_main_context_new_with_flags(G_MAIN_CONTEXT_FLAGS_OWNERLESS_POLLING) 创建 GMainContext,否则您将遇到一些竞争问题,详见 https://gitlab.gnome.org/GNOME/glib/-/commit/e26a8a59813ce651c881fe223e7d1a5034f2f816
我刚写的整合代码已经上线,你可以在https://gitlab.com/emilua/glib/-/blob/bc2b4236aa7f296a08739f23522809817229792f/src/service.cpp找到它。这段代码还考虑了Boost.Asio串,因此您应该能够从多个线程调用io_context.run()并且这段代码仍然有效。如果您不需要这个约束条件,代码可以简化很多。
当您编写此类代码时,请记住以下几点技巧:
请注意堆栈溢出。处理程序是否可能触发检测到另一个准备好的源,从而以嵌套方式调用第二个处理程序?避免这种情况。
调度语义并不总是安全的。代码很少在假定调用函数以安排某些IO时可以执行任意处理程序的情况下编写。默认情况下只进行发布语义,并最多提供调度语义作为选项。
小心饥饿和不公平。现在和然后总是将“事件循环滴答声”让给其他服务。
GLib特定规则:
  • GMainContext 是线程安全的。
  • 使用 GLib 的代码可能不会显式地引用或传递 GMainContext,而是直接引用全局的或线程默认的上下文。如果此代码使用线程默认上下文,则解决方案很简单(在处理程序分派之前设置线程默认值)。如果此代码使用全局上下文,则您的应用程序刚刚获得了一个新限制,即不创建多个 GMainContext 对象并始终引用同一个对象。
  • GLib 没有待处理工作的概念。它希望其循环永远运行。我之前提供的代码有一个解决方案。
  • 请勿重复使用上一次迭代中的“fd watchers”以尝试节省系统调用。兴趣集不是有状态的,GLib 不需要通知您已关闭在上一次迭代中使用的 fd。如果发生该事件,则程序的另一个线程可能会打开具有相同编号的 fd,您的 fd watcher 就成为了定时炸弹。危险程度取决于“fd watcher”实现方式(例如,我使用的是 epoll)和特定于您的程序的交互。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接