Threads, événements et QObject

Le but de ce document n'est pas de vous enseigner la manière d'utiliser les threads, de faire des verrouillages appropriés, d'exploiter le parallélisme ou encore d'écrire des programmes extensibles : il y a de nombreux bons livres sur ce sujet ; par exemple, jetez un coup d'œil sur la liste des lectures recommandées par la documentation. Ce petit article est plutôt voué à être un guide pour introduire les utilisateurs dans le monde des threads de Qt 4, afin d'éviter les écueils les plus récurrents et de les aider à développer des codes à la fois plus robustes et mieux structurés.

4 commentaires Donner une note à l'article (5)

Article lu   fois.

Les deux auteur et traducteur

Traducteur :

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. L'article original

Le Qt Developer Network est un réseau de développeurs utilisant Qt afin de partager leur savoir sur ce framework. Vous pouvez le consulter en anglais.

Nokia, Qt, Qt Quarterly et leurs logos sont des marques déposées de Nokia Corporation en Finlande et/ou dans les autres pays. Les autres marques déposées sont détenues par leurs propriétaires respectifs.

Cet article est la traduction de Threads, Events and QObjects.

II. Introduction

« Vous vous y prenez mal. » - Bradley T. Hughes

L'un des sujets les plus populaires du canal IRC de Qt sur Freenode est l'utilisation des threads : beaucoup de monde rejoint le canal et demande comment résoudre leur problème avec l'exécution d'un code dans un thread différent.

Neuf fois sur dix, une rapide inspection de leur code montre que le plus grand problème réside, en premier lieu, dans la manière d'utiliser les threads, ce qui les fait tomber dans l'un des pièges sans fond de la programmation parallèle.

La facilité de créer et exécuter des threads avec Qt, combinée avec quelques lacunes quant aux styles de programmation (en particulier, la programmation réseau asynchrone, combinée avec l'architecture des signaux et slots fournie par Qt) ou avec des habitudes développées lors de l'utilisation d'autres toolkits ou d'autres langages, mènent généralement les utilisateurs à se ramasser des pelles. De surcroît, le support des threads par Qt est une épée à double tranchant : tandis que cela rend le multithreading très simple, cela ajoute un certain nombre de fonctionnalités (surtout lors des interactions avec les objets héritant de la classe QObject) auxquelles on doit faire attention.

Le but de ce document n'est pas de vous enseigner la manière d'utiliser les threads, de faire des verrouillages appropriés, d'exploiter le parallélisme ou encore d'écrire des programmes extensibles : il y a de nombreux bons livres sur ce sujet ; par exemple, jetez un coup d'œil sur la liste des lectures recommandées par la documentation. Ce petit article est plutôt voué à être un guide pour introduire les utilisateurs dans le monde des threads de Qt 4, afin d'éviter les écueils les plus récurrents et de les aider à développer des codes à la fois plus robustes et mieux structurés.

II-A. Prérequis

« Considérez ça de cette manière : les threads sont comme le sel, pas comme les pâtes. Vous aimez le sel, j'aime le sel, nous aimons tous le sel… mais nous mangeons plus de pâtes. »- Larry McVoy.

Ceci n'étant une introduction générale à la programmation (avec les threads), on attend de vous que vous maîtrisiez les notions ci-dessous :

  • bases de la programmation en C++, bien que la plupart des suggestions s'appliquent aussi bien aux autres langages ;
  • bases de la programmation avec Qt, à savoir QObject, signaux, slots et traitement des événements ;
  • connaissance de la nature des threads et des relations entre les threads, les processus et le système d'exploitation ;
  • aptitude à démarrer et arrêter un thread, ainsi qu'à attendre cet arrêt de terminer, sur, au moins, un système d'exploitation majeur ;
  • aptitude à utiliser les mutex, les sémaphores et les conditions d'attente pour créer des fonctions, des structures de données et des classes, réentrantes ou sécurisées pour les threads.

Dans ce document, les conventions de nommage de Qt seront suivies, lesquelles sont :

  • la classe A est réentrante s'il est sûr d'utiliser ces instances depuis plus d'un thread, en admettant que plus d'un thread accède simultanément à la même instance. Une fonction est réentrante s'il est sûr de l'appeler depuis plus d'un thread, en admettant que chaque appel fait référence à des données uniques. En d'autres termes, cela signifie que les utilisateurs de cette classe ou respectivement de cette fonction doivent sérialiser tous les accès aux instances ou aux données partagées, impliquant la mise en place de mécanismes externes de verrouillage ;
  • la classe A est sécurisée pour les threads s'il est sûr d'utiliser simultanément ces instances depuis plus d'un thread à la fois. Une fonction est sécurisée pour les threads s'il est sûr de l'appeler simultanément ces instances depuis plus d'un thread à la fois, même si les appels font référence à des données partagées.

III. Événements et boucle événementielle

Qt étant basé sur les événements, ces derniers et leur transfert jouent un rôle prépondérant dans l'architecture de Qt. Cet article ne s'étendra pas beaucoup sur ce sujet ; on se concentrera plutôt sur les concepts-clés liés aux threads. Pour de plus amples informations quant au système de gestion des événements de Qt, veuillez lire la documentation sur le système événementiel de Qt et cet article de Qt Quaterly sur les événements.

Qt traite un événement comme un objet qui représente quelque chose d'intéressant qui a eu lieu ; la principale différence entre un événement et un signal est que les événements s'adressent à un objet spécifique de l'application, qui décidera de ce qu'il devra faire avec cet événement, tandis les signaux sont émis « dans la nature ». Du point de vue du code, tous les événements sont des instances de quelque classe héritant de QEvent, toutes les classes héritant de QObject peuvent redéfinir la méthode virtuelle QObject::event() afin de traiter les événements qui sont adressés à leurs instances.

Les événements peuvent être générés aussi bien depuis l'application elle-même que depuis son environnement. Par exemple :

  • les objets des classes QKeyEvent et QMouseEvent représentent certaines des interactions avec le clavier et la souris et elles sont gérées par le gestionnaire de fenêtres ;
  • les objets de la classe QTimerEvent sont envoyés à un QObject quand l'un de ses minuteurs se déclenche et sont (habituellement) gérés par le système d'exploitation ;
  • les objets de la classe QChildEvent sont envoyés à un QObject quand on lui ajoute ou retire un enfant et proviennent de votre application Qt elle-même.

Une chose importante à savoir sur les événements : ils ne sont pas transmis dès qu'ils sont générés ; au contraire, ils sont d'abord stockés dans une file et envoyés un peu plus tard. Le distributeur, lors de chaque passage dans la boucle, vérifie la file d'attente et envoie les événements qui y sont stockés aux objets auxquels ils sont destinés ; c'est pourquoi on appelle cette boucle une boucle événementielle. Voici grosso modo à quoi ressemble la boucle événementielle (cf. l'article de Qt Quaterly) :

 
Sélectionnez
while (is_active)
{
	while (!event_queue_is_empty)
		dispatch_next_event();
	
	wait_for_more_events();
}

La fonction wait_for_more_events() bloque (c'est-à-dire que l'attente n'est pas active) jusqu'à ce qu'un événement soit généré. Si on y réfléchit, tout ce qui peut générer des événements à ce moment-là n'est autre qu'une source externe (la distribution de tous les événements internes est maintenant effectuée et il n'y avait plus événements en attente dans la file événementielle à transmettre). Ainsi, la boucle événementielle peut être réveillée par :

  • l'activité du gestionnaire de fenêtres (pression d'une touche du clavier ou de la souris, interaction avec les fenêtres, etc.) ;
  • l'activité des sockets (il y a des données disponibles à la lecture, il y a une nouvelle connexion entrante, on peut écrire dans un socket sans bloquer quoi que ce soit, etc.) ;
  • des minuteurs (c'est-à-dire que l'un d'entre eux s'est déclenché) ;
  • des événements postés par d'autres threads.

Dans un système de type UNIX, l'activité du gestionnaire de fenêtres, X11, est notifiée aux applications via des sockets (domaine Unix ou TCP/IP), vu que les clients les utilisent pour communiquer avec le serveur X. Si l'on décide d'implémenter l'émission d'événements interthreads avec un couple interne de sockets, ce qui en résultera pourra être réveillé par l'activité des sockets et des minuteurs ; c'est exactement ce que fait l'appel système de sélection : il surveille un ensemble de descripteurs d'activité et expire (le délai étant configurable) s'il n'y a aucune activité durant un certain temps. Tout ce que Qt a besoin de faire est de convertir ce que la sélection renvoie dans un objet d'une classe fille adéquate de QEvent et envoie dans la file d'attente événementielle.

III-A. Que requiert une boucle événementielle ?

Ce qui suit n'est pas une liste exhaustive mais, grâce à cette vue d'ensemble, vous devriez être à même de deviner quelle classe requiert une boucle événementielle.

  • L'interaction et le dessin des widgets : QWidget::paintEvent() est appelé à la réception d'objets de la classe QPaintEvent, qui sont générés aussi bien par l'appel de la fonction QWidget::update() (c'est-à-dire de manière interne) que par le gestionnaire de fenêtres (par exemple, parce qu'une fenêtre réduite ou cachée apparaît). Il en va de même pour tout type d'interaction (clavier, souris, etc.) : l'événement correspondant requiert une boucle événementielle pour être envoyé aux objets concernés.
  • Les minuteurs : ils sont déclenchés lorsqu'une sélection ou un appel similaire arrive à expiration ; en conséquence, on laisse Qt effectuer ces appels par un retour vers la boucle événementielle.
  • La programmation réseau : toutes les classes Qt de bas niveau pour la programmation réseau (QTcpSocket, QUdpSocket, QTcpServer, etc.) sont, par conception, asynchrones. Quand on appelle la fonction read(), ils ne retournent que les données déjà disponibles, tandis que, lorsqu'on appelle write(), ils programment l'écriture pour plus tard ; ce n'est qu'au retour à la boucle événementielle que la lecture (respectivement l'écriture) a lieu. Ces classes offrent des méthodes synchrones, comme la famille de méthodes waitFor*, mais leur usage est déconseillé car elles bloquent la boucle événementielle à cause de l'attente ; les classes de haut niveau, comme QNetworkAccessManager, n'offrent tout simplement aucune interface applicative synchrone et nécessitent une boucle événementielle.

III-B. Bloquer la boucle événementielle

Avant d'expliquer pourquoi on ne doit jamais bloquer la boucle événementielle, on tente de comprendre ce que signifie ce blocage. On suppose que l'on a un bouton qui émet un signal quand il est cliqué ; connecté à ce signal, un slot d'une instance de la classe Worker qui fait tout un tas de traitements. Une fois le bouton cliqué, le contenu de la file ressemble à ceci :

 
Sélectionnez
main(int, char **)
QApplication::exec()
[…]
QWidget::event(QEvent *)
Button::mousePressEvent(QMouseEvent *)
Button::clicked()
[…]
Worker::doWork()

Dans la fonction main(), on lance la boucle événementielle, comme à l'accoutumée, par l'appel de QApplication::exec() (ligne 2). Le gestionnaire de fenêtres envoie le clic de la souris, qui est détecté par le noyau de Qt, converti en une instance de QMouseEvent et envoyé à la méthode event(QEvent*) du widget (ligne 4) par la fonction QApplication::notify() - qui n'est pas montrée ici. Comme la classe Button ne réimplémente pas la méthode event(), l'implémentation de base, fournie par la classe QWidget, est appelée. QWidget::event() détecte que l'événement est en fait un clic de souris et appelle le gestionnaire d'événements spécialisé, c'est-à-dire Button::mousePressEvent(QMouseEvent*) (ligne 5). Cette méthode a été réimplémentée pour qu'elle émette le signal Button::clicked() (ligne 6), ce qui appelle le slot Worker::doWork de l'instance concernée (ligne 7).

Tant que cette instance s'affaire, que fait la boucle événementielle ? Vous l'avez sûrement deviné : rien ! La boucle, après avoir envoyé l'événement qui représente le clic de souris, se retrouve bloquée à attendre que le gestionnaire d'événement ait fini son traitement. On a donc réussi à bloquer la boucle événementielle, ce qui veut dire que plus aucun événement n'est envoyé, et ce, jusqu'à ce que l'on quitte le slot doWork(), retourne à la file et la boucle événementielle et puisse de nouveau traiter les événements en attente.

L'envoi des événements est alors bloqué : les widgets ne s'actualiseront pas (les objets QPaintEvent resteront dans la file) ; pour la même raison, plus aucune interaction avec les widgets n'est possible ; les minuteurs ne se déclencheront pas ; les communications réseau seront ralenties, voire arrêtées. De surcroît, de nombreux gestionnaires de fenêtres détecteront que l'application ne traite plus les événements et informeront l'utilisateur qu'elle ne répond plus. C'est pourquoi il est important de vite réagir aux événements et de retourner, dès que possible, à la boucle événementielle.

III-C. Forcer l'envoi des événements

Alors que fait-on si on a une tâche longue à exécuter et qu'on ne veut pas bloquer la boucle événementielle ? Une réponse possible : effectuer la tâche dans un autre thread ; dans les sections suivantes, on verra comment s'y prendre. On a aussi la possibilité de forcer manuellement l'exécution de la boucle événementielle par l'appel (répété) de la fonction QCoreApplication::processEvents() à l'intérieur de la tâche bloquante. Cette fonction traitera tous les événements présents dans la file et retournera à l'instance qui l'a appelée.

Une autre possibilité, qu'on peut utiliser pour forcer l'application à revenir dans la boucle événementielle, est la classe QEventLoop. En appelant QEventLoop::exec(), on revient dans la boucle événementielle et on peut connecter des signaux au slot QEventLoop::quit() pour en sortir. Par exemple :

 
Sélectionnez
QNetworkAccessManager qnam;
QNetworkReply *reply = qnam.get(QNetworkRequest(QUrl(...))); 
QEventLoop loop;
QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
loop.exec();
/* reply a fini son travail, on peut l'utiliser */

La classe QNetworkReply n'offre pas d'interface applicative bloquante et requiert une boucle événementielle pour s'exécuter. On entre dans une instance locale de la classe QEventLoop et, lorsque la réponse est terminée, on quitte la boucle événementielle locale.

Il faut faire très attention quand on revient dans la boucle événementielle « par d'autres chemins » : cela peut mener à des récursions non désirées ! Reprenons donc l'exemple avec la classe Button. Si la fonction QCoreApplication::processEvents() est appelée à l'intérieur du slot Button::doWork() et que l'utilisateur clique de nouveau sur le bouton, la méthode doWork() sera de nouveau appelée :

 
Sélectionnez
main(int, char **)
QApplication::exec()
[…]
QWidget::event(QEvent *)
Button::mousePressEvent(QMouseEvent *)
Button::clicked()
[…]
Worker::doWork() // primo, appel interne
QCoreApplication::processEvents() // on envoie manuellement les événements et…
[…]
QWidget::event(QEvent * ) // un autre clic de souris est envoyé à l'instance de Button… Button::mousePressEvent(QMouseEvent *)
Button::clicked() // ce qui émet à nouveau le signal clicked()…
[…]
Worker::doWork() // Oh ! La belle récursion !

On peut aisément contourner ce problème en passant l'argument QEventLoop ::ExcludeUserInputEvents à la méthode QCoreApplication::processEvents() : ainsi la boucle événementielle n'enverra aucun événement lié à une interaction avec l'utilisateur ; ces derniers resteront simplement dans la file.

Heureusement, ceci ne s'applique aux événements de suppression (ceux qui sont envoyés dans la file via la fonction QObject::deleteLater()). En fait, ils sont gérés d'une manière spéciale par Qt et ne sont traités que si la boucle événementielle exécutée a un degré d'« imbrication » moindre que celle où la fonction deleteLater() a été appelée.

 
Sélectionnez
QObject *object = new QObject;
object->deleteLater();
QDialog dialog;
dialog.exec();

Le code ci-dessus ne générera pas de pointeur fou, car la boucle événementielle lancée par QDialog::exec() est plus imbriquée que l'appel de QObject::deleteLater(). Ceci s'applique aussi aux boucles événementielles locales instanciées avec QEventLoop. La seule exception notable à cette règle qu'on a pu rencontrer (dans Qt 4.7.3) est que, si cette dernière fonction est appelée alors qu'aucune boucle événementielle n'est en train de tourner, alors la première boucle lancée détectera cet événement et supprimera l'objet. C'est plutôt raisonnable, vu que Qt sait qu'aucune boucle événementielle « externe » n'effectuera la suppression et supprime donc immédiatement l'objet.

IV. Classes de Qt pour les threads

« Un ordinateur est un automate fini. Les threads sont prévus pour les personnes qui ne savent pas programmer d'automate fini. » - Alan Cox

Qt supporte les threads depuis plusieurs années (Qt 2.2, publié le 22 septembre 2000, a introduit la classe QThread) et, depuis la version 4.0, le support des threads est disponible par défaut sur toutes les plateformes supportées, bien que cette fonctionnalité puisse être désactivée (pour plus de détails, voir la documentation officielle). Aujourd'hui, Qt offre plusieurs classes pour manipuler les threads ; commençons donc par une vue d'ensemble de ces classes.

IV-A. QThread

QThread est la classe centrale de bas niveau pour le support des threads de la bibliothèque logicielle. Une instance de la classe QThread représente un thread en cours d'exécution. Reflet du caractère multiplateforme de Qt, QThread dissimule tout le code spécifique requis pour l'utilisation des threads sur les différents systèmes d'exploitation. Afin d'utiliser un objet QThread et exécuter du code dans un thread, il faut créer une classe fille et réimplémenter la méthode QThread::run() :

 
Sélectionnez
class Thread : public QThread
{
protected:
	void run(){
		// L'implémentation du thread
	}
};

Ensuite, le code ci-dessous peut être utilisé pour démarrer le nouveau thread.

 
Sélectionnez
Thread* t = new Thread;
t->start(); // start() et pas run() !

Depuis Qt 4.4, QThread n'est plus une classe abstraite ; désormais, la méthode virtuelle QThread::run() appelle simplement QThread::exec(), ce qui lance la boucle événementielle du thread (ce point sera traité plus en détail dans un autre paragraphe).

IV-B. QRunnable et QThreadPool

QRunnable est une classe abstraite légère qui peut être utilisée pour lancer une tâche dans un autre thread sans arrière-pensée. Pour ce faire, il suffit, en tout et pour tout, de créer une classe héritant de QRunnable et d'implémenter sa méthode virtuelle pure run() :

 
Sélectionnez
class Task : public QRunnable
{
public:
	void run(){
		// Le code à lancer
	}
};

Pour exécuter un objet QRunnable, on utilise la classe QThreadPool, qui gère un ensemble de threads. La méthode QThreadPool::start(runnable) place un objet QRunnable dans la file d'attente d'un QThreadPool ; aussitôt qu'un thread devient disponible, l'objet QRunnable est pris et lancé dans ce thread-là. Toutes les applications programmées avec Qt possèdent un ensemble global de threads qui peut être récupéré par la méthode QThreadPool::globalInstance(), mais on peut toujours créer une instance privée de la classe QThreadPool et le gérer de manière explicite.

Comme QRunnable n'hérite pas de QObject, cette classe n'a aucun moyen intégré pour communiquer quoi que ce soit à d'autres composants ; on doit alors coder ceci à la main, en utilisant les primitives de bas niveau du multithreading, comme une queue FIFO gérée par des mutex pour collecter les résultats.

IV-C. Qt Concurrent

Qt Concurrent est une interface de programmation, basée sur QThreadPool, utile pour traiter des modèles de calcul parallèle les plus communs, à savoir : map, reduce et filter. Il offre aussi une méthode QtConcurrent::run(), qui peut être utilisée pour simplement lancer une fonction dans un autre thread.

Contrairement aux classes QThread et QRunnable, Qt Concurrent ne requiert pas l'utilisation de primitives de synchronisation de bas niveau : toutes les méthodes de Qt Concurrent retournent un objet de la classe QFuture, qui peut être utilisé pour demander le statut du calcul (sa progression), ainsi que pour mettre en pause, reprendre ou annuler le calcul ; par ailleurs, cet objet en contient aussi les résultats. La classe QFutureWatcher peut être utilisée pour suivre la progression de l'instance de QFuture et interagir avec cette dernière au moyen de signaux et slots (QFuture, étant une classe à sémantique de valeur, n'hérite pas de QObject).

Comparaison des caractéristiques
  QThread QRunnable Qt Concurrent (1)
Interface de haut niveau Non Non Oui
Orienté tâche Non Oui Oui
Support intégré pour la pause, la reprise et l'annulation Non Non Oui
Peut être lancé avec une priorité choisie Oui Non Non
Peut lancer une boucle événementielle Oui Non Non

V. Threads et QObject

V-A. Une boucle événementielle par thread

Jusqu'ici, on a parlé de « la boucle événementielle », considérant d'une manière ou d'une autre comme allant de soi le fait qu'il n'y ait qu'une seule boucle événementielle dans une application codée avec Qt. Ce n'est cependant pas le cas : les instances de QThread peuvent, en effet, lancer des boucles événementielles locales dans les threads qu'elles représentent. Par conséquent, on peut dire que la boucle événementielle principale est celle qui est créée par le thread invoqué par la fonction main() et qui est initiée avec la méthode QCoreApplication::exec() ; cette dernière méthode doit, par ailleurs, être appelée par ce thread. Le thread principal est aussi celui de l'interface graphique, parce qu'il s'agit du seul thread dans lequel les opérations concernant cette interface peuvent être effectuées. Dans les instances de QThread, la boucle événementielle locale peut être lancée par la méthode QThread::exec() à l'intérieur de la méthode virtuelle protégée QThread::run() :

 
Sélectionnez
class Thread : public QThread
{
protected:
	void run(){
		/* Initialisation */
		exec() ;
	}
};

Comme mentionné plus tôt, la méthode QThread::run() n'est plus une méthode virtuelle pure depuis la publication de Qt 4.4 ; en effet, cette méthode appelle la fonction QThread::exec(). À l'instar de la classe QCoreApplication, QThread possède aussi les méthodes QThread::quit() et QThread::exit() pour arrêter la boucle événementielle.

La boucle événementielle d'un thread délivre des événements à toutes les instances de QObject présentes dans ce thread ; ceci inclut par défaut tous les objets qui ont été créés ou déplacés dans ce thread. On peut aussi parler d'appartenance d'un QObject à un thread. Ceci s'applique aux objets qui sont créés dans le constructeur d'un objet de la classe QThread :

 
Sélectionnez
class MyThread : public QThread
{
	public:
		MyThread()
		{
			otherObj = new QObject;
		}
private:
	QObject obj;
	QObject* otherObj;
	QScopedPointer<QObject> yetAnotherObj;
};

À quel thread appartiennent les objets obj, otherObj, yetAnotherObj après que l'on crée un objet de la classe MyThread ? On doit considérer le thread qui les a créés : il s'agit du thread qui a exécuté le constructeur de l'objet de la classe MyThread. En conséquence, ces trois objets n'appartiennent pas au thread géré par l'instance de MyThread mais bien à celui qui a créé cette instance.

On peut, à n'importe quel moment, vérifier l'appartenance d'un objet à un thread via QObject::thread(). Par ailleurs, il est bon de savoir que les instances de QObject qui sont créées avant un objet QCoreApplication n'appartiennent à aucun thread ; ainsi, aucun événement ne leur sera délivré : en d'autres termes, QCoreApplication crée l'objet QThread qui représente le thread principal.

Image non disponible

La méthode sécurisée pour le thread QCoreApplication::postEvent() peut être utilisée en vue de poster un événement à destination d'un objet en particulier. Elle enverra l'événement dans la file de la boucle événementielle du thread auquel l'objet appartient ; ainsi, l'événement ne sera pas délivré à moins que ce thread n'ait une boucle événementielle en cours d'exécution.

Il est très important de comprendre que QObjet et toutes ses classes filles ne sont pas sécurisées pour les threads, bien qu'elles puissent être réentrantes ; en conséquence, on ne peut pas accéder à une instance de QObject depuis plus d'un thread à la fois, à moins que l'on n'adapte tous les accès aux données internes de l'objet (par exemple, en les protégeant au moyen d'un mutex). Il faut se rappeler que l'objet peut être en train de traiter des événements délivrés par la boucle événementielle du thread auquel il appartient, alors que l'on souhaite accéder à ces données depuis un autre thread ! Pour cette même raison, on ne peut supprimer un objet depuis un autre thread ; au contraire, on doit utiliser la méthode QObject::deleteLater(), qui postera un événement qui finira par causer la suppression de cet objet par le thread auquel ce dernier appartient.

De plus, QWidget et toutes ses classes, ainsi que les classes concernant l'interface graphique (quand bien même elles n'héritent pas de QObject, comme QPixmap), ne sont même pas réentrantes : elles ne peuvent être utilisées que depuis le thread principal.

L'appartenance d'un objet QObject peut être changée en appelant QObject::moveToThread() ; ce changement s'effectuera non seulement pour l'objet concerné, mais aussi pour tous les objets dont il est le parent. Comme QObject n'est pas sécurisé pour les threads, ce changement doit être fait depuis le thread auquel il appartient ; cela signifie que l'on ne peut changer l'appartenance d'un objet que depuis le thread auquel il appartient. De plus, Qt requiert que les objets enfants d'un objet de la classe QObject doivent appartenir au thread que leur parent. Ceci implique :

  • que la méthode QObject::moveToThread() ne peut pas être utilisée avec un objet ayant un parent ;

  • que des objets ne peuvent pas être créés dans une instance de la classe QThread en leur imposant cette instance comme parent :

     
    Sélectionnez
    class MyThread : public QThread
    {
    	protected:
    		void run()
    		{
    			QObject* obj = new QObject(this) ; // FAUX !!!
    			/* &#8230; reste de l'implémentation&#8230; */
    		}
    };

    Ceci est dû au fait que l'objet QThread appartient à un autre thread, à savoir celui dans lequel il a été créé.

Qt requiert aussi que tous les objets appartenant à un thread soient supprimés avant que l'objet de la classe QThread qui représente ce thread ne soit détruit ; ceci peut être facilement effectué en créant tous les objets appartenant à ce thread dans la file de la méthode QThread::run().

V-B. Signaux et slots échangés à travers les threads

Les prémisses ainsi exposées, comment appelle-t-on les méthodes des objets de la classe QObject appartenant à d'autres threads ? Qt offre une solution vraiment sympathique et propre : un événement est posté dans la file événementielle de ce thread et le traitement de cet événement consistera en l'appel de la méthode concernée ; bien évidemment, ce thread doit avoir une boucle événementielle en cours d'exécution. Ce mécanisme se base sur l'introspection fonctionnelle fournie par le MOC : par conséquent, seuls les signaux, les slots et les méthodes marqués avec la macro Q_INVOKABLE peuvent être appelés depuis les autres threads.

La méthode statique QMetaObject::invokeMethode() effectue tout le travail pour les développeurs :

 
Sélectionnez
QMetaObject::invokeMethode(objet, 
                           "nomDeLaMethode",
                           Qt::QueuedConnection,
                           Q_ARG(type1, arg1),
                           Q_ARG(type2, arg2));

Comme les arguments doivent être copiés dans l'événement qui est créé en coulisse, leurs types requièrent un constructeur, un constructeur de copie et un destructeur publics et doivent être enregistrés dans le registre des types de Qt par l'intermédiaire de la fonction qRegisterMetaType().

Les signaux et slots au travers des threads fonctionnent d'une manière similaire. Lorsque l'on connecte un signal à un slot, le cinquième argument de la méthode QObject::connect() est utilisé pour spécifier le type de la connexion :

  • une connexion directe (direct connection) signifie que le slot est directement appelé par le thread duquel est émis le signal ;
  • une connexion différée (queued connection) signifie qu'un événement est posté dans la file événementielle du thread auquel appartient le destinataire ; cet événement sera, plus tard, pris de la file par la boucle événementielle et causera l'appel du slot concerné  ;
  • une connexion différée bloquante (blocking queued connection) fonctionne comme la connexion précédente, mais implique que l'émetteur bloque la boucle événementielle du thread auquel il appartient, et ce, jusqu'à ce que l'événement engendré soit transmis et entièrement traité par le slot concerné ;
  • une connexion automatique (automatic connection), la connexion par défaut, correspond à la connexion directe si le destinataire appartient au thread courant, sinon à la connexion différée.

Dans tous les cas, il faut garder à l'esprit le fait que le thread auquel appartient l'objet-émetteur n'a aucune importance ! En cas de connexion automatique, Qt regarde le thread qui a appelé le signal et le compare avec le thread auquel appartient le destinataire afin de déterminer le type de connexion à utiliser. Notamment, la documentation de Qt 4.7.1 a tout simplement tort quand elle énonce#0160;:

« Connexion automatique (connexion par défaut) : son comportement est le même que celui de la connexion directe, si l'émetteur et le destinataire sont dans le même thread ; son comportement est le même que celui de la connexion différée, si l'émetteur et le destinataire sont dans des threads différents. »

En effet, l'appartenance de l'objet-émetteur à un thread n'importe aucunement. Par exemple :

 
Sélectionnez
class Thread : public QThread
{
	Q_OBJECT
	signals :
		void aSignal();
	protected:
		void run()
		{
			emit aSignal();
		}
};
/* &#8230; */
Thread thread;
Object obj;
QObject::connect(&thread, SIGNAL(aSignal()), &obj, SLOT(aSlot()));
thread.start();

Le signal aSignal() sera émis par le nouveau thread (représenté par un objet de la classe Thread) ; vu que ce n'est pas le thread auquel appartient l'objet de la classe Object (thread auquel l'objet de la classe Thread appartient aussi, ce qui accentue le fait que l'appartenance de l'émetteur à un thread n'importe pas), une connexion différée sera utilisée.

Voici un autre piège commun :

 
Sélectionnez
class Thread : public QThread
{
	Q_OBJECT
	slots :
		void aSlot()
		{
			/* &#8230; */
		}
	protected:
		void run()
		{
			/* &#8230; */
		}
};

/* &#8230; */

Thread thread;
Object obj;
QObject::connect(&obj, SIGNAL(aSignal()), &thread, SLOT(aSlot()));
thread.start();
obj.emitSignal();

Quand l'objet obj émet son signal aSignal(), quel type de connexion sera utilisé ? Comme vous avez dû le deviner : une connexion directe. En effet, l'objet de la classe Thread appartient au thread d'où le signal est émis. Dans le slot aSlot(), on peut donc accéder aux variables membres de la classe Thread alors qu'elles sont utilisées par la méthode run(), laquelle est simultanément au cours d'exécution : la recette parfaite du désastre.

Voici encore un exemple, probablement le plus important :

 
Sélectionnez
class Thread : public QThread
{
	Q_OBJECT
	slots:
		void aSlot()
		{
			/* &#8230; */
		}
	protected:
		void run()
		{
			Object* obj = new Object;
			QObject::connect(obj, SIGNAL(aSignal()), &thread, SLOT(aSlot()));
			/* &#8230; */
		}
};

Dans ce cas une connexion différée est utilisée ; ainsi, il faut que la méthode run() initie une boucle événementielle dans le thread auquel appartient l'objet de la classe Thread.

Une solution que l'on trouve souvent dans les forums, les billets de blog, etc. est d'ajouter un moveToThread(this) dans le constructeur de la classe Thread :

 
Sélectionnez
class Thread : public QThread
{
	Q_OBJECT
	public:
		Thread()
		{
			moveToThread(this); // FAUX
		}
	/* &#8230; */
};

Certes, ce code fonctionnera, notamment parce que l'appartenance de l'objet de la Thread a changé, mais c'est une très mauvaise manière de procéder, en ce que l'on comprend mal le but des objets de la classe QThread et de ses classes filles : ces objets ne sont pas des threads mais des interfaces entre threads et sont donc destinés à être utilisés depuis un autre thread (en général, depuis le thread auquel ils appartiennent).

Un bon moyen pour obtenir le même résultat est de séparer la partie « gestionnaire » de la partie « interface », c'est-à-dire de coder une classe héritant de QObject et d'utiliser la méthode QObject::moveToThread() pour changer son appartenance :

 
Sélectionnez
class Worker : public QObject
{
	Q_OBJECT
	public slots:
		void aSlot()
		{
			/* &#8230; */
		}
};

/* &#8230; */

QThread *thread = new QThread;
Worker *worker = new Worker;
connect(obj, SIGNAL(workReady()), worker, SLOT(doWork()));
worker->moveToThread(thread);
thread->start();

V-C. Choses à faire ou à ne pas faire

On peut :
  • ajouter des signaux à des classes-filles de QThread : c'est parfaitement sûr et ils feront la « bonne chose » (comme vu plus tôt : l'appartenance de l'émetteur à un thread n'a pas d'importance).
On ne devrait pas :
  • utiliser moveToThread(this) ;
  • forcer le type de la connexion : cela signifie habituellement que l'on fait quelque chose de mauvais, comme mélanger l'interface de contrôle de QThread avec la logique de programmation (chose qui devrait être gérée par un objet distinct appartenant au thread concerné) ;
  • ajouter des slots à une classe-fille de QThread : ils seront appelés depuis le « mauvais » thread, c'est-à-dire pas depuis le thread que l'objet gère, mais celui auquel il appartient, ce qui oblige de spécifier une connexion directe ou d'utiliser moveToThread(this) ;
  • utiliser QThread::terminate().
On ne doit pas :
  • arrêter le programme quand des threads sont encore en cours d'exécution ; il faut utiliser QThread::wait pour attendre la fin de leur exécution ;
  • supprimer une instance de QThread alors que le thread qu'il gère est encore en cours d'exécution ; si on veut que l'instance « s'autodétruise », on peut connecter le signal finished() au slot deleteLater().

VI. Remerciements

Merci à Thibaut Cuvelier, Claude Leloup et Fabien pour leur relecture !

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


À l'exception de QtConcurrent::run(), dont l'implémentation utilise QRunnable et qui partage donc ses avantages et inconvénients.

  

Copyright © 2012 - 2013 Developpez.com Developpez LLC. Tous droits réservés Developpez LLC. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents et images sans l'autorisation expresse de Developpez LLC. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.