Qt - 如何从线程创建 QFuture

Qt - How to create a QFuture from a thread

我目前正在开发一个编辑器程序;我需要编写一个功能,它需要使用项目的异步文件 API 连续加载多个文件,然后在加载这些文件后执行更多计算。

在另一种语言中,这可能会通过 async/await 工作流程来实现,例如:

let firstFile = await FileAPI.loadFile("Foo.xxx", ...);
let otherFile = await FileAPI.loadFile("Bar/Foobar.xxx", ...);

与此代码等效的 Qt 将使用 QtConcurrent::run 生成一个新线程,返回 QFuture,并等待该未来产生结果。

但是,在我从事的项目中,文件打开 API 在单个工作线程上运行,这意味着我无法使用 QtConcurrent::run.这是代码库的一个既定的、不可协商的部分。例如,文件 API 的构造函数如下所示:

FileApiWorker* worker = new FileApiWorker();
m_workerThread = new QThread();
worker->moveToThread( m_workerThread );

// Input signals
connect( this, &FileApi::loadFile, worker, &FileApiWorker::loadFile);
connect( this, &FileApi::loadData, worker, &FileApiWorker::loadData);
connect( this, &FileApi::loadDir, worker, &FileApiWorker::loadDir);

这意味着我访问文件系统数据的唯一方法是调用一个发出信号的方法,该方法在另一个线程上开始计算,最终在最后发出自己的信号以传递加载的数据。

这对于上面的用例来说是极其不切实际的,因为我实际上需要在另一个函数中说 "do thing, load data (with call back 'keep doing things')" 和 "keep doing things" 而不是说 "do thing, load data, wait, keep doing things",这会引入各种代码的脆弱性。 (而且,嗯,你知道,这正是我们发明期货的那种工作流程)

有什么方法可以从 loadFile 方法创建 QFuture 或一些未来等效对象(可以在方法内部等待),前提是 loadFile 总是在同一个工作线程上运行,我不能创建新线程?

IMO,不使用现成的解决方案(AsyncFuture)并尝试从头开始重写是很奇怪的。

但我可以建议我自己的 "wheel":lambda 作为插槽。

void FileApi::awaitLoadFile()
{
    qDebug() << "\"await\" thread is" << thread(); 

    emit loadFile("Foo.xxx");

    static bool once = connect(m_worker, &FileApiWorker::loadFileDone, this, // there is possible to avoid the third "this" parameter, but it is important to specify the lifetime of the connection and the receiver context while using lambdas
        [=](QByteArray result)
    {
        qDebug() << "\"comeback-in-place\" thread is" << thread(); // will be the same as "await" thread was

        // do what you need with your result
    },
    Qt::QueuedConnection // do not forget
    );

    qDebug() << "here is an immediate return from the \"await\" slot";
}

有用的文章New Signal Slot Syntax - Qt Wiki

在 Qt 中创建 QFuture 的最简单方法是使用未记录的 QFutureInterface class.

示例代码:

Q_DECLARE_METATYPE( QFutureInterface<FileData> );

// ...

qRegisterMetaType<QFutureInterface<FileData>>();

FileApiWorker* worker = new FileApiWorker();
connect( this, &FileApi::loadFile_signal, worker, &FileApiWorker::loadFile_signal);

// ...

QFuture<FileData> FileApi::loadFile()
{
    QFutureInterface<FileData> futureInterface;
    // IMPORTANT: This line is necessary to be able to wait for the future.
    futureInterface.reportStarted();

    emit loadFile_signal(futureInterface);
    return futureInterface.future();
}

FileApiWorker::loadFile_signal(QFutureInterface<FileData>& futureInterface)
{
    // Do some things
    // ...
    futureInterface.reportResult(...);
    // IMPORTANT: Without this line, future.waitForFinished() never returns.
    futureInterface.reportFinished();
}

需要考虑的一些因素:

  • 以上代码使用了Q_DECLARE_METATYPE;这是能够通过跨线程信号传递 QFutureInterface 所必需的。准确地说,如果不包含 Q_DECLARE_METATYPEconnect 行将无法编译;如果未调用 qRegisterMetaTypeemit loadFile_signal 行将在运行时失败。有关详细信息,请参阅 the Qt documentation on metatypes

  • 您可以传播错误,这样调用 loadFile().waitForFinished() 就会抛出错误。为此,需要创建一个特殊用途的class继承QException,然后调用:

    futureInterface.reportException( MyException(...) );
    futureInterface.reportFinished();
    

    在你的错误路径中。

    QException 本质上是 actual 需要在线程之间传输的异常的包装器。有关详细信息,请参阅 the documentation

  • 虽然 QFutureInterface 是稳定的,并且与 QFuture 和 QFutureWatcher 大部分具有相同的 API,但它仍然是一个未记录的功能,这可能会让在共享代码库中遇到它的贡献者感到惊讶。 class 可能是违反直觉的,如果您不遵守以上几点(我必须通过反复试验来学习),就会无声地失败。必须在使用 QFutureInterface 的任何共享代码的注释中强调这一点。 class的源代码can be found here.