Utiliser un thread travailleur avec Qt en employant les signaux et les slots

Christophe Dumez

Traducteur

Thibaut Cuvelier

Qt fournit des classes de threads indépendantes de la plate-forme, une manière thread-safe de poster des événements et des connexions de signaux à slots entre les threads. La programmation multithreadée est un avantage des machines à plusieurs processeurs ; elle est aussi utile pour effectuer les opérations chronophages sans geler l'interface utilisateur d'une application. Sans multithreading, les opérations sont réalisées séquentiellement, sans parallélisme possible.

Commentez

- Titre : Utiliser un thread travailleur avec Qt en employant les signaux et les slots

- Auteur : Christophe Dumez

- Traducteur : Thibaut Cuvelier

- Parution : 5 février 2012

- Mise à jour : 5 janvier 2019

- Licence : Copyright ® 2012 Christophe Dumez. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. L'article original

- II. Connexions entre signaux et slots entre les threads

- III. Un exemple simple

- IV. La classe SorterWorker

- V. Utiliser le thread travailleur

- VI. Débogage

- VII. L'alternative Qt Concurrent

- VIII. Remerciements

I - L'article original

Cet article est une adaptation en langue française de Worker Thread in Qt using Signals & Slots.

II - Connexions entre signaux et slots entre les threads

Qt fournit des classes de threads indépendantes de la plate-forme, une manière thread-safe de poster des événements et des connexions de signaux à slots entre les threads. La programmation multithreadée est un avantage des machines à plusieurs processeurs elle est aussi utile pour effectuer les opérations chronophages sans geler l'interface utilisateur d'une application. Sans multithreading, les opérations sont réalisées séquentiellement, sans parallélisme possible.

Comme cela vient d'être mentionné, Qt supporte les connexions de signaux à slots entre les threads. Cela fournit une manière intéressante de passer des données entre les threads.

Voici le prototype de la méthode QObject::connect() :

bool QObject::connect(const QObject *sender, const
char *signal, const QObject *receiver, const char
*method, Qt::ConnectionType type = Qt::AutoConnection);

Le dernier paramètre est le type de connexion et il est important de le comprendre. La valeur par défaut est Qt::AutoConnection, ce qui signifie que, si le signal est émis d'un thread différent de l'objet le recevant, le signal est mis dans la queue de gestion d'événements (1), un comportement semblable à Qt::QueuedConnection. Sinon, le slot est invoqué directement, comme Qt::DirectConnection. Le type de connexion est déterminé quand le signal est émis.

Dans le cas discuté ici, on se montrera particulièrement intéressé par Qt::QueuedConnection, parce que :

- il est thread-safe d'utiliser une telle connexion entre deux threads différents (au contraire de la connexion directe) ;

- le slot est exécuté dans le thread de l'objet le recevant. Cela signifie que l'on peut émettre un signal du thread principal et le connecter à un slot dans un thread travailleur. Le traitement est alors effectué dans ce dernier.

III - Un exemple simple

On considère un exemple simple où l’on souhaite trier un vecteur d'entiers dans un thread séparé. Par conséquent, on devrait avoir deux threads : le principal pour l'interface et le thread travailleur qui s'occupe du tri.

Comme montré dans la figure ci-dessous, on doit avoir deux objets : un qui vit dans le thread principal (Sorter) et un dans le thread travailleur (SorterWorker).

On dit d'une instance de QObject qu'elle vit dans le thread dans lequel elle a été créée. Les événements pour cet objet sont gérés par la boucle événementielle du thread.

Représentation visuelle des threads.

Pour la classe Sorter, puisqu'on veut créer un nouveau thread, on devra hériter de QThread (héritant elle-même de QObject). Elle fournira une méthode sortAsync(QVector) qui sera appelée par les clients pour trier un vecteur de manière asynchrone. Puisque l'opération est asynchrone, on doit aussi définir un signal vectorSorted(QVector) pour notifier le client de la fin du tri.

En-tête de la classe Sorter

#include <QThread>
#include <QVector>

/*! Classe pour effectuer le travail (tri) dans un thread travailleur. */
class Sorter : public QThread {
Q_OBJECT

public:
/*! Constructeur par défaut */
Sorter(QObject *parent = 0);

/*! Trie de manière asyncrhone un vecteur dans le thread travailleur. */
void sortAsync(const QVector<int> &list);

signals:
/*! Signal interne utilisé pour communiquer avec le thread travailleur. */
void sortingRequested(const QVector<int> &list);
/*! Signal émis quand le vecteur est trié. */
void vectorSorted(const QVector<int> &list);

protected:
void run();

private:
/*!
* Booléen indiquant si le thread travailleur est prêt à traiter les
* demandes.
*/
bool m_ready;
};

Voici quelques détails sur l'implémentation de cette classe.

Dans le constructeur, on démarre le thread travailleur (le code dans run() sera exécuté). Enfin, le constructeur attend que le thread soit prêt (c'est-à-dire que l'objet SorterWorker ait été créé et que les signaux et slots soient connectés) pour s'assurer que le client ne puisse effectuer de requête avant que le thread ne soit prêt à les exécuter.

Dans la méthode run(), on crée un objet SorterWorker. Il est important que cet objet soit créé dans la méthode run() et non dans le constructeur de SorterWorker, de telle sorte qu'il vive dans le thread travailleur (et non dans le thread principal). On utilise un signal interne sortingRequested(QVector) pour rediriger les requêtes de tri au SorterWorker dans le thread travailleur. Puisque le signal est émis dans le thread principal et que l'objet le recevant (SorterWorker) vit dans le thread travailleur, une connexion en queue va être utilisée. Par conséquent, le slot doSort(QVector) sera exécuté dans le thread travailleur.

On note aussi l'utilisation de QMetaType::qRegisterMetaType() avant de connecter les signaux et les slots. Quand un signal est mis dans la queue, les paramètres doivent être d'un type connu par le système de métaobjets de Qt, parce que Qt a besoin de copier les arguments pour les stocker dans un événement en coulisses. En cas d'oubli, on aura l'erreur suivante :

QObject.connect: Cannot queue arguments of type 'QList<int>'
(Make sure 'QList<int>' is registered using qRegisterMetaType().)

Implémentation de la classe Sorter

#include <QMetaType>
#include <QDebug>
#include "sorter.h"
#include "sorter_p.h"

Sorter::Sorter(QObject *parent):
QThread(parent), m_ready(false) {
qDebug() << Q_FUNC_INFO << QThread::currentThreadId(); // Le thread principal
// On démarre le thread travailleur.
start();
// On attend que le thread soit prêt.
while(!m_ready) msleep(50);
}

void Sorter::sortAsync(const QVector<int> &v)
{
qDebug() << Q_FUNC_INFO << QThread::currentThreadId(); // Le thread principal
emit sortingRequested(v);
}

void Sorter::run()
{
qDebug() << Q_FUNC_INFO << QThread::currentThreadId(); // Le thread travailleur
// Ce QObject vit dans le thread travailleur
SorterWorker worker; // NE PAS définir le pointeur this comme parent.
// On doit enregistrer QList<int>, parce qu'il n'est pas connu du système de
// métaobjets de Qt.
qRegisterMetaType< QVector<int> >("QVector<int>");
// On passe les requêtes de tri au thread travailleur.
connect(this, SIGNAL(sortingRequested(QVector<int>)),
&worker, SLOT(doSort(QVector<int>))/*, Qt::QueuedConnection*/);
// On transmet le signal aux clients.
connect(&worker, SIGNAL(vectorSorted(QVector<int>)), this,
SIGNAL(vectorSorted(QVector<int>))/*, Qt::QueuedConnection*/);
// On marque le thread travailleur comme prêt.
m_ready = true;
// On lance la boucle d'événements (nécessaire pour gérer les signaux).
exec();
}

IV - La classe SorterWorker

L'implémentation de la classe SorterWorker est rapide. On doit juste s'assurer qu'elle hérite de QObject (ne pas oublier la macro Q_OBJECT pour que les signaux et slots soient utilisables). Les méthodes telles que doSort(QVector) doivent être définies comme des slots publics. Au cas où ces méthodes devraient retourner des données, on utilise des signaux. Ne pas oublier de faire passer les signaux aux clients dans la classe Sorter.

#include <QObject>
#include <QVector>
#include <QThread>
#include <QDebug>

/*! Classe effectuant le vrai travail (tri). */
class SorterWorker: public QObject {
Q_OBJECT

signals:
/*! Signal émis une fois que le vecteur est trié. */
void vectorSorted(const QVector<int> &v);

public slots:
/*! Méthode s'occupant du tri. */
void doSort(const QVector<int> &v) {
qDebug() << Q_FUNC_INFO << QThread::currentThreadId(); // Thread travailleur
QVector<int> v_sorted = v;
qSort(v_sorted);
emit vectorSorted(v_sorted);
}
};

V - Utiliser le thread travailleur

Pour utiliser le thread travailleur, on doit simplement créer un objet Sorter et s'assurer de connecter son signal vectorSorted(QVector) à un slot local pour récupérer le résultat. On peut alors appeler la méthode sortAsync(QVector) pour demander au thread travailleur de trier le vecteur.

Sorter t;
connect(&t, SIGNAL(vectorSorted(QVector<int>)),
SLOT(handleVectorSorted(QVector<int>)));
t.sortAsync(QVector<int>() << 1 << 3 << 2);

VI - Débogage

Pour s'assurer que les fonctions sont exécutées dans le bon thread, on peut utiliser l'instruction suivante :

qDebug() << Q_FUNC_INFO << QThread::currentThreadId();

QThread::currentThreadId() est une fonction statique qui retourne l'identifiant du thread d'exécution courant.

VII - L'alternative Qt Concurrent

Pour des cas simples, comme celui traité ici, Qt Concurrent est une bonne alternative qui nécessitera moins de code.

On peut utiliser la fonction suivante pour lancer une fonction dans un thread séparé :

QFuture<T> QtConcurrent::run(Function func, ...);

Cela exécute la fonction func dans un thread séparé et retourne un objet QFuture. On peut vérifier si la fonction a fini son travail en exécutant la méthode QFuture::isFinished(). De manière alternative, on peut utiliser un objet QFutureWatcher pour recevoir un signal à ce moment.

VIII - Remerciements

Merci à Guillaume Belz, Jacques Thery et escartefigue pour leur relecture attentive !

(1)Chaque thread possède sa propre queue de gestion d'événements.