FAQ Qt
FAQ QtConsultez toutes les FAQ
Nombre d'auteurs : 26, nombre de questions : 298, dernière mise à jour : 15 juin 2021
- Comment fonctionne QThread ?
- Pourquoi ne faut-il pas faire de traitement IHM dans un thread ?
- Comment est définie l'appartenance aux threads des objets Qt ?
- Comment utiliser les threads avec Qt ?
- Comment se passe une connexion entre threads ?
- Comment manipuler un mutex ?
- Comment mettre en pause un QThread ?
- Comment utiliser le système de signaux et de slots avec des threads ?
- Comment équilibrer le temps CPU ?
- Comment utiliser QThread grâce au signal/slot ?
- Comment appeler des fonctions de QObject appartenant à un autre thread ?
- Comment définir l'affinité des threads sous Linux ?
QThread est une interface simple permettant la création et manipulation d'un thread. Pour les versions de Qt inférieures à la 4.4, elle ne peut être instanciée, car la méthode run() est virtuelle pure. Pour les versions plus récentes, la méthode run() est uniquement virtuelle et la classe peut donc être instanciée. Il faut donc créer une classe héritant de QThread et qui réimplémente la méthode run(). Pour les versions de Qt inférieures ou égales à la 4.4, par défaut, QThread exécute une boucle d'événements.
- run() : méthode exécutée par le thread ;
- exec() : fonction bloquante qui va exécuter une event loop dans le thread ; cette fonction ne peut être appelée que par le run() ;
- quit() : slot qui stoppe la boucle d'événements du thread ;
- start() : slot qui lance un thread qui exécute la méthode run() ; cette fonction peut prendre en paramètre la priorité donnée au thread ;
- setPriority() : modifie la priorité d'exécution du thread ;
- wait() : fonction bloquante qui attend la fin de l'exécution ; il est possible de spécifier un timeout.
Cette classe émet le signal started() lorsqu'un thread est lancé et finished() lorsque le thread est terminé.
- sans boucle d'événements : seuls des objets n'héritant pas de QObject peuvent être utilisés. C'est à vous de gérer la condition de sortie de la fonction run(). Un signal étant thread safe, il n'y a aucun problème à en utiliser dans ce cas ;
- avec boucle d'événements : si le thread doit utiliser des objets héritant de QObject, la boucle d'événements doit être exécutée. Pour cela, il suffit d'utiliser la fonction exec() dans la méthode run() ;
- fonctionnement par défaut pour Qt après 4.4 : QThread exécute une boucle d'événements. Le but est alors de transférer les QObject que l'on veut vers ce QThread pour qu'il utilise le thread interfacer pour leur exécution. Cela implique que ces QObject ne doivent être manipulés qu'au travers du système de signaux et de slots de Qt. Sinon les fonctions doivent être thread safe. Attention à la destruction de ces QObject.
- QThread n'est pas un thread et n'appartient pas au thread. Ses slots ne s'exécutent pas dans le thread qu'elle interface ;
- Seule la fonction run() devrait être implémentée. Cette fonction run() est similaire au main() du programme principal. Son exécution correspond au moment où le thread existe réellement ;
- les QObject appartiennent au thread qui les crée. Il faut donc créer/supprimer les QObject utilisés par le thread dans la méthode run().
- Un signal étant thread safe, le thread peut utiliser les signaux du QThread implémenté.
Remarque : Qthread fournit aussi un slot nommé terminate() qui termine brutalement le thread. Cette fonction est à éviter le plus possible.
La grande majorité des API graphiques des divers OS ne sont absolument pas thread-safe. Le traitement IHM dans différents threads peut être la source d'accès concurrents qui peuvent mener à des erreurs fatales de l'application.
C'est pour cela que Qt oblige les traitements IHM dans le thread principal (celui exécutant le main() du programme). Attention, cette vérification est effectuée uniquement par des assertions, en compilation débug.
Si vous devez manipuler l'affichage par un thread, utilisez le système de connexion signal/slot.
Chaque QObject possède une affinité avec un QThread. Et ainsi chaque QObject va utiliser la boucle événementielle exécutée par le thread interfacé par ce QThread.
Par défaut, cette affinité dépend du thread qui crée le QObject. Le QObject prendra le QThread qui interface ce thread. Attention, lorsque l'on crée un QObject, le parent doit être associé au QThread interfaçant le thread.
obj1 et obj2 sont créés par le thread principal et sont associés au même QThread que QApplication. obj3 est créé dans le thread interfacé par QThread et est associé au QThread. obj4 génère un avertissement et son parent est 0.
Il est possible de transférer l'appartenance d'un QObject entre QThread grâce à la méthode moveToThread(). Attention à ces quelques règles :
- seul le thread associé au QThread du QObject doit utiliser cette fonction ;
- le QObject ne doit pas avoir de parent ;
- tous les enfants du QObject sont aussi transférés. Il est donc important que les QObject membres d'une classe dérivée de QObject aient pour parent this (sauf les QWidget) ;
- une fois transféré, le QObject ne doit plus être utilisé par l'ancien thread.
Qt fournit plusieurs possibilités pour manipuler des threads.
- QtConcurrent : un ensemble d'algorithmes simplifiant énormément l'utilisation des threads. Il partage un pool de threads qui s'adaptera à la machine d'exécution.
- QThread : c'est une classe qui interface un thread. Elle va créer, stopper, faire exécuter sa méthode run() et autres opérations sur un thread.
- QThreadPool : pour optimiser la création de threads, il est possible de manipuler un pool de thread avec QThreadPool. Ce pool de threads exécutera des classes héritant de QRunnable et réimplementant la méthode run().
Par défaut, la connexion entre thread est asynchrone, car le slot sera exécuté dans le thread qui possède l'objet receveur. Pour cette raison, les paramètres du signal doivent pouvoir être copiés. Ceci implique quelques règles simples :
- ne jamais utiliser un pointeur ou une "référence non const" dans les signatures des signaux/slots. Rien ne permet de certifier que la mémoire sera encore valide lors de l'exécution du slot ;
- s'il y a une référence const, l'objet sera copié ;
- il est préférable d'utiliser des classes Qt car elles implémentent une optimisation de la copie (cf. COW).
Il est possible d'utiliser ses propres classes. Pour cela, il faut :
- que cette classe ait un constructeur, un constructeur de copie et un destructeur public ;
- l'enregistrer dans les métatypes par la méthode qRegisterMetaType().
Contrairement aux slots, les signaux sont thread safe et peuvent donc être appelés par n'importe quel thread.
Pour protéger des données partagées entre threads, Qt fournit la classe QMutex. Cette classe fournit une base :
- lock : bloque le mutex ;
- tryLock : essaie de bloquer le mutex (possibilité de mettre un timeout) ;
- unlock : libère le mutex.
Afin de simplifier sa manipulation, Qt fournit la classe QMutexLocker basée sur le pattern RAII qui permet de manipuler correctement le mutex et éviter certains problèmes (un thread qui essaye de bloquer deux fois un mutex, un mutex non débloqué suite à une exception...). QMutexLocker va bloquer le mutex lors de sa création et le libérer lors de sa destruction. Il permet aussi de libérer (unlock()) et de bloquer à nouveau (relock()) le mutex.
Lorsqu'une ressource est partagée entre plusieurs threads, ces threads ont le droit d'accéder parallèlement à la ressource uniquement s'ils ne font que des accès en lecture. Ainsi, pour optimiser les accès, Qt fournit QReadWriteLock, qui est un autre mutex, beaucoup plus adapté. Contrairement à QMutex, QReadWriteLock va différencier les lock() en lecture et écriture :
- si un thread essaye de bloquer le mutex en lecture : aucune attente, le thread peut accéder à la ressource ;
- si un thread essaye de bloquer le mutex en écriture : le thread attend la libération du mutex.
- dans les deux cas, le thread attend la libération du mutex.
Comme QMutex, cette classe fournit une base :
- lockForRead() : bloque le mutex en lecture ;
- tryLockForRead() : essaie de bloquer le mutex en lecture (possibilité de mettre un timeout) ;
- lockForWrite() : bloque le mutex en écriture ;
- tryLockForWrite() : essaie de bloquer le mutex en écriture (possibilité de mettre un timeout) ;
- unlock() : libère le mutex.
De la même manière que QMutexLocker, Qt fournit deux classes qui simplifient la manipulation de ce mutex :
- QReadLoker : manipulation du mutex en lecture ;
- QWriteLoker : manipulation du mutex en écriture.
Pour diverses raisons, il peut être intéressant de mettre en pause l'exécution d'un thread pour une durée déterminée. Pour cela la classe QThread fournit plusieurs méthodes statiques :
Il n'y a pas vraiment de meilleure solution. Une possibilité consiste à utiliser QThread comme une sorte de passerelle de signaux et de slots. Pour cela, on tire profit de la possibilité de connecter un signal à un autre.
La technique est très simple. L'instance de QThread :
- ne possède aucun slot, car elle ne doit pas accéder aux objects manipulés par le thread ;
- définira, au besoin, des signaux dans sa définition de base, appelés des signaux sortants ;
- définira, au besoin, à la place des slots, des signaux qui serviront de passerelle vers les slots des objets utilisés par le thread, appelés des signaux entrants ; dans ce cas il faut utiliser la boucle d'événements.
On pourrait être tenté d'utiliser la fonction moveToThread sur le QThread en début du run() pour exécuter ses slots dans le thread. Seulement, cela poserait des problèmes : par exemple, pour redémarrer le thread après une première exécution, si vous utilisez start() comme un slot.
Avant toute chose, il faut bien comprendre que, dans la grande majorité des cas, les OS font déjà très bien leur travail et que vous n'aurez absolument pas besoin de faire tout ceci puisque le problème est différent selon chaque OS. Ces techniques sont donc, à utiliser en connaissance de cause.
Lorsqu'un thread fait énormément de traitements, il a tendance à monopoliser le maximum de temps CPU. Les autres threads se retrouvent ainsi fortement ralentis voire en attente constante de temps CPU. Il existe quelques solutions pour résoudre ce problème.
- La façon la plus simple est de diminuer la priorité d'exécution du thread. Pour ceci, il vous suffit d'utiliser la fonction QThread::setPriority() avec une priorité inférieure ou égale à QThread::LowPriority.
- La seconde méthode est de mettre en pause régulièrement le thread. Lorsque le thread est en pause, l'OS va récupérer le CPU utilisé par le thread et exécuter un autre thread. Une fois le thread réveillé, il est mis en attente. Le problème de cette méthode est qu'il est assez difficile de déterminer le temps de pause à appliquer. Si le temps de pause est trop faible, le thread va garder l'utilisation du CPU et cela ne réglera pas le problème. Si le temps est trop long, le thread peut être ralenti inutilement.
- Depuis la version 4.5, QThread fournit une méthode publique statique assez intéressante : QThread::yieldCurrentThread(). Avec cette méthode, contrairement à la mise en pause, le thread redonne la main du CPU à l'OS et se met en attente. L'OS détermine quel thread sera exécuté. Ainsi du temps CPU est toujours libéré régulièrement et, en principe, partagé. Il est cependant très difficile d'évaluer la fréquence à utiliser pour appeler cette méthode.
Une méthode intéressante est d'utiliser un QThread comme espace d'exécution d'un QObject. Pour cela il faut que :
- le QThread utilisé lance une eventloop (appel exec()). Depuis Qt 4.4, QThread lance une eventloop par défaut ;
- vos traitements soient implémentés dans un QObject ;
- utiliser moveToThread pour spécifier le thread d'exécution au QObject.
De plus il faut que ce QObject ait certaines propriétés :
- les fonctions appelées entre les thread sont des slots qui ne retournent rien. Ils sont appelés via un connect avec un signal (par défaut, un slot appelé par un connect s'exécute dans le thread du QObject) ;
- le résultat d'une fonction est retourné par un signal ;
- toutes les instances de QObject internes doivent avoir la classe comme parent. Ceci permet que tous les QObject changent de thread lors du moveToThread.
Cette façon de faire a aussi d'autres avantages :
- travailler en mono thread et pouvoir tester son code au maximum avant de passer au multi-threading ;
- équilibrage des charges sur les thread plus facile ;
- évite l'utilisation de mutex & co dans une majorité de cas ;
- développement multi-thread assez propre et simplifié.
Il peut être embêtant de devoir créer des signaux uniquement pour appeler un slot de la classe et un appel direct à la fonction est plus intéressant. Dans ce cas, il faut protéger l'exécution de cette fonction. Avec la classe précédente, comme la fonction est un slot et que le résultat est retourné par un signal, il est très simple de protéger l'appel directement. Pour cela il vous faut créer un QObject qui ne définit que des signaux et dont la classe est déclarée comme amie (friend). Les signaux ont la même signature que les slots de la classe. Il suffit alors de :
- connecter les signaux de cette classe avec les slots correspondants ;
- vérifier à chaque début de slot du QObject, si le thread d'exécution est celui dont le QObject dépend. Si différent, appeler le signal correspondant défini par cette classe. Ainsi, l'appel de la fonction est déplacé dans le bon thread.
Voici un exemple illustrant les explications. Le checkbox moveToThread permet de passer d'un fonctionnement mono-thread à multi-thread. Un clic sur l'image lance une génération.
On pourrait être tenté de créer un QObject à partir de QThread et d'utiliser la fonction moveToThread sur lui-même en début du run pour exécuter ses slots dans la thread. Seulement, cela poserait des problèmes : par exemple, pour redémarrer la thread après une première exécution, si vous utilisez le start comme un slot…
Lien : QThread::run
Lien : QThread::exec
Lien : QThread::moveToThread
Lien : Vous vous y prenez mal…
Il y a deux méthodes basiques pour appeler une fonction à partir d'un thread sur un QObject appartenant à un autre thread :
- la plus simple est de poster un événement sur l'objet à partir de l'autre thread. La boucle événementielle du thread de destination va alors délivrer l'événement sur l'objet de destination ;
- la deuxième approche est d'utiliser la queue de connexions de Qt. Cela est aussi implémenté dans la première méthode et permet d'atteindre n'importe quel objet qui a une boucle événementielle tournant sur le thread auquel appartient l'objet. Vous pouvez même spécifier la queue de connexions en passant en paramètre Qt::QueueConnection à la déclaration de la connexion ou utiliser Qt::AutoConnection, celui par défaut et qui décidera durant l'exécution comment le slot devra être appelé.
Lien : Boucle d'événements par thread
Lien : Les signaux et les slots à travers les threads
Si vous voulez définir l'affinité du processeur pour chaque QThread, vous devrez utiliser des appels aux fonctions natives (ce qui rendra votre programme moins portable !), car Qt ne propose aucune fonction pour le faire. Qt fournit un identifiant natif sur le thread actuellement en exécution avec la fonction statique Qthread::currentThreadId().
Sous Linux, vous devez regarder à la fonction pthread_setaffinity_np() (dans /usr/include/pthread.h). Cette fonction vous permet de définir l'affinité du thread. L'identifiant retourné par QThread::currentThreadId() est le même qu'en utilisant pthread_self(). Afin de changer l'affinité du thread en cours, vous pouvez utiliser un code équivalent à ceci :
CPU_ZERO(&
cpuset);
CPU_SET(cpuNumber, &
cpuset);
pthread_setaffinity((pthread_t) QThread
::
currentThreadId(), &
cpuset);
Vous pouvez utiliser pthread_self() pour éviter un cast.