【问题标题】:Qt: Multi-Threaded DLL DesignQt:多线程 DLL 设计
【发布时间】:2015-02-25 16:56:32
【问题描述】:

简介 这是一个开放式问题,我认为这可能对社区有益,因为我一直无法找到与此相关的出色文档。不幸的是,我学到了在 Qt 中实现 DLL 与在其他语言中不同的艰难方式,我稍后会解释

问题陈述 在 Qt 中实现一个非 Qt 应用程序可以轻松使用的多线程 DLL

背景信息

Qt 是首选工具,因为它固有的跨平台兼容性 API 使用回调函数来告诉调用应用程序何时发生某些事件

假设

-将链接到 Qt dll 的应用程序与 Qt 编译器兼容(c/c++ -mingw,C# -msvc) - 信号/槽用于从主线程到工作线程进行通信(例如告诉工作线程收集数据)以及从工作线程返回到主线程(例如通过回调函数通知主线程数据收集已完成)

问题描述

由于 Qt 的体系结构,我了解到在 QT 中编写多线程 DLL 与其他语言不同。由于 QT 事件循环处理生成线程、计时器、发送信号和接收槽,会出现问题。当主应用程序是 Qt(Qt 可以访问 QT 特定库)时,可以从主应用程序调用此 Qt 偶数循环(QApplication.exec())。但是,当调用应用程序不是 Qt 时,例如 C#,调用应用程序(也称为主线程)因此无法调用 Qt 特定的库,因此有必要设计您的 DLL,并在其中嵌入事件循环.重要的是,这在设计中被预先考虑,因为以后很难硬塞它,因为 QApplication.exec 是阻塞的。

简而言之,我正在寻找有关在 Qt 中构建多线程 dll 以使其与非 QT 应用程序兼容的最佳方法的意见。

总结

  • 事件循环在整体架构中的位置是什么?
  • 对于信号/插槽,您应该特别注意哪些事项?
  • 社区有没有遇到过什么问题? 实现类似于我所描述的内容?

【问题讨论】:

标签: qt dll qthread qeventloop qcoreapplication


【解决方案1】:

只是为了提供有关此内容的快速更新,以便您从我们的错误中吸取教训。由于上面列出的问题,当我们尝试将 Qt 编写的 dll 与 C# 等非 Qt 语言集成时,我们遇到了各种类型的问题。虽然 Qt 擅长提供多平台解决方案,但它的缺点是对 DLL 不太友好,因为很难让 DLL 工作在 Qt 以外的任何语言上。我们目前正在调查我们是否想用标准的可移植 C++ 重写我们的整个 DLL 并放弃将非常昂贵的 Qt 实现。

公平的警告,我会避免在创建 DLL 时使用 QT 作为您的框架。

【讨论】:

  • 如果我的回答误导了您,我很抱歉。解决方案相当简单:在 Windows 上,Qt 只要求主线程提供一个自旋的本机事件循环。 Qt 使用的 windows 事件调度器将与本机事件循环集成。就这样。不需要专用的 gui 线程 - 一个已经存在,而且您永远不需要调用 app.exec()。您需要在那里存在一个QApplication 实例,最好是通过Q_GLOBAL_STATIC 创建,并且您需要在创建它之后调用processEvents 一次。而已。我用这种方式编写了一个 VS 插件,据我所知 - 它可以工作。
  • 其他(非 gui)线程在任何方面都不是特别的,无论您是否从 DLL 中的代码中触发它们都无关紧要。重要的是不要尝试启动一个单独的 GUI 线程 - 一个已经存在。
【解决方案2】:

[...] 当调用应用程序不是 Qt 时,例如 C#,调用应用程序(也称为主线程)没有能力调用 Qt 特定的库,因此有必要使用事件循环嵌入其中。

这不准确。在 Windows 上,每个线程需要一个事件循环,并且可以使用纯 WINAPI、C# 或任何您需要的语言/框架来实现该事件循环。只要该事件循环正在调度 Windows 消息,Qt 代码就可以工作。

唯一需要存在的特定于 Qt 的东西是从主线程创建的 QApplication(或 QGuiApplicationQCoreApplication,取决于您的需要)的实例。

不得在该实例上调用exec(),因为本机代码(主应用程序)已经在发送 Windows 消息。您确实需要在创建应用程序实例后调用QCoreApplication::processEvents一次,以“启动”它。您确实需要这样做是一个错误(遗漏),我不确定在 Qt 5.5 中是否有必要。

之后,本机应用程序中的 gui 线程将正确地将本机事件分派给 Qt 小部件和对象。

您使用未更改的QThread::run 创建的任何工作线程都将旋转本机事件循环,并且每个线程都可以托管本机对象(Windows 句柄)和 QObject 类似的对象,以及执行异步过程调用。

最简单的设置方法是在 DLL 中提供一个 initialize 函数,该函数由主应用程序调用一次以启动 Qt:

static int argc = 1;
static char arg0[] = ""; 
static char * argv[] = { arg0, nullptr };
Q_GLOBAL_STATIC_WITH_ARGS(QApplication, app, (arc, argv))

extern "C" __declspec(dllexport) void initialize() {
  app->processEvents(); // prime the application instance
  new MyWindow(app)->show();
}

DllMain 中不进行初始化的要求是not specific to Qt。禁止使用原生 WINAPI 的代码在 DllMain 中执行大部分操作 - 无法创建窗口等。

我重申,从DllMain 执行任何可能分配内存、窗口句柄、线程等的操作都是错误的。您只能调用kernel32 API,但有一些例外。在那里分配QThreadQApplication 实例显然是禁忌。将来自“当前”(随机)线程的 APC 调用排队是您能做的最好的事情,但仍然不能保证该线程将存活足够长的时间来执行您的 APC,或者它会警觉地等待以便 APC可以有机会跑。


如果您喜欢冒险,according to this answer,您可以将呼叫排队到 initialize() 作为 APC。那么主要的问题是你永远无法确定DllMain 是从正确的线程调用的。调用它的线程必须最终处于可警报等待状态(例如泵送消息循环)。然后您可以创建一个专用的应用程序线程,并且无法确定是否应该使用任何特定的其他“主”线程来代替新线程。线程句柄必须分离,因此我们必须使用std::thread 而不是QThread

void guiWorker() {
  int argc = 1;
  const char dummy[] = "";
  char * argv[] = { const_cast<char*>(dummy), 0 };
  QApplication app(argc, argv);
  QLabel label("Hello, World!");
  label.show();
  app.exec();
}

VOID CALLBACK Start(_In_ ULONG_PTR) {
  std::thread thread { guiWorker };
  thread.detach();
}    

BOOL WINAPI DllMain(HINSTANCE, DWORD reason, LPVOID)
{
  switch (reason) {
  case DLL_PROCESS_ATTACH:
    QueueUserAPC(Start, GetCurrentThread(), NULL);
    break;
  case DLL_PROCESS_DETACH:
    // Reasonably safe, doesn't allocate
    if (QCoreApplication::instance()) QCoreApplication::instance()->quit();
    break;
  }
}

一般来说,您永远不需要这样的代码。主应用程序必须从带有事件泵的线程(通常是主线程)调用初始化函数,然后一切都会正常工作 - 就像它只使用本机功能初始化 DLL 一样。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多