Qt: многопоточный дизайн DLL
Введение Это открытый вопрос, который, по моему мнению, может быть полезен для сообщества, потому что я не смог найти хорошего документа по этому вопросу. К сожалению, я усвоил трудный способ, которым реализация DLL в Qt отличается от других языков, как я объясню позже
Постановка проблемы Реализация многопоточной DLL в Qt, которая может легко использоваться не-Qt-приложениями
Справочная информация
Qt является инструментом выбора, потому что его присущая кроссплатформенная совместимость. API использует функции обратного вызова, чтобы сообщать вызывающему приложению, когда произошли определенные события.
Предположения
-Приложения, которые будут ссылаться на dll Qt, совместимы с компиляторами Qt (c/ C++ -mingw, C# -msvc) -Сигналы / слоты используются для связи от основного потока к рабочим потокам (например, сообщить рабочему потоку для сбора данных) а также из рабочих потоков обратно в основные потоки (например, уведомить основной поток через функцию обратного вызова о завершении сбора данных)
описание проблемы
Я усвоил сложный способ, которым написание многопоточной DLL в QT отличается от других языков из-за архитектуры Qt. Проблемы возникают из-за циклов событий QT, которые обрабатывают потоки spwaning, таймеры, отправляющие сигналы и слоты приема. Этот цикл Qt Even (QApplication.exec()) может вызываться из основного приложения, когда основным приложением является Qt (Qt имеет доступ к определенным библиотекам QT). Однако, когда вызывающее приложение не является Qt, например, C#, вызывающее приложение (также называемое основным потоком) не имеет возможности вызывать определенные библиотеки Qt, поэтому возникает необходимость в создании вашей DLL с встроенным в нее циклом обработки событий., Важно, чтобы это учитывалось в проекте заранее, потому что это сложно сделать позже, потому что QApplication.exec блокирует.
Короче говоря, я ищу мнения о том, как лучше всего спроектировать многопоточный dll в Qt так, чтобы он был совместим с приложениями, не относящимися к QT.
В итоге
- Где находится цикл обработки событий в общей архитектуре?
- Какие особые соображения вы должны сделать в отношении сигналов / слотов?
- Есть ли какие-то ошибки, с которыми столкнулось сообщество при реализации чего-то похожего на то, что я описал?
2 ответа
[...] когда вызывающее приложение не является Qt, например, C#, вызывающее приложение (также называемое основным потоком) не имеет возможности вызывать определенные библиотеки Qt, поэтому возникает необходимость в разработке вашей DLL со встроенным циклом событий внутри него.
Это не точно. В Windows вам нужен один цикл обработки событий для каждого потока, и этот цикл обработки событий может быть реализован с использованием чистого WINAPI, или с использованием C#, или любого другого языка / инфраструктуры, который вам необходим. Пока этот цикл обработки отправляет сообщения Windows, код Qt будет работать.
Единственное, что должно существовать в Qt - это экземпляр QApplication
(или же QGuiApplication
или же QCoreApplication
в зависимости от ваших потребностей) создается из основного потока.
Вы не должны звонить exec()
в этом случае, поскольку нативный код (основное приложение) уже качает сообщения Windows. Вам нужно позвонить QCoreApplication::processEvents
один раз после создания экземпляра приложения, чтобы "заправить" его. То, что вам нужно сделать, это ошибка (упущение), и я не уверен, что это даже необходимо в Qt 5.5.
После этого поток GUI в нативном приложении будет правильно отправлять нативные события в виджеты и объекты Qt.
Любые рабочие потоки, которые вы создаете, используя неизмененные QThread::run
будет вращать собственные циклы событий, и каждый из этих потоков может содержать собственные объекты (дескрипторы окон) и QObject
так же, как и выполнять асинхронные вызовы процедур.
Самый простой способ настроить все это - предоставить initialize
функция в DLL, которая вызывается основным приложением один раз для запуска 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, за некоторыми исключениями. Выделение QThread
или же QApplication
экземпляр там очевидный нет-нет. Постановка вызова APC из "текущего" (случайного) потока - это лучшее, что вы можете сделать, и до сих пор нет твердой гарантии, что поток продержится достаточно долго, чтобы выполнить ваш APC, или что он когда-либо будет с бдительностью ожидать, чтобы APCs может получить шанс бежать.
Если вы чувствуете себя авантюрным, согласно этому ответу, вы можете поставить в очередь вызов initialize()
как БТР. Основная проблема заключается в том, что вы никогда не можете быть уверены, что 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 с не-Qt языками, такими как C#, из-за проблем, перечисленных выше. Хотя Qt отлично подходит для предоставления многоплатформенного решения, у него есть недостаток, заключающийся в том, что он не очень дружелюбен к DLL, так как очень трудно заставить DLL работать на любом языке, кроме Qt. В настоящее время мы изучаем, хотим ли мы переписать всю нашу DLL в стандартном переносимом C++ и отказаться от реализации Qt, что было бы очень дорого.
Честное предупреждение, я бы не стал использовать QT в качестве основы при создании DLL.