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

QThreadPool и QRunnable: когда использовать вместо QThread

Введение

Создание отдельного QThread под каждую короткую фоновую задачу — расточительно: запуск и остановка потока имеют ощутимые накладные расходы на уровне ОС. Когда задач много, а каждая из них недолгая (загрузка файла, обработка изображения, парсинг данных), правильнее использовать пул потоков — QThreadPool, который держит ограниченное число рабочих потоков и распределяет между ними задачи в виде объектов QRunnable. В статье разберём, когда стоит выбрать пул потоков вместо ручного управления QThread.

Концепция

QThreadPool управляет переиспользуемым набором потоков и автоматически масштабирует их количество (по умолчанию — QThread::idealThreadCount(), то есть число логических ядер процессора). Задача оформляется как класс, наследующий QRunnable, с переопределённым методом run(), и передаётся в пул через start(). Пул сам решает, в каком потоке и когда выполнить задачу, ставя её в очередь, если все рабочие потоки заняты.

QThread уместен, когда нужен долгоживущий поток с собственным event loop (например, постоянно слушающий сокет или таймер), либо когда нужен явный контроль над жизненным циклом конкретного потока. QThreadPool уместен для разовых, независимых друг от друга вычислительных задач без необходимости в собственном event loop внутри задачи.

Пример кода

#include <QCoreApplication>
#include <QThreadPool>
#include <QRunnable>
#include <QDebug>
#include <QThread>

class ImageProcessingTask : public QRunnable
{
public:
    explicit ImageProcessingTask(int imageId) : m_imageId(imageId) {}

    void run() override
    {
        qDebug() << "Обработка изображения" << m_imageId
                  << "в потоке" << QThread::currentThreadId();
        // Имитация тяжёлой обработки
        QThread::msleep(500);
        qDebug() << "Изображение" << m_imageId << "обработано";
    }

private:
    int m_imageId;
};

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

    QThreadPool *pool = QThreadPool::globalInstance();
    qDebug() << "Максимум потоков в пуле:" << pool->maxThreadCount();

    for (int i = 0; i < 8; ++i) {
        auto *task = new ImageProcessingTask(i);
        task->setAutoDelete(true); // пул сам удалит задачу после выполнения
        pool->start(task);
    }

    pool->waitForDone(); // дождаться завершения всех задач
    qDebug() << "Все задачи выполнены";

    return 0;
}

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

QThreadPool::globalInstance() возвращает общий для всего приложения пул, которого достаточно в большинстве случаев — создавать собственный экземпляр QThreadPool стоит только если нужна изолированная очередь с собственными лимитами на число потоков. Каждая задача ImageProcessingTask наследует QRunnable и реализует run() — именно этот метод выполнится в одном из рабочих потоков пула.

Флаг setAutoDelete(true) (он установлен по умолчанию) указывает пулу самостоятельно удалить объект задачи после выполнения run() — это снимает с разработчика необходимость отслеживать жизненный цикл каждой задачи вручную. Вызов waitForDone() блокирует текущий поток до завершения всех поставленных задач — полезно в утилитах командной строки, но в GUI-приложении лучше использовать неблокирующее отслеживание прогресса.

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

  • run() не должен напрямую обновлять GUI. Виджеты Qt не потокобезопасны — любое взаимодействие с UI из рабочего потока должно происходить через сигналы/слоты с Qt::QueuedConnection (это происходит автоматически при подключении между объектами из разных потоков).
  • Глобальный пул разделяется всем приложением. Если в разных частях кода интенсивно используется QThreadPool::globalInstance(), задачи начинают конкурировать за ограниченное число потоков — для изоляции критичных по времени задач от фонового «мусора» стоит завести отдельный QThreadPool.
  • setAutoDelete(false) требует ручного управления памятью задачи — если забыть удалить объект QRunnable самостоятельно, возникнет утечка памяти.
  • Задачи в очереди не имеют приоритета по умолчанию равного значения. QThreadPool::start() принимает необязательный параметр приоритета, но без явного указания все задачи выполняются в порядке очереди — для дифференцированной обработки важности задач нужно расставлять приоритеты вручную.