Pour qu’une interface graphique reste fluide aux yeux de l’utilisateur, le thread qui la gère ne doit jamais être interrompu plus d’une poignée de millisecondes. Or le parcours d’une liste avec une boucle est de nature à bloquer ce thread. Voici comment parcourir une liste sans bloquer le thread.
Soit l’algorithme suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
qDebug() << "Début du traitement"; QList<QObject*> liste; //... //insertion de 1 000 éléments dans la liste //... int iteration(0); ui->progressBar->setMaximumValue(1000); foreach(QObject* element, liste) { iteration += 1; //on fait quelque chose avec l’élément //le traitement pour chaque itération dure 50 à 90 ms //Mise à jour de la barre de progression ui->progressBar->setValue(iteration); } qDebug() << "Fin du traitement"; |
L’exécution complète de cet algorithme durera, selon l’estimation faite en commentaire, entre 50 et 90 secondes.
Si l’application ne contient que le thread principal, c’est-à-dire le même que celui de l’interface graphique, l’utilisateur ne pourra pas agir sur la fenêtre pendant 50 à 90 secondes.
Pire, l’utilisateur ne verra même la barre de progression évoluer, elle passera directement de vide à remplie.
Une des solutions constiterait à effectuer cette boucle dans un thread… mais Qt offre des mécanismes beaucoup plus souples, ce serait une mauvaise réponse à un problème conceptuel simple. Dans un prochain article je vous expliquerai pourquoi je ne suis pas partisan d’utiliser les threads pour autre chose que la parallélisation.
Boucle asynchrone
Une solution plus adaptée à la conception de Qt est l’usage de la boucle asynchrone, au lieu de boucler, nous allons faire des appels successifs à une fonction d’itération. Biensûr ces appels passeront par la run-loop.
L’avantage de cette solution est que les itérations effectuées sur la run-loop seront effectuées dans le respect des appels de fonction liés à l’interface graphique.
En effet, l’appel ui->progressBar->setValue(iteration) induit une mise à jour de l’interface graphique par un appel à la run-loop. La run-loop contient un très grand nombre d’appels de fonctions liés à l’interface graphique, que vous ne soupçonnez même pas.
En ajoutant des appels à une fonction d’itération passant par la run loop, vous laissez s’exécuter les fonctions liées à l’interface graphique, puis la run-loop exécutera l’appel la fonction d’itération.
Vous vous imaginez que par conséquent l’itération sera plus longue puisqu’elle sera différée et que vous ne pouvez pas savoir quand elle le sera. Pour le second point je suis d’accord, en revanche l’expérience prouve que l’itération sera en réalité moins longue.
La raison est facile à comprendre : le fait de bloquer l’interface graphique et de boucler longuement sur une liste surcharge le processeur. Cela ralentit l’application mais aussi tout le système (selon la priorité des processus). Si vous allouez de la mémoire dans votre itération, l’activité mémoire augmentera et, ce faisant, vous encouragez le système à swapper, ce qui le ralentira.
Pour coder ce type de boucle, il existe plusieurs méthodes, je vous propose celle que j’utilise :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
class MonControleur : public QObject { Q_OBJECT public: MonControleur(QObject* parent = 0); virtual ~MonControleur() {} void demarreAlgo(); //Fonctions privées private slots: void iteration(); //Membres privés private: int iteration_ = 0; QList<QObject*> liste_; }; MonControleur::MonControleur(QObject* parent) : QObject(parent) {} void MonControleur::demarreAlgo() { //Réinitialisation de l’itérateur iteration_ = 0; //Programmation d’un appel à l’itération //La valeur de 1ms permet de forcer le passage par la run-loop //Une valeur nulle entrainera un appel direct (branchement) QTimer::singleShot(1, this, SLOT(iteration())); } void MonControleur::iteration() { qDebug() << "Nombre d’itérations : " << iteration_; //Effectue l’opération if(++iteration_ < liste_.length()) { //Continue l’itération QTimer::singleShot(1, this, SLOT(iteration())); return; } else { //Le parcours est terminé, on peut envoyer un signal } } |
Cette façon de procéder, en plus de vous permettre de conserver la fluidité de votre interface graphique, vous oblige à concevoir le reste de votre application de façon asynchrone car pour connaître la fin du parcours de la liste il faut être capable de recevoir un signal.
Il est important de noter deux choses :
- il doit exister au moins un thread dans l’application. Vous devez donc appeler la fonction exec() de QApplication.
- la fonction appelée doit elle aussi être conçue selon le même modèle non-bloquant. Si besoin il faudra découper la fonction appelée en autant d’appels sur la run-loop et récupérer un signal en fin de traitement.
Voici comment procéder :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
class AutreControleur : public QObject{ Q_OBJECT public: AutreControleur(QObject* parent = 0); virtual ~AutreControleur() {} signals: signalTraitementTermine(); public slots: void demarreTraitement(QObject*); }; AutreControleur:AutreControleur(QObject* parent) : QObject(parent) {} void AutreControleur::demarreTraitement(QObject* element) { //Traitement sur l'élément qui dure moins de 50ms //Sinon on le découpe en plus petits traitements //A la fin du traitement on envoie un signal emit signalTraitementTermine(); } class MonControleur : public QObject { Q_OBJECT public: MonControleur(QObject *parent = 0); virtual ~MonControleur() {} void demarreAlgo(); //Fonctions privées private slots: void onTraitementTermine(); //Membres privés private: int iteration_ = 0; QList<QObject*> liste_; AutreControleur *autreControleur_; }; MonControleur::MonControleur(QObject* parent) : QObject(parent), autreControleur_(new AutreControleur(this)) { //Connexion entre le signal de AutreControleur et le slot //qui continuera l'itération dans cette classe QObject::connect(autreControleur_, &AutreControleur::signalTraitementTermine, this, &MonControleur::onTraitementTermine); } void MonControleur::demarreAlgo() { //Réinitialisation de l’itérateur iteration_ = 0; //Déclenchement du traitement //On utilise le méta-objet pour faire l'appel et on force //Le passage par la run-loop à cause de l'affinité de thread //de this et autreControleur_ QMetaObject::invokeMethod(autreControleur_, "demarreTraitement", Qt::QueuedConnection, Q_ARG(QObject*, liste_[iteration_])); } //Cette fonction est appelée quand le contrôleur AutreControleur //a terminé le traitement void MonControleur::onTraitementTermine() { qDebug() << "Nombre d’itérations : " << iteration_; //Effectue l’opération if(++iteration_ < liste_.length()) { //Continue l’itération QMetaObject::invokeMethod(autreControleur_, "demarreTraitement", Qt::QueuedConnection, Q_ARG(QObject*, liste_[iteration_])); return; } else { //Le parcours est terminé, on peut envoyer un signal } } |
Si vous souhaitez être notifié lorsqu'un nouvel article est publié, abonnez-vous à notre newsletter et vous recevrez un email dès qu'un article sera publié.