Qt:多线程DLL设计



简介这是一个开放式的问题,我认为这可能对社区有益,因为我一直找不到关于这方面的好文件。不幸的是,我通过艰难的方式了解到,用Qt实现DLL与其他语言不同,稍后我将解释

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

背景信息

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

假设

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

问题描述

我艰难地了解到,由于QT的体系结构,用QT编写多线程DLL与其他语言不同。由于QT事件循环处理间谍线程、计时器、发送信号和接收插槽,因此会出现问题。当主应用程序是Qt(Qt可以访问Qt特定的库)时,可以从主应用程序调用此Qt-even循环(QApplication.exec())。然而,当调用应用程序不是Qt(例如C#)时,调用应用程序(也称为主线程)就无法调用Qt特定的库,因此有必要设计嵌入事件循环的DLL。重要的是,在设计中要提前考虑这一点,因为由于QApplication.exec处于阻塞状态,以后很难将其插入。

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

总结

  • 事件循环在整个体系结构中的位置
  • 关于信号/插槽,您应该特别考虑哪些事项
  • 当执行类似于我所描述的内容

[…]例如,当调用应用程序不是Qt时,C#,调用应用程序(也称为主线程)不具备调用Qt特定库的能力,因此有必要设计嵌入事件循环的DLL。

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

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

您不能在该实例上调用exec(),因为本机代码(主应用程序)已经在发送窗口消息。在创建应用程序实例后,您确实需要调用QCoreApplication::processEvents一次来"初始化"它。您确实需要这样做是一个错误(遗漏),我不确定它在Qt 5.5中是否有必要。

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

使用未更改的QThread::run创建的任何工作线程都将旋转本机事件循环,并且这些线程中的每一个都可以承载本机对象(窗口句柄)和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中进行初始化的要求并不特定于Qt。使用代码的本地WINAPI被禁止在DllMain中做任何事情-不可能创建窗口等。

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


根据这个答案,如果你觉得有冒险精神,你可以把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一样。

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

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

最新更新