首页 > 解决方案 > 在导出的 python 模块中使用异步 c++/qt 函数

问题描述

我有一个 dll 广泛依赖于 Qt 并导出某些返回 QFuture 的异步函数,例如

QFuture<QString> loadUserAsync();

我想导出这样的函数(通过pybind11),以便客户可以用python编写脚本。不过,我不想将异步 Qt 接口泄漏到 python 中,因此我正在用 C++ 编写一个包装 API,如下所示:

class API_EXPORT API {
public:
   std::string loadUsername();
   //...
};

std::string API::loadUsername() {
   Future<QString> future = _core->loadUserAsync();
   return future.result().toStdString(); 
}

然后通过pybind11导出:

py::class_<API>(m, "api")
  .def(py::init<>())
  .def("loadUsername", &API::loadUsername);

好吧,这有多个问题,我正在努力如何正确解决这个问题。

首先,我当然需要实例化 aQCoreApplication,以便库中的信号/插槽和事件正常工作。这似乎可行,但我真的不确定这是否被认为是最佳实践以及是否必须调用该exec函数(我不能exec在调用线程上调用,否则它会阻塞):

API::API() {
    if (!QCoreApplication::instance()) {
      int argc = 1;
      char* argv[] = {"api"};
      _qt = std::make_shared<QCoreApplication>(argc, argv);
    }
}

第二future.result().toStdString();死锁。我可以“修复”这个实例化我自己的QEventLoop,但我不确定这是否是要走的路:

QFutureWatcher<void> watcher;
QEventLoop loop;

watcher.connect(&watcher, SIGNAL(finished()), &loop, SLOT(quit()), Qt::QueuedConnection);
watcher.setFuture(future);

loop.exec();

第三,在 dll a 中的某处QTimer被实例化,因此我在 python 中打印出令人讨厌的警告,我很困惑如何处理它:

QObject::startTimer: Timers can only be used with threads started with QThread

标签: pythonc++qtpybind11qfuture

解决方案


请让我先说我像瘟疫一样避免使用 Python。

你有没有在没有任何 Python 的情况下在 C++ 土地上工作?我问是因为:

std::string API::loadUsername() {
   Future<QString> future = _core->loadUserAsync();
   return future.result().toStdString(); 
}

不可能工作。

是的,我之前发表了评论。因为 Qt 是一个应用程序框架,所以不能“洒上一点 Qt”。它也是一个深受单线程影响的应用程序框架,因为在主事件循环之外几乎没有进行任何测试。他们还会抱怨运行多个事件循环是一种反模式。(这是应用现实,但这是一个不同的论点。)

QFuture 的全部意义在于使用线程池中的线程,并在该函数/任务成功或悲惨地结束时发出信号。

如果您的 return 语句实际上返回了您想要的值,那么您根本不需要 QFuture,因为您发生了阻塞 I/O。

顺便说一句,这个:

API::API() {
    if (!QCoreApplication::instance()) {
      int argc = 1;
      char* argv[] = {"api"};
      _qt = std::make_shared<QCoreApplication>(argc, argv);
    }
}

考虑到所有当时古老的 x86 和较低处理器的知识,关于第一个参数是可执行文件的完整路径,这是令人难以置信的危险。在QCoreApplication中设置了一些东西,比如 applicationFilePath() 有点依赖它。

这是我在彩票跟踪器应用程序中使用 QFuture 的一些帖子,因为数据库 I/O 可能很长。

https://www.logikalsolutions.com/wordpress/information-technology/how-far-weve-come-pt-12/ https://www.logikalsolutions.com/wordpress/information-technology/how-far-weve- come-pt-13/ https://www.logikalsolutions.com/wordpress/information-technology/how-far-weve-come-pt-14/

https://www.logikalsolutions.com/wordpress/information-technology/how-far-weve-come-pt-16/

第 16 部分有我将在下面粘贴的代码,但您需要阅读第 12、13 和 14 部分才能理解目标。

void DataBaseIO::top12Report()
{
    QFuture future = QtConcurrent::run(this, &DataBaseIO::detachedTop12);
}

void DataBaseIO::detachedTop12()
{
    QString msgTxt;
    QTextStream rpt(&msgTxt);

    QSqlQuery q(db);

    rpt << "Number   hit_count" << endl
        << "--------- ---------" << endl;

    rpt.setFieldWidth(9);
    rpt.setFieldAlignment(QTextStream::AlignRight);

    q.exec("select elm_no, count(*) as hit_count from drawings group by elm_no order by hit_count desc limit 12;");

    while (q.next())
    {
        QSqlRecord rec = q.record();
        int no = rec.field("elm_no").value().toInt();
        int hits = rec.field("hit_count").value().toInt();
        rpt << no << hits << endl;
    }

    rpt.flush();

    emit displayReport( "Top 12 Report", msgTxt);
}

使用 QFuture 最简单的方法是通过 run()。不需要观察者。在你的任务结束时发出一个信号。诚然,您无法知道这是否会失败,因为您没有观察者。

顺便说一句,您的计时器来自 QFutureWatcher,因为它有一个 timerEvent()。 https://doc.qt.io/qt-5/qfuturewatcher-members.html

一旦你只在 C++ 中工作,你需要阅读 C++ 信号和 Python。

https://www.tutorialspoint.com/pyqt5/pyqt5_signals_and_slots.htm

https://www.mfitzp.com/tutorials/pyqt-signals-slots-events/

没有办法绕过应用程序必须有这样的东西。

import sys
from PyQt5.QtWidgets import QApplication

app = QApplication(sys.argv)

app.exec()

语法可能并不完美,因为我避免使用 Python。

你不能只扔一点 Qt。谁使用你正在开发的东西,谁就必须使用整个框架。

Qt 有一个有限的子集不需要主事件循环。我找不到东西的清单。该项目可能不再发布它。现实情况是,如果没有主事件循环,您将无能为力。当您发出信号时,它必须有一个事件循环,以便它可以放在事件队列中(假设它不直接连接)。在 C++ 中,emit 在很多时候实际上是一个直接的函数调用。我不知道 Python 的世界。我认为它必须是一个排队事件。

根据经验,如果您使用任何具有信号的类,则必须运行一个主事件循环。如果您允许 Python 加入,那么 Python 必须启动主事件循环。如果没有,它就无法与 Qt 通信。

这是一个关于如何从 Python 使用 C/C++ 的非常详细的教程。 https://realpython.com/python-bindings-overview/

底线是,除非你在 Qt 上全力以赴,否则你无法做你想做的事。

很可能您可以使用纯 C++ 执行您尝试执行的操作,具体取决于您将 loadUserAsync() 重写为无任何 Qt 的直接 I/O 阻塞函数的能力。


推荐阅读