Qt:多线程 DLL 设计

4

简介

这是一个开放式问题,我认为对社区有益,因为我一直无法找到关于这方面的很好的文档。不幸的是,我后来发现,在Qt中实现DLL与其他语言不同,我将在后面解释。

问题陈述

在Qt中实现一个多线程DLL,可以轻松地被非Qt应用程序使用。

背景信息

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

假设

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

问题描述

我后来发现,由于Qt的架构,使用QT编写多线程DLL与其他语言不同。由于QT事件循环处理生成线程、计时器、发送信号和接收插槽,因此会出现问题。当主应用程序不是Qt时(例如C#),调用应用程序(即主线程)无法调用Qt特定库,因此有必要在DLL内嵌入事件循环。这一点在设计时需要事先考虑到,因为QApplication.exec()是阻塞的,所以后期强行添加困难重重。

简而言之,我正在寻求关于如何构建一个多线程dll的最佳方法,使其与非Qt应用程序兼容的意见。

总结

  • 事件循环在整个架构中的位置在哪里?
  • 在信号/插槽方面应该考虑什么特殊因素?
  • 当实现类似于我描述的内容时,社区遇到了哪些问题?

嘘...它是“Qt”。 :p - Mitch
请参见此链接:https://dev59.com/3nPYa4cB1Zd3GeqPkY9O#27969425 - Nejat
2个回答

1
当调用应用程序不是Qt时,例如C#,调用应用程序(也称为主线程)无法调用Qt特定库,因此有必要在DLL中嵌入事件循环。但这并不准确。在Windows上,每个线程都需要一个事件循环,并且可以使用纯WINAPI或使用C#或任何语言/框架来实现该事件循环。只要该事件循环正在分派窗口消息,Qt代码就会起作用。唯一需要存在的Qt特定内容是从主线程创建的QApplication(或根据您的需求创建的QGuiApplication或QCoreApplication)的实例。不应在该实例上调用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实例显然是不可以的。从“当前”(随机)线程排队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 时那样。

0

为了让您从我们的错误中学习,我来快速更新一下。当我们尝试将我们用Qt编写的dll与非Qt语言(如C#)集成时,遇到了各种问题,正如上面列出的问题所述。虽然Qt在提供多平台解决方案方面非常出色,但它的缺点是不太友好于DLL,因为很难使DLL在除Qt之外的任何语言中工作。我们目前正在调查是否要重写我们整个DLL并放弃Qt实现,这将非常昂贵。

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


非常抱歉如果我的回答误导了你。解决方案相当简单:在Windows上,Qt只要求主线程提供一个自旋的本地事件循环。由Qt使用的Windows事件分派器将与本地事件循环集成。这就是全部。不需要专门的GUI线程——一个已经存在,并且您永远不需要调用app.exec()。您需要存在一个QApplication实例,最好通过Q_GLOBAL_STATIC创建,并且在创建后调用processEvents一次。就这样。我以这种方式编写了一个VS插件,所以据我所知——它可以正常工作。 - Kuba hasn't forgotten Monica
其他(非GUI)线程在任何方面都不是特殊的,无论您是否从DLL中的代码启动它们都没有关系。重要的是不要尝试启动单独的GUI线程 - 已经存在一个。 - Kuba hasn't forgotten Monica

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