Résolution des problèmes de rafraîchissement de l'interface utilisateur avec le modèle Producteur-Consommateur dans Qt

Étant donné la complexité de la modification du framework d'interface utilisateur existant du projet, la solution adoptée a été d'implémenter un modèle Producteur-Consommateur. Ce modèle permet de gérer efficacement les mises à jour de l'interface utilisateur provenant de plusieurs threads.

Architecture du Modèle

  • Producteurs (Threads Réseau) : Deux threads réseau distincts (NetThread1 et NetThread2) agissent comme producteurs. Ils sont responsables de la récupération des données et de leur ajout à une file d'attente partagée.
  • File d'attente (QQueue) : Une file d'attente, implémentée avec QQueue, sert de tampon pour stocker les données à afficher sur l'interface utilisateur.
  • Consommateur (Thread de Mise à Jour UI) : Un thread dédié (UpdateUiThread) agit comme consommateur. Il survielle la file d'attente et, lorsqu'elle contient des données, les retire pour effectuer les mises à jour de l'interface utilisateur.
  • Synchronisation : QWaitCondition et QMutex sont utilisés pour synchroniser l'accès à la file d'attente partagée et pour gérer la communication entre les producteurs et le consommateur.

Le flux de données est le suivant : Les threads réseau ajoutent les données à la file d'attente, et le thread de mise à jour de l'interface utilisateur attend que des données soient disponibles avant de les traiter.

netthread.cpp (Producteur)

Ce fichier implémente un thread producteur. Un QTimer est utilisé pour simuler l'arrivée de données réseau.


#include "netthread.h"
#include <QDebug>
#include <QTimer>

NetThread::NetThread(QWaitCondition *condition, QMutex *mutex, QQueue <QString>* queue, QObject *parent)
   : QObject(parent), condition(condition), mutex(mutex), queue(queue)
{
   // Simulation de l'envoi de données réseau via un QTimer
   timer = new QTimer(this);
   connect(timer, &QTimer::timeout, this, &NetThread::slotSimulateNetworkData);
   timer->start(1000); // Déclenche toutes les 1000 ms
}

void NetThread::setThreadIdentifier(const QString& id) {
   threadId = id;
}

void NetThread::slotSimulateNetworkData()
{
   QString dataToSend = threadId + " a reçu des données!";

   mutex->lock();
   queue->enqueue(dataToSend);
   qDebug() << "Ajouté à la queue par" << threadId << ":" << dataToSend;
   condition->wakeAll(); // Notifie le consommateur qu'il y a de nouvelles données
   mutex->unlock();
}
   

updateuithread.cpp (Consommateur)

Ce fichier contient le thread consommateur responsable de la mise à jour de l'interface utilisateur.


#include "updateuithread.h"
#include <QDebug>

UpdateUiThread::UpdateUiThread(QWaitCondition *condition, QMutex *mutex, QQueue <QString>* queue, QObject *parent)
   : QObject(parent), condition(condition), mutex(mutex), queue(queue)
{
}

void UpdateUiThread::slotProcessQueue()
{
   while (true) {
       mutex->lock();

       // Attend si la queue est vide
       if (queue->isEmpty()) {
           qDebug() << "Queue vide, en attente...";
           condition->wait(mutex); // Libère le mutex et se bloque jusqu'à ce que wakeAll/wakeOne soit appelé
           qDebug() << "Réveil ! Vérification de la queue...";
       }

       // Traite toutes les données disponibles dans la queue
       while (!queue->isEmpty()) {
           QString netData = queue->dequeue();
           qDebug() << "Traitement de:" << netData;
           // Dans un projet réel, cela déclencherait des opérations de mise à jour UI potentiellement coûteuses.
           // Ici, nous émettons un signal.
           emit signalUpdateUi(netData);
       }

       mutex->unlock(); // Libère le mutex après traitement des données
   }
}
   

widget.cpp (Gestionnaire Principal)

Ce fichier gère la création et le démarrage des threads, ainsi que la configuration des connexions.


#include "widget.h"
#include "ui_widget.h"
#include <QThread>
#include <QDebug>
#include "netthread.h"
#include "updateuithread.h"

Widget::Widget(QWidget *parent)
   : QWidget(parent), ui(new Ui::Widget)
{
   ui->setupUi(this);
   queue.clear(); // Assure que la queue est vide au démarrage

   // Création des objets QThread pour chaque tâche
   uiUpdateThread = new QThread(this);
   networkThread1 = new QThread(this);
   networkThread2 = new QThread(this);

   // Instanciation des objets worker et déplacement vers leurs threads respectifs
   updateProcessor = new UpdateUiThread(&condition, &mutex, &queue);
   updateProcessor->moveToThread(uiUpdateThread);

   netWorker1 = new NetThread(&condition, &mutex, &queue);
   netWorker1->moveToThread(networkThread1);

   netWorker2 = new NetThread(&condition, &mutex, &queue);
   netWorker2->moveToThread(networkThread2);

   // Connexions pour démarrer le traitement de la queue
   connect(this, &Widget::startUiUpdateProcessing, updateProcessor, &UpdateUiThread::slotProcessQueue);
   // Connexion pour mettre à jour le QTextEdit depuis le signal du consommateur
   connect(updateProcessor, &UpdateUiThread::signalUpdateUi, this, [=](const QString& data) {
       ui->plainTextEdit->appendPlainText(data);
   });

   // Connexions pour démarrer les threads réseau
   connect(this, &Widget::startNetworkUpdate, netWorker1, &NetThread::slotSimulateNetworkData);
   connect(this, &Widget::startNetworkUpdate, netWorker2, &NetThread::slotSimulateNetworkData);

   // Configuration des identifiants pour les threads réseau
   netWorker1->setThreadIdentifier("Thread Réseau 1");
   netWorker2->setThreadIdentifier("Thread Réseau 2");
}

Widget::~Widget()
{
   // Arrêt et nettoyage des threads
   uiUpdateThread->quit();
   networkThread1->quit();
   networkThread2->quit();

   delete updateProcessor;
   delete netWorker1;
   delete netWorker2;

   delete uiUpdateThread;
   delete networkThread1;
   delete networkThread2;

   delete ui;
}

void Widget::on_startButton_clicked()
{
   ui->plainTextEdit->appendPlainText("Démarrage du thread de mise à jour UI...");
   uiUpdateThread->start();
   emit startUiUpdateProcessing(); // Déclenche le début du traitement de la queue
}

void Widget::on_startNet1Button_clicked()
{
   ui->plainTextEdit->appendPlainText("Démarrage du Thread Réseau 1...");
   networkThread1->start();
   // Le timer dans NetThread s'occupera d'envoyer des données une fois le thread démarré.
}

void Widget::on_startNet2Button_clicked()
{
   ui->plainTextEdit->appendPlainText("Démarrage du Thread Réseau 2...");
   networkThread2->start();
   // Le timer dans NetThread s'occupera d'envoyer des données une fois le thread démarré.
}
   

Résultat Attendue

En utilisant ce modèle, les données provenant des différents threads réseau sont mises en file d'attente de manière sécurisée. Le thread de mise à jour de l'interface utilisateur traite ensuite ces données séquentiellement, garantissant ainsi que les opérations de mise à jour de l'interface utilisateur ne sont jamais exécutées simultanément par plusieurs threads, ce qui résout les problèmes de blocage et de désordre de l'interface utilisateur.

Étiquettes: Qt C++ multithreading Producteur-Consommateur QWaitCondition

Publié le 12 juin à 17h48