QtConcurrent::run 如何在主线程上结束?

Posted

技术标签:

【中文标题】QtConcurrent::run 如何在主线程上结束?【英文标题】:How does QtConcurrent::run wind up on main thread? 【发布时间】:2015-02-04 21:53:18 【问题描述】:

我在我的应用程序中构建了一个基于 QFuture 的异步网络外观。大致是这样工作的:

namespace NetworkFacade 
    QByteArray syncGet(const QUrl& url) 
        QEventLoop l;
        QByteArray rc;

        get(url, [&](const QByteArray& ba) 
            rc = ba;
            l.quit();
        );

        l.exec();
        return rc;
    

    void get(const QUrl& url, const std::function<void (const QByteArray&)>& handler) 
        QPointer<QNetworkAccessManager> m = new QNetworkAccessManager;

        QObject::connect(m, &QNetworkAccessManager::finished, [=, &m](QNetworkReply *r) 
            QByteArray ba;

            if (r && r -> error() == QNetworkReply::NoError)
                ba = r -> readAll();

            m.clear();

            if (handler)
                handler(ba);
        );
        m -> get(QNetworkRequest(url));
    

我有一个QTimer,它触发了对主线程的调用,该调用执行以下操作(显然简化了):

foreach(Request r, requests) 
    futures.push_back(get(r));


foreach(QFuture<SomeType> f, futures) 
    f.waitForFinished();
    [do stuff with f.result()]

我的假设是waitForFinished() 会在后台线程执行我的网络请求时阻塞主线程。相反,我收到 qFatal 错误:

ASSERT: "m_blockedRunLoopTimer == m_runLoopTimer" in file eventdispatchers/qeventdispatcher_cf.mm, line 237

在堆栈跟踪中,我在主线程上看到了我的waitForFinished(),但是我看到的不是被阻塞(从下往上读取):

com.myapp   0x0008b669 QEventDispatcherCoreFoundation::processEvents(QFlags<QEventLoop::ProcessEventsFlag>) + 1753
com.myapp   0x000643d7 QiosEventDispatcher::processEvents(QFlags<QEventLoop::ProcessEventsFlag>) + 823
com.myapp   0x0130e3c7 QEventLoop::processEvents(QFlags<QEventLoop::ProcessEventsFlag>) + 119
com.myapp   0x0130e5fb QEventLoop::exec(QFlags<QEventLoop::ProcessEventsFlag>) + 539
com.myapp   0x0003a550 NetworkFacade::syncGet(QUrl const&) + 208
com.myapp   0x00037ed1 QtConcurrent::StoredFunctorCall0<std::__1::shared_ptr<QuoteFacade::Quote>, QuoteFacade::closingQuote(QString const&, QDate const&)::$_0>::runFunctor() + 49
com.myapp   0x00038967 QtConcurrent::RunFunctionTask<std::__1::shared_ptr<QuoteFacade::Quote> >::run() + 87
com.myapp   0x00038abc non-virtual thunk to QtConcurrent::RunFunctionTask<std::__1::shared_ptr<QuoteFacade::Quote> >::run() + 28
com.myapp   0x010dc40f QThreadPoolPrivate::stealRunnable(QRunnable*) + 431
com.myapp   0x010d0c35 QFutureInterfaceBase::waitForFinished() + 165

因此,与其等待QFuture 获得值,不如在主线程上发出我所谓的并发任务。这会导致我上面概述的get() 函数被调用,该函数侦听QEventLoop 上的事件。与此同时,QTimer 再次触发,我从上面得到了断言。

我做错了什么,或者QtConcurrent::run 可以导致控制返回到主线程是完全有效的吗?

=== 更新 1

@peppe:正在执行的 lambda 只是简单地执行 HTTP GET 并生成将 JSON 响应解析为 SomeType 对象。通过QFuture 访问结果。

=== 更新 2

显然这是设计使然。来自 qfutureinterface.cpp 来自 Qt 5.4.0 第 293-295 行:

// To avoid deadlocks and reduce the number of threads used, try to
// run the runnable in the current thread.
d->pool()->d_func()->stealRunnable(d->runnable);

【问题讨论】:

为什么要使用 QtConcurrent 来调度网络请求?您能否详细说明传递给 run 的 lambda 内部发生了什么?我感觉里面有问题。 我想要一种简单的方法来发出多个网络请求,然后只看结果,即使用未来。 QtConcurrent 似乎很适合。使用有关 lambda 的更多详细信息更新了问题。 但是 QNetworkAccessManager 本质上是完全异步的。它们提供异步信号来通知正在发生的事情。发出请求并“稍后”检查它们有什么问题? 另外我还特意询问了在 lambda 中做了什么,因为 QObjects 的线程亲和性发挥了作用。 QNetworkAccessManager 是完全异步的,但只能通过信号/插槽,这并不是在所有情况下工作的最佳方式。我的算法将计算各种东西,一些需要网络数据,另一些需要来自数据库的东西。我想要一种简单的方法来表示“获取供以后使用的东西”。 QFuture 似乎是一种很好的方法来抽象数据的来源,并在我以后需要时提供非常方便的访问。 【参考方案1】:

显然这是设计使然。来自 Qt 5.4.0 第 293-295 行的 qfutureinterface.cpp:

// To avoid deadlocks and reduce the number of threads used, try to
// run the runnable in the current thread.
d->pool()->d_func()->stealRunnable(d->runnable);

QtConcurrent::run() 返回一个QFuture,它是使用QFutureInterface 实现的。 QFutureInterfacewaitForFinished()waitForResult() 中都包含该代码。

stealRunnableQThreadPool 的一个未公开的私有方法。在 headerdoc 中是这样描述的:

/*!
    \internal
    Searches for \a runnable in the queue, removes it from the queue and
    runs it if found. This function does not return until the runnable
    has completed.
*/

所以我们的结论是,如果QtConcurrent::run() 内部创建的QRunnable 没有从分配给它的QThreadPool 中出列,那么调用waitForFinishedwaitForResult 将导致它在当前线程上运行(即不并发)

这意味着这样的代码(以及我在问题中所做的)可能会以神秘的方式失败:

foreach (FuncPossiblyTriggeringQEvents fn, tasks) 
    futures.push_back(QtConcurrent::run(fn));


foreach (QFuture<> f, futures) 
    f.waitForFinished(); // Some of my tasks will run in this thread, not concurrently.

通过使用std::futurestd::async,我得到了我的设计(从QNetowrkAccessManager 获取未来)。

【讨论】:

这让我有点不舒服——而且看起来很糟糕......如果你至少可以关闭这种行为肯定会很好。当您明确指定要在其上运行函数的线程池时,它特别不直观-而是在调用线程上运行...-例如: QtConcurrent::run(&myThreadPool, fn) 是的,这很烦人。我改用std::async,如果你可以使用c++ 11,效果会更好。 你愿意分享一个代码 sn-p 显示你如何将上面的例子转换为使用 std::async 吗?看到您对此的回答后,我看了一点 std::async ——但我不确定它是否适用于我的用例。我需要安排异步工作以在特定的长寿命线程上运行......(我现在正在我的场景中通过使用最大线程大小设置为 1 和无限线程到期日期的 QThreadPool 来实现这一点...... .) 如果您碰巧知道使用 std::async 实现此目的的方法,我会喜欢一些指针... std::async 不幸的是没有 std::thread_pool。但是,如果您使用的是QThreadPool,您可以连接来自您的runnable 的信号以了解它何时完成并完全避免使用QtConcurrent 差不多 6 年后,我遇到了同样的问题。这是一个设计糟糕的 API。由于这种破坏行为(可能还有其他一些问题),我的小应用程序出现了死锁。

以上是关于QtConcurrent::run 如何在主线程上结束?的主要内容,如果未能解决你的问题,请参考以下文章

QtConcurrent::run 如何停止后台任务

使用 QtConcurrent::run 在单独的线程上连接信号/插槽

停止由 QtConcurrent::run 启动的线程?

QtConcurrent::run => QWaitCondition: 在线程仍在等待时被销毁

qt 创建线程

在 QtConcurrent::run 中使用 QSqlDatabase 连接(伪连接池)