Thread travailleur avec Qt en utilisant les signaux et les slots

Qt fournit des classes de threads indépendantes de la plateforme, une manière thread-safe de poster des événements et des connexions entre signaux et slots entre les threads. La programmation multithreadée s'avantage des machines à plusieurs processeurs et est aussi utile pour effectuer les opérations chronophages sans geler l'interface utilisateur d'une application. Sans multithreading, tout est fait dans le thread principal.

1 commentaire Donner une note à l'article (5)

Article lu   fois.

Les deux auteurs

Site personnel

Site personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

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 plateforme, une manière thread-safe de poster des événements et des connexions entre signaux et slots entre les threads. La programmtation multithreadée s'avantage des machines à plusieurs processeurs et est aussi utile pour effectuer les opérations chronophages sans geler l'interface utilisateur d'une application. Sans multithreading, tout est fait dans le thread principal.

Comme cela vient d'être mentionné, Qt supporte les connexions entre signaux et 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()

 
Sélectionnez
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ù 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 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.

Image non disponible
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
Sélectionnez
#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é). Avant de retourner, le constructeur attend que le thread soit prêt (c'est-à-dre que l'objet SorterWorker a été créé et que les signaux et slots sont 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 :

 
Sélectionnez
 QObject.connect: Cannot queue arguments of type 'QList<int>'
 (Make sure 'QList<int>' is registered using qRegisterMetaType().)
Implémentation de la classe Sorter
Sélectionnez
#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 doivent retourner des données, on utilise des signaux. Ne pas oublier de faire passer les signaux aux clients dans la classe Sorter.

 
Sélectionnez
#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.

 
Sélectionnez
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 :

 
Sélectionnez
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 QtConcurrent

Pour des cas simples, comme celui traité ici, QtConcurrent 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é :

 
Sélectionnez
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 et Jacques Thery pour leur relecture attentive !

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   


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

  

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.