【问题标题】:Qt - How to create a QFuture from a threadQt - 如何从线程创建 QFuture
【发布时间】:2020-02-04 18:13:37
【问题描述】:

我目前正在开发一个编辑器程序;我需要编写一个功能,它需要使用项目的异步文件 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);

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

这对于上面的用例来说是极其不切实际的,因为我不需要说“做事,加载数据,等待,继续做事”,我本质上需要说“做事,加载数据(带有回调'继续做事” ')”和“继续做事”在另一个函数中,这在代码中引入了各种脆弱性。 (而且,嗯,你知道,这正是我们发明期货的那种工作流程)

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

【问题讨论】:

标签: c++ multithreading qt async-await future


【解决方案1】:

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

示例代码:

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() 引发错误的方式传播错误。为此,需要创建一个继承QException的专用类,然后调用:

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

    在您的错误路径中。

    QException 本质上是对需要在线程之间传输的实际 异常的包装。详情请见the documentation

  • 虽然 QFutureInterface 很稳定,并且大多数 API 与 QFuture 和 QFutureWatcher 相同,但它仍然是一个未记录的功能,这可能会让在共享代码库中遇到它的贡献者感到惊讶。如果您不尊重上述几点(我必须通过反复试验来学习),这门课可能会违反直觉,并且会默默地失败。必须在使用 QFutureInterface 的任何共享代码的 cmets 中强调这一点。类的源代码can be found here

【讨论】:

    【解决方案2】:

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

    但我可以建议我自己的“轮子”: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

    【讨论】:

    • 之后 lambda 没有断开连接。多次调用它会导致同一个 lambda 槽被多次执行。可能的解决方案是使用另一个被 lambda 破坏的 QObject。但是,这仍然不适用于并行调用,因为第二个调用可能在第一个调用之前完成,因此两个 lambdas 仍将被执行。
    • @LNJ 是的,每个 awaitLoadFile() 调用都应该产生一个新的 lambda 调用,为什么这是个问题?
    猜你喜欢
    • 2013-04-15
    • 2020-06-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-07-05
    • 1970-01-01
    相关资源
    最近更新 更多