Как QtConcurrent::run оказывается в основном потоке?

Я построил в своем приложении фасад асинхронной сети на основе 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: выполняемая лямбда просто выполняет 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);

person Tim    schedule 04.02.2015    source источник
comment
Почему вы используете QtConcurrent для отправки сетевых запросов? И не могли бы вы уточнить, что происходит внутри лямбды, переданной run? У меня такое чувство, что там что-то не так.   -  person peppe    schedule 05.02.2015
comment
Я хочу простой способ выдавать несколько сетевых запросов и смотреть на результат только позже, то есть с использованием будущего. QtConcurrent казалось, хорошо подходит для этого. Обновлен вопрос с более подробной информацией о лямбде.   -  person Tim    schedule 05.02.2015
comment
Но QNetworkAccessManager полностью асинхронен по своей природе. Они обеспечивают асинхронные сигналы для уведомления о том, что происходит. В чем проблема выдавать запросы и проверять их позже?   -  person peppe    schedule 05.02.2015
comment
Также я специально спросил, что делается в лямбде, потому что играет роль сходство потоков QObjects.   -  person peppe    schedule 05.02.2015
comment
QNetworkAccessManager полностью асинхронен, но только через сигналы/слоты, что не всегда является лучшим способом работы. Мой алгоритм будет вычислять различные вещи, некоторые из которых будут нуждаться в сетевых данных позже, другие будут нуждаться в материалах из БД. Я хотел простой способ представить «материал, полученный для последующего использования». QFuture кажется отличным способом абстрагироваться от источника данных и обеспечить очень удобный доступ к ним, когда мне это понадобится позже.   -  person Tim    schedule 05.02.2015
comment
Обновлен с полным кодом, показывающим, как выполняется сетевой запрос.   -  person Tim    schedule 05.02.2015


Ответы (1)


Видимо это по замыслу. Из 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() возвращает QFuture, который реализован с использованием QFutureInterface. QFutureInterface содержит этот код как в waitForFinished(), так и в waitForResult().

stealRunnable — это недокументированный закрытый метод QThreadPool. Это описано таким образом в 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.
*/

Итак, что мы получаем, так это то, что если QRunnable, созданный внутри QtConcurrent::run(), не был удален из очереди любого QThreadPool, которому он был назначен, то вызов waitForFinished или waitForResult заставит его работать в текущем потоке (т.е. не одновременно).

Это означает, что такой код (и то, что я сделал в вопросе) может не сработать загадочным образом:

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.
}

Я получил свой дизайн (получение будущего из QNetowrkAccessManager) с использованием std::future и std::async.

person Tim    schedule 05.02.2015
comment
Это укусило меня - и кажется довольно сломанным ... Было бы неплохо, если бы вы могли хотя бы отключить это поведение. Это особенно не интуитивно понятно, когда вы явно указываете пул потоков, в котором хотите запустить функцию, но вместо этого он запускается в вызывающем потоке... -- например: QtConcurrent::run(&myThreadPool, fn) - person Ben; 16.08.2016
comment
Да, это раздражает. Вместо этого я использую std::async, что гораздо лучше, если вы можете использовать c++ 11. - person Tim; 17.08.2016
comment
Не могли бы вы поделиться фрагментом кода, показывающим, как вы преобразовали приведенный выше пример для использования std::async? Я немного посмотрел на std::async, увидев ваш ответ на этот вопрос, но я не уверен, что это сработает для моего варианта использования. У меня есть необходимость запланировать асинхронную работу для запуска в конкретном долгоживущем потоке... (я хакерски добиваюсь этого в своем сценарии прямо сейчас, используя QThreadPool с максимальным размером потока, установленным равным 1, и бесконечной датой истечения срока действия потока.. .) Если вы знаете способ добиться этого с помощью std::async, мне бы хотелось получить несколько указателей... - person Ben; 17.08.2016
comment
К сожалению, std::async не поставляется с std::thread_pool. Но если вы используете QThreadPool, вы можете подключить сигнал от вашего runnable, чтобы узнать, когда он завершен, и полностью избежать использования QtConcurrent. - person Tim; 17.08.2016
comment
Хороший совет - я не хотел запускать MOC в этой части кода - этот вариант использования произошел в небольшом классе, который является частью внешнего интерфейса, используемого проектами, отличными от qt ... Возможно, я могу реорганизовать и решить со слотами/сигналами... QtConcurrent делает все, что мне нужно, как есть (за исключением этой странной причуды внутри waitForFinished())... Я мог бы просто продолжать использовать QtConcurrent, но решить эту конкретную проблему с помощью блокировки - она ​​появляется (я думаю) что отсутствие вызова waitForFinished() достаточно, чтобы никогда не запускать путь кодаstealRunnable()...? - person Ben; 18.08.2016
comment
Почти через 6 лет я столкнулся с той же проблемой. Это плохо спроектированный API. Из-за этого неправильного поведения (и, возможно, нескольких других проблем) мое маленькое приложение зашло в тупик. - person Nawaz; 24.01.2021