Потоки и многопоточность

QFuture и QtConcurrent: асинхронные вычисления без ручного управления потоками

5 просмотров

Введение

Модуль QtConcurrent предоставляет высокоуровневый API для параллельных вычислений, скрывающий детали создания и синхронизации потоков. Вместо явного создания QThread или QRunnable достаточно вызвать функцию вроде QtConcurrent::run(), передав ей обычную функцию или лямбду — она будет выполнена в пуле потоков, а результат можно получить через объект QFuture. В статье разберём базовые сценарии использования QFuture и QtConcurrent::run.

Концепция

QtConcurrent::run() помещает переданный вызываемый объект в QThreadPool и немедленно возвращает QFuture<T>, который представляет собой «обещание» будущего результата. У QFuture есть методы для блокирующего ожидания (result(), waitForFinished()) и неблокирующей проверки состояния (isFinished(), isRunning()). Для реакции на завершение без блокирования потока используется QFutureWatcher, который испускает сигнал finished() при готовности результата — это позволяет интегрировать асинхронные вычисления в событийную модель Qt без блокировки GUI-потока.

Пример кода

#include <QCoreApplication>
#include <QtConcurrent/QtConcurrent>
#include <QFuture>
#include <QFutureWatcher>
#include <QDebug>

long long computeFactorial(int n)
{
    long long result = 1;
    for (int i = 2; i <= n; ++i) {
        result *= i;
    }
    return result;
}

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);

    // Вариант 1: блокирующее ожидание результата
    QFuture<long long> future = QtConcurrent::run(computeFactorial, 15);
    qDebug() << "Вычисление запущено в фоне...";
    long long blockingResult = future.result(); // блокирует текущий поток
    qDebug() << "15! =" << blockingResult;

    // Вариант 2: неблокирующее ожидание через QFutureWatcher
    auto *watcher = new QFutureWatcher<long long>;
    QObject::connect(watcher, &QFutureWatcher<long long>::finished, [watcher, &app]() {
        qDebug() << "20! =" << watcher->result();
        watcher->deleteLater();
        app.quit();
    });

    QFuture<long long> asyncFuture = QtConcurrent::run(computeFactorial, 20);
    watcher->setFuture(asyncFuture);

    return app.exec();
}

Пояснения к коду

Первый вариант показывает простейший, но блокирующий способ получить результат: future.result() дождётся завершения вычисления в текущем потоке. Это уместно в консольных утилитах, но недопустимо в GUI-потоке, так как заблокирует интерфейс на время вычисления.

Второй вариант — правильный паттерн для интерфейсных приложений: QFutureWatcher подключается к QFuture через setFuture(), и когда вычисление завершается в фоновом потоке, сигнал finished() доставляется в поток владельца watcher (как правило, основной поток) через стандартный механизм межпотоковых сигналов — результат можно безопасно забрать через watcher->result() без блокировки.

Подводные камни

  • future.result() в GUI-потоке замораживает интерфейс ровно так же, как синхронный вызов — использование QFuture само по себе не освобождает от необходимости думать об асинхронности взаимодействия с результатом.
  • Утечка QFutureWatcher, если не вызвать deleteLater() после получения результата — в примере это сделано внутри лямбды finished, но при копировании паттерна в реальный код об этом легко забыть.
  • QtConcurrent::run использует глобальный QThreadPool по умолчанию — при большом числе параллельных вызовов из разных частей приложения они конкурируют за общий пул потоков, как и при прямом использовании QRunnable.
  • Исключения внутри функции, переданной в QtConcurrent::run, перехватываются и сохраняются в QFuture, и будут выброшены повторно при вызове result() — если не обрабатывать их через try/catch вокруг result(), необработанное исключение приведёт к завершению приложения.