Gestion Avancée des Fils d'Exécution avec Qt

Introduction au Multithreading avec Qt

L'intégration du multithreading dans les applications modernes est cruciale pour maintenir une interface utilisateur (UI) réactive et exploiter pleinement les capacités des processeurs multi-cœurs. Lorsque des opérations lourdes, comme des calculs complexes, des requêtes réseau ou du traitement de données, sont exécutées directement sur le fil d'exécution principal (UI), l'application peut sembler geler, offrant une mauvaise expérience utilisateur. Les fils d'exécution secondaires (ou "threads" en anglais) permettent d'exécuter ces tâches en arrière-plan, libérant ainsi le fil d'exécution principal pour qu'il puisse continuer à traiter les événements de l'interface.

Qt fournit la classe QThread pour gérer les fils d'exécution. Il existe principalement deux approches pour utiliser QThread :

  1. Héritage de QThread : Le moyen le plus simple d'initier une tâche dans un nouveau fil d'exécution est d'hériter de QThread et de redéfinir sa méthode virtuelle run(). Cette méthode contient le code qui sera exécuté dans le nouveau fil.
  2. Déplacement d'un objet vers un fil d'exécution (moveToThread) : C'est la méthode préférée et la plus flexible. Elle consiste à créer une instance de QObject (ou une classe en dérivant) contenant les "slots" de travail, puis à la déplacer vers un QThread distinct à l'aide de moveToThread(). Cela permet de séparer calirement la logique de travail de la gestion du fil d'exécution lui-même.

Quelques considérations importantes lors de l'utilisation de fils d'exécution avec Qt :

  • Les objets de l'interface utilisateur (dérivant de QWidget) ne doivent jamais être manipulés directement depuis un fil d'exécution secondaire. Toutes les mises à jour de l'UI doivent être effectuées via des signaux et des slots connectés au fil d'exécution principal.
  • Lorsqu'un objet est destiné à être déplacé vers un fil d'exécution secondaire avec moveToThread(), il ne doit pas avoir de parent. La gestion de sa durée de vie sera alors assurée par le fil d'exécution vers lequel il est déplacé.
  1. Approche par Héritage de QThread

Cette section illustre l'approche consistant à dériver une classe de QThread pour exécuter une tâche bloquante en arrière-plan. La méthode run() de la classe dérivée sera exécutée dès que start() est appelée sur l'instance du fil d'exécution.

Classe de Tâche (Worker)

Nous définissons une classe TaskWorker qui hérite de QThread. Sa méthode run() simulera une opération longue et émettra un signal une fois celle-ci terminée.

taskworker.h :

#ifndef TASKWORKER_H
#define TASKWORKER_H

#include <QThread>

class TaskWorker : public QThread
{
    Q_OBJECT
public:
    explicit TaskWorker(QObject *parent = nullptr);

signals:
    void taskFinished(); // Signal émis à la fin de la tâche

protected:
    void run() override; // Méthode exécutée dans le nouveau fil
};

#endif // TASKWORKER_H

taskworker.cpp :

#include "taskworker.h"
#include <QDebug>

TaskWorker::TaskWorker(QObject *parent) : QThread(parent)
{
}

void TaskWorker::run()
{
    qDebug() << "Début de la tâche dans le fil d'exécution : " << QThread::currentThreadId();
    // Simulation d'un traitement complexe de 5 secondes
    QThread::sleep(5);
    qDebug() << "Tâche terminée dans le fil d'exécution : " << QThread::currentThreadId();
    emit taskFinished(); // Informe l'interface que la tâche est achevée
}

Fenêtre Principale

La fenêtre principale, MainWindow, gère un compteur affiché sur un QLCDNumber et un bouton pour lancer la tâche. Un QTimer est utilisé pour incrémenter le compteur toutes les 100 ms. La tâche secondaire, après 5 secondes, arrêtera ce minuteur via un signal.

mainwindow.h :

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QWidget>
#include <QTimer>
#include "taskworker.h"

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QWidget
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private slots:
    void on_startButton_clicked(); // Slot pour le clic sur le bouton de démarrage
    void updateLcdCounter();       // Slot pour la mise à jour du compteur
    void handleTaskCompletion();   // Slot pour gérer la fin de la tâche du fil d'exécution
    void cleanupOnDestroy();       // Slot pour nettoyer les ressources à la fermeture de la fenêtre

private:
    Ui::MainWindow *ui;
    QTimer *m_lcdTimer;         // Minuteur pour l'affichage LCD
    TaskWorker *m_longTaskThread; // Instance de notre fil d'exécution de tâche longue
};

#endif // MAINWINDOW_H

mainwindow.cpp :

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QDebug>

MainWindow::MainWindow(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    m_lcdTimer = new QTimer(this);
    m_longTaskThread = new TaskWorker(this); // Le fil d'exécution est enfant de la fenêtre

    // Connecte le minuteur pour mettre à jour l'affichage
    connect(m_lcdTimer, &QTimer::timeout, this, &MainWindow::updateLcdCounter);
    // Connecte le signal de fin de tâche du fil d'exécution à un slot de la fenêtre
    connect(m_longTaskThread, &TaskWorker::taskFinished, this, &MainWindow::handleTaskCompletion);
    // Connecte le signal de destruction de la fenêtre pour arrêter le fil d'exécution
    connect(this, &QWidget::destroyed, this, &MainWindow::cleanupOnDestroy);

    qDebug() << "Fil principal (UI) ID : " << QThread::currentThreadId();
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::updateLcdCounter()
{
    static int counter = 0;
    counter++;
    ui->lcdNumber->display(counter);
}

void MainWindow::handleTaskCompletion()
{
    // La tâche est terminée, arrête le minuteur
    m_lcdTimer->stop();
    qDebug() << "Minuteur arrêté par le signal du fil d'exécution.";
}

void MainWindow::cleanupOnDestroy()
{
    // Assure l'arrêt et le nettoyage du fil d'exécution à la fermeture de l'application
    m_longTaskThread->quit();
    m_longTaskThread->wait(); // Attend la fin de l'exécution du fil
    qDebug() << "Fil d'exécution arrêté et nettoyé.";
}

void MainWindow::on_startButton_clicked()
{
    if (!m_lcdTimer->isActive()) {
        m_lcdTimer->start(100); // Démarre le minuteur (toutes les 100 ms)
    }
    if (!m_longTaskThread->isRunning()) {
        m_longTaskThread->start(); // Démarre le fil d'exécution de la tâche
    }
}

Lors de l'exécution, le compteur commence à s'incrémenter. Après environ 5 secondes (le temps de sommeil du fil d'exécution), le compteur s'arrête, indiquant que le signal taskFinished() a été reçu et traité par le fil principal.

  1. Modèle de Travailleur avec moveToThread

L'approche moveToThread est généralement préférée car elle permet de mieux séparer les repsonsabilités : QThread gère le fil d'exécution lui-même, tandis qu'un QObject dédié (le "travailleur") contient la logique métier à exécuter dans ce fil. Ce travailleur communique avec le fil principal via des signaux et slots, et peut être arrêté de manière plus souple.

Classe Travailleur (Worker Object)

Notre objet travialleur, ComputationWorker, hérite de QObject. Il contient une méthode pour démarrer une computation itérative et une flag pour gérer son arrêt.

computationworker.h :

#ifndef COMPUTATIONWORKER_H
#define COMPUTATIONWORKER_H

#include <QObject>

class ComputationWorker : public QObject
{
    Q_OBJECT
public:
    explicit ComputationWorker(QObject *parent = nullptr);

public slots:
    void startIterativeComputation(); // Slot pour démarrer le calcul itératif
    void requestStop();               // Slot pour demander l'arrêt du calcul

signals:
    void progressUpdated(int value); // Signal de mise à jour de la progression
    void computationCompleted();     // Signal indiquant la fin du calcul

private:
    bool m_stopRequested; // Flag pour signaler l'arrêt du traitement
};

#endif // COMPUTATIONWORKER_H

computationworker.cpp :

#include "computationworker.h"
#include <QThread> // Pour QThread::sleep() et QThread::currentThreadId()
#include <QDebug>

ComputationWorker::ComputationWorker(QObject *parent) : QObject(parent)
{
    m_stopRequested = false;
}

void ComputationWorker::startIterativeComputation()
{
    qDebug() << "Début de la computation dans le fil : " << QThread::currentThreadId();
    for (int i = 0; i < 20 && !m_stopRequested; ++i) {
        // Simule un travail en émettant des mises à jour de progression
        QThread::sleep(1); // Bloque le fil d'exécution du worker pendant 1 seconde
        emit progressUpdated(i + 1);
        qDebug() << "Progression : " << i + 1 << ", Fil : " << QThread::currentThreadId();
    }

    if (m_stopRequested) {
        qDebug() << "Computation arrêtée par requête.";
    } else {
        qDebug() << "Computation terminée naturellement.";
        emit computationCompleted();
    }
    m_stopRequested = false; // Réinitialise la flag pour une prochaine utilisation
}

void ComputationWorker::requestStop()
{
    m_stopRequested = true;
    qDebug() << "Requête d'arrêt reçue.";
}

Fenêtre Principale

La MainWindow instancie QThread et ComputationWorker, déplace le travailleur vers le fil d'exécution, puis connecte les signaux/slots pour démarrer et arrêter la computation, et afficher la progression.

mainwindow.h :

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QWidget>
#include <QThread>
#include "computationworker.h"

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QWidget
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private slots:
    void on_startComputationButton_clicked();
    void on_stopComputationButton_clicked();
    void displayProgress(int value);
    void handleComputationFinish();
    void cleanupResources();

signals:
    void startWorkerComputation(); // Signal pour démarrer la tâche du worker

private:
    Ui::MainWindow *ui;
    ComputationWorker *m_worker;   // L'objet qui effectue le travail
    QThread *m_workerThread;       // Le fil d'exécution dans lequel le worker opère
};

#endif // MAINWINDOW_H

mainwindow.cpp :

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QDebug>

MainWindow::MainWindow(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    m_worker = new ComputationWorker(); // IMPORTANT: Pas de parent pour le worker
    m_workerThread = new QThread(this); // Le QThread est enfant de la fenêtre

    // Déplace l'objet worker vers le fil d'exécution secondaire
    m_worker->moveToThread(m_workerThread);

    // Connexions des signaux et slots :
    // Démarrage de la computation dans le fil secondaire
    connect(this, &MainWindow::startWorkerComputation, m_worker, &ComputationWorker::startIterativeComputation);
    // Mise à jour de l'UI depuis le fil secondaire (via signal/slot)
    connect(m_worker, &ComputationWorker::progressUpdated, this, &MainWindow::displayProgress);
    // Traitement de la fin de computation
    connect(m_worker, &ComputationWorker::computationCompleted, this, &MainWindow::handleComputationFinish);
    // Nettoyage à la fermeture de la fenêtre
    connect(this, &QWidget::destroyed, this, &MainWindow::cleanupResources);

    qDebug() << "Fil principal (UI) ID : " << QThread::currentThreadId();
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::displayProgress(int value)
{
    ui->lcdNumber->display(value);
}

void MainWindow::handleComputationFinish()
{
    qDebug() << "Computation terminée dans le fil d'exécution principal.";
    ui->startButton->setEnabled(true);
}

void MainWindow::cleanupResources()
{
    // Demande l'arrêt du worker, puis quitte et attend le fil d'exécution
    m_worker->requestStop();
    m_workerThread->quit();
    m_workerThread->wait();
    delete m_worker; // Le worker n'ayant pas de parent, il doit être supprimé manuellement
    qDebug() << "Ressources du fil d'exécution nettoyées.";
}

void MainWindow::on_startComputationButton_clicked()
{
    if (!m_workerThread->isRunning()) {
        m_workerThread->start();           // Démarre le fil d'exécution
        emit startWorkerComputation();     // Lance la tâche dans le fil d'exécution
        ui->startButton->setEnabled(false);
    }
}

void MainWindow::on_stopComputationButton_clicked()
{
    if (m_workerThread->isRunning()) {
        m_worker->requestStop(); // Demande au worker d'arrêter sa tâche
        m_workerThread->quit();  // Signale au fil de quitter sa boucle d'événements
        m_workerThread->wait();  // Attend que le fil se termine proprement
        ui->startButton->setEnabled(true);
        qDebug() << "Fil d'exécution arrêté via le bouton.";
    }
}

L'exécution de cet exemple montre le compteur LCD s'incrémenter chaque seconde, reflétant le travail du fil secondaire. Le fait de cliquer sur "Arrêter" interrompt le processus grâce au flag m_stopRequested.

  1. Rendu Graphique Asynchrone

Dans cet exemple, nous démontrons comment déléguer la génération d'images à un fil d'exécution secondaire. Une fois l'image créée, elle est envoyée au fil principal via un signal, où elle est ensuite affichée dans la fenêtre. Cela évite de bloquer l'UI pendant la création d'images potentiellement complexes.

Générateur d'Images (Worker Object)

La classe ImageGenerator (héritant de QObject) contient la logique de dessin. Elle crée une QImage, y dessine une forme aléatoire, puis émet cette image via un signal.

imagegenerator.h :

#ifndef IMAGEGENERATOR_H
#define IMAGEGENERATOR_H

#include <QObject>
#include <QImage>
#include <QPainter>
#include <QRandomGenerator> // Pour des nombres aléatoires plus modernes

class ImageGenerator : public QObject
{
    Q_OBJECT
public:
    explicit ImageGenerator(QObject *parent = nullptr);

public slots:
    void generateRandomShape(); // Slot pour déclencher la génération d'image

signals:
    void imageGenerated(const QImage &image); // Signal émis avec l'image générée
};

#endif // IMAGEGENERATOR_H

imagegenerator.cpp :

#include "imagegenerator.h"
#include <QPoint>
#include <QPen>
#include <QBrush>
#include <QDebug>
#include <QThread>

ImageGenerator::ImageGenerator(QObject *parent) : QObject(parent)
{
    qsrand(QDateTime::currentMSecsSinceEpoch()); // Initialisation du générateur aléatoire
}

void ImageGenerator::generateRandomShape()
{
    qDebug() << "Génération d'image dans le fil : " << QThread::currentThreadId();
    QImage image(500, 500, QImage::Format_ARGB32);
    image.fill(Qt::white); // Fond blanc

    QPainter painter(&image);
    painter.setRenderHint(QPainter::Antialiasing);

    QPen pen;
    pen.setWidth(QRandomGenerator::global()->bounded(2, 10)); // Largeur aléatoire
    pen.setColor(QColor(QRandomGenerator::global()->bounded(256),
                        QRandomGenerator::global()->bounded(256),
                        QRandomGenerator::global()->bounded(256))); // Couleur aléatoire
    painter.setPen(pen);

    QBrush brush;
    brush.setStyle(Qt::SolidPattern);
    brush.setColor(QColor(QRandomGenerator::global()->bounded(256),
                          QRandomGenerator::global()->bounded(256),
                          QRandomGenerator::global()->bounded(256),
                          180)); // Couleur aléatoire semi-transparente
    painter.setBrush(brush);

    // Dessine une forme aléatoire
    int shapeType = QRandomGenerator::global()->bounded(3);
    switch (shapeType) {
        case 0: { // Rectangle
            int x = QRandomGenerator::global()->bounded(400);
            int y = QRandomGenerator::global()->bounded(400);
            painter.drawRect(x, y, 50 + QRandomGenerator::global()->bounded(100), 50 + QRandomGenerator::global()->bounded(100));
            break;
        }
        case 1: { // Ellipse
            int x = QRandomGenerator::global()->bounded(400);
            int y = QRandomGenerator::global()->bounded(400);
            painter.drawEllipse(x, y, 50 + QRandomGenerator::global()->bounded(100), 50 + QRandomGenerator::global()->bounded(100));
            break;
        }
        case 2: { // Polygone aléatoire
            QPoint points[5];
            for (int i = 0; i < 5; ++i) {
                points[i] = QPoint(QRandomGenerator::global()->bounded(500), QRandomGenerator::global()->bounded(500));
            }
            painter.drawPolygon(points, 5);
            break;
        }
    }
    
    QThread::msleep(100); // Petite pause pour simuler un travail
    emit imageGenerated(image); // Envoie l'image au fil principal
}

Fenêtre Principale

La MainWindow contient un bouton pour demander la génération d'une nouvelle image. Elle reçoit l'image générée et l'affiche via sa méthode paintEvent().

mainwindow.h :

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QWidget>
#include <QImage>
#include <QThread>
#include <QPaintEvent>
#include "imagegenerator.h"

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QWidget
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

protected:
    void paintEvent(QPaintEvent *event) override; // Méthode de dessin de la fenêtre

private slots:
    void receiveGeneratedImage(const QImage &img); // Slot pour recevoir l'image du worker
    void on_generateImageButton_clicked();         // Slot pour le clic sur le bouton
    void cleanupThreadOnClose();                   // Slot de nettoyage à la fermeture

private:
    Ui::MainWindow *ui;
    QImage m_displayedImage;    // L'image couramment affichée
    ImageGenerator *m_generator; // L'objet qui génère les images
    QThread *m_genThread;       // Le fil d'exécution pour le générateur
};

#endif // MAINWINDOW_H

mainwindow.cpp :

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QPainter>
#include <QDebug>

MainWindow::MainWindow(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    m_generator = new ImageGenerator(); // Pas de parent pour l'objet worker
    m_genThread = new QThread(this);     // QThread est enfant de la fenêtre

    m_generator->moveToThread(m_genThread); // Déplace le générateur vers le fil d'exécution

    m_genThread->start(); // Démarre le fil d'exécution (mais pas encore de tâche)

    // Connecte le bouton à la demande de génération d'image dans le fil secondaire
    connect(ui->generateImageButton, &QPushButton::clicked, m_generator, &ImageGenerator::generateRandomShape);
    // Connecte le signal d'image générée au slot de réception de la fenêtre principale
    connect(m_generator, &ImageGenerator::imageGenerated, this, &MainWindow::receiveGeneratedImage);
    // Nettoyage à la destruction de la fenêtre
    connect(this, &QWidget::destroyed, this, &MainWindow::cleanupThreadOnClose);

    // Initialisation de l'image affichée
    m_displayedImage = QImage(500, 500, QImage::Format_ARGB32);
    m_displayedImage.fill(Qt::lightGray); // Image par défaut
}

void MainWindow::receiveGeneratedImage(const QImage &img)
{
    m_displayedImage = img; // Met à jour l'image à afficher
    update();               // Déclenche un événement paintEvent pour redessiner la fenêtre
    qDebug() << "Image reçue et mise à jour de l'UI.";
}

void MainWindow::paintEvent(QPaintEvent *event)
{
    Q_UNUSED(event);
    QPainter painter(this);
    // Dessine l'image générée à une position spécifique
    painter.drawImage(QPoint(50, 50), m_displayedImage);
}

void MainWindow::cleanupThreadOnClose()
{
    // Quitte et attend la fin du fil d'exécution
    m_genThread->quit();
    m_genThread->wait();
    delete m_generator; // Supprime l'objet worker manuellement
    qDebug() << "Fil de génération d'image arrêté et nettoyé.";
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::on_generateImageButton_clicked()
{
    qDebug() << "Bouton 'Générer Image' cliqué sur le fil principal.";
    // Le signal est déjà connecté pour appeler generateRandomShape sur m_generator
    // dans le fil m_genThread.
}

Étiquettes: Qt C++ multithreading QThread moveToThread

Publié le 24 juillet à 03h27