Mise en place de traitements parallèles avec les threads

DéfinitionThread

La traduction informatique de thread signifie fil d'exécution. On appelle cela aussi un processus léger en français.

Dans la pratique, il s'agit d'un bloc de code au sein d'une application dont l'exécution est parallèle au thread principal que représente le plus souvent l'IHM de l'application.

Il est ainsi possible d'augmenter la vitesse d'exécution d'un programme en parallélisant les calculs.

Le processus est dit léger car il ne nécessite pas l'allocation de segment mémoire comme pour la création d'un processus classique. Sa création est donc plus rapide, mais il est dépendant du processus principale (thread principal). Si le processus principale cesse d'exister, les threads qui en dépendent aussi.

FondamentalThread

Un thread appartient au même espace mémoire que le processus principal.

Thread et mémoire
Thread et mémoire

ExempleUtilisation des threads

Un programme effectue l'acquisition de mesures sur des capteurs.

Chaque capteur est géré par un objet le représentant.

Chaque objet pourrait gérer un thread interrogeant cycliquement les capteurs et rendant disponibles les mesures, de manière à soulager le programme principale qui se concentrerait sur d'autres activités comme l'IHM.

C'est la stratégie qui a été choisie dans l'application finale.

L'objet QThtread

Qt fournit un objet QThread pour faciliter l'utilisation d'un thread.

Cet objet permet la gestion d'un thread à partir du thread (processus) principal.

Méthodes et attributs remarquables d'un objet QThread

run()

Point d'entrée du thread. C'est dans cette méthode que se trouvent les instructions du thread.

start()

Lance l'exécution du thread. La méthode run() est invoquée automatiquement.

Il existe plusieurs moyens de créer un thread. Pour les comprendre, il faut savoir ce qu'est l'héritage en C++. C'est un concept de programmation objets qu'on rencontre dans tous les langages objets.

ComplémentL'héritage en C++

Une classe peut hériter d'une autre.

Analogie avec un humain qui hérite de son parent (on ne parlera pas de l’État qui prend sa part (- ; ).

La classe de base est la classe dont on hérite.

La classe qui hérite est la classe dérivée.

En cas d'héritage :

  • Toutes les méthodes et attributs publiques (public :) de la classe de base sont publiques dans la classe dérivée. On y accède comme s'ils étaient déclarés dans la classe dérivée.

  • Toutes les méthodes et attributs privés (private:) de la classe de base sont inaccessibles dans la classe dérivée. Le seul moyen est d'utiliser les accesseurs s'ils existent.

  • Toutes les méthodes et attributs protégés (protected:) de la classe de base sont accessibles dans la classe dérivée comme des membres privés.

Symbolisme UML de l'héritage
Symbolisme UML de l'héritage

Création d'un thread par héritage

Une solution consiste à créer une classe qui hérite de QThread et à surcharger la méthode run(), c'est à dire redéfinir son contenu car par défaut, elle ne fait rien.

1
class WorkerThread : public QThread
2
{
3
    Q_OBJECT
4
    void run() override {
5
        QString result;
6
        /* ... here is the expensive or blocking operation ... */
7
        emit resultReady(result);
8
    }
9
signals:
10
    void resultReady(const QString &s);
11
};
12
13
void MyObject::startWorkInAThread()
14
{
15
    WorkerThread *workerThread = new WorkerThread(this);
16
    connect(workerThread, &WorkerThread::resultReady, this, &MyObject::handleResults);
17
    connect(workerThread, &WorkerThread::finished, workerThread, &QObject::deleteLater);
18
    workerThread->start();
19
}

Ici, la méthode run() est surchargée pour personnaliser l'exécution du thread.

Notez qu'un thread peut avoir besoin de signifier qu'il a terminé son travail. Le code ci-dessus permet l'émission d'un signal resultReady(result). L'IHM peut ainsi récupérer le résultat.

RemarqueQuel est le thread ?

Seule la méthode run() fera partie du thread nouvellement créé.

L'instanciation d'un objet WorkerThread se faisant dans le thread principale, il appartient donc à ce thread et dispose de la même boucle de messages.

De manière pratique avec qt-creator

Pour créer une nouvelle classe qui hérite de QThread, utilisez l'assistant de création de classe :

  • Cliquez bouton de droite sur le nom du projet dans le gestionnaire de projet.

  • Ajoutez une nouvelle classe C++ et cliquez validez la fenêtre.

Ajout d'une nouvelle classe
Ajout d'une nouvelle classe

Remplissez la fenêtre suivante selon le modèle ci-dessous :

Paramètres de la nouvelle classe
Paramètres de la nouvelle classe

Après validation, votre classe est visible dans le gestionnaire de projet.

Pour l'utiliser, n'oubliez pas de placer la ligne suivante dans le fichier d'interface de classe qui l'utilisera :

1
#include "cboutonpoussoir.h"

Création d'un objet géré par la classe QThread

Autre méthode : Imaginons la création d'une classe CWork (par exemple) dont un objet sera exécuté en tant que thread.

Pour cela, il faut que la classe CWork hérite de QObject, la classe de base de tous les objets Qt.

Cela permet d'hériter de la méthode moveToThread(). Cette méthode permet de sous traiter son exécution à un objet QThread.

Illustration de l'utilisation de la méthode moveToThread() :

1
QThread workerThread;
2
CWork *worker = new CWork;
3
worker->moveToThread(&workerThread);
4
workerThread.start();

Dans cet exemple, le lancement du thread exécutera l'objet worker.

Voici un exemple plus complet :

1
class CWork : public QObject
2
{
3
    Q_OBJECT
4
5
public slots:
6
    void doWork(const QString &parameter) {
7
        QString result;
8
        /* ... here is the expensive or blocking operation ... */
9
        emit resultReady(result);
10
    }
11
12
signals:
13
    void resultReady(const QString &result);
14
};
15
16
class Controller : public QObject
17
{
18
    Q_OBJECT
19
    QThread workerThread;
20
public:
21
    Controller() {
22
        CWork *worker = new CWork;
23
        worker->moveToThread(&workerThread);
24
        connect(&workerThread, SIGNAL(&QThread::finished()), worker, SLOT(&QObject::deleteLater()));
25
        connect(this, SIGNAL(&Controller::operate()), worker, SLOT(&Worker::doWork()));
26
        connect(worker, SIGNAL(&Worker::resultReady()), this, SLOT(&Controller::handleResults()));
27
        workerThread.start();
28
    }
29
    ~Controller() {
30
        workerThread.quit();
31
        workerThread.wait();
32
    }
33
public slots:
34
    void handleResults(const QString &);
35
signals:
36
    void operate(const QString &);
37
};

AttentionInstance de thread

It is important to remember that a QThread instance lives in the old thread that instantiated it, not in the new thread that calls run(). This means that all of QThread's queued slots will execute in the old thread. Thus, a developer who wishes to invoke slots in the new thread must use the worker-object approach; new slots should not be implemented directly into a subclassed QThread.

When subclassing QThread, keep in mind that the constructor executes in the old thread while run() executes in the new thread. If a member variable is accessed from both functions, then the variable is accessed from two different threads. Check that it is safe to do so.

ComplémentSignaux/slots dans un thread

Diagramme de classe d'exemple
Diagramme de classe d'exemple

La classe QThread représente une classe de gestion d'un thread, pas le thread lui-même.

Cela signifie que l'instance de cet objet appartient au thread "père", la plupart du temps, le thread principal.

De ce fait tous les signaux échangés avec cette instance seront placés dans la file des messages du thread principal et non du nouveau thread créé.

Cela peut poser un problème, notamment si dans un slot, vous attendez un signal ou qu'une donnée membre se mette à jour. Cette donnée sera mise à jour ou le signal traité qu'après sortie du slot.

Examinez le diagramme de classes ci-contre :

Dans la classe CIHM... on instancie l'objet CAppareil (m_app).

Toujours dans la classe CIHM... on lance le thread : m_app->start() ;

Seule la méthode CAppareil::run() est exécutée dans un thread différent. Les autres méthodes de la classe sont dans le thread initial.

Imaginons que dans la méthode CAppareil::run(), on boucle pour attendre d'avoir reçu un certain nombre de caractères par la voie série avant de lire l'ensemble.

Voilà illustré le problème : étant dans la méthode run() qui boucle, l'objet m_app appartient au thread principal.

Il faudra attendre la sortie de la méthode run() pour que la mise à jour des attributs de classe soit effective.

Pour des explications plus détaillées, voici le lien :

Oui je sais, c'est pas évident.

C'est pas non plus essentiel pour la suite.

Il faut pratiquer les threads et rencontrer les problèmes pour commencer à comprendre.