Gestion asynchrone des journaux système avec QThread en C++

Dans le développement d'applications avec Qt, l'écriture inetnsive de journaux (logs) sur le disque peut impacter significtaivement les performances, notamment en bloquant le thread principal (UI thread). Pour pallier ce problème, une solution efficace consiste à déporter les opérations d'écriture dans un thread dédié.

Cette approhce repose sur un modèle producteur-consommateur utilisant une file d'attente sécurisée, un verrou mutuel pour l'accès aux données et un sémaphore pour la synchronisation.

1. Définition de la classe LoggerThread

Le fichier d'en-tête définit une classe héritant de QThread. Nous utilisons une QStringList pour stocker les messages en attente de traitement.

#ifndef LOGGERTHREAD_H
#define LOGGERTHREAD_H

#include <QThread>
#include <QMutex>
#include <QSemaphore>
#include <QStringList>
#include <QFile>
#include <QTextStream>
#include <QDateTime>

class LoggerThread : public QThread
{
    Q_OBJECT

public:
    explicit LoggerThread(QObject *parent = nullptr);
    ~LoggerThread();

    // Méthode appelée par les autres threads pour enregistrer un message
    void postMessage(const QString &text);

protected:
    void run() override;

private:
    QMutex m_bufferLock;      // Protège l'accès à la liste de messages
    QSemaphore m_signalInput; // Gère l'attente du thread
    QStringList m_logsQueue;  // File d'attente des messages
    bool m_isRunning;
};

#endif // LOGGERTHREAD_H

2. Implémentation de la logique de journalisation

Dans l'implémentation, la méthode postMessage ajoute des données au tampon, tandis que la méthode run consomme ces données en boucle fermée, en restant bloquée tant qu'aucun nouveau message n'est disponible.

#include "loggerthread.h"

LoggerThread::LoggerThread(QObject *parent) 
    : QThread(parent), m_isRunning(true) 
{
}

LoggerThread::~LoggerThread()
{
    m_isRunning = false;
    m_signalInput.release(); // Débloque le thread pour permettre l'arrêt
    wait();
}

void LoggerThread::postMessage(const QString &text)
{
    m_bufferLock.lock();
    
    // Formatage du message avec horodatage
    QString timeStamp = QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss.zzz");
    m_logsQueue.append(QString("[%1] %2").arg(timeStamp, text));
    
    m_bufferLock.unlock();
    
    // Signale au thread qu'une nouvelle donnée est disponible
    m_signalInput.release();
}

void LoggerThread::run()
{
    while (m_isRunning) 
    {
        // Attend qu'un message soit posté (bloquant, ne consomme pas de CPU)
        m_signalInput.acquire();

        if (!m_isRunning && m_logsQueue.isEmpty())
            break;

        QString logEntry;

        // Extraction sécurisée du premier message
        m_bufferLock.lock();
        if (!m_logsQueue.isEmpty()) {
            logEntry = m_logsQueue.takeFirst();
        }
        m_bufferLock.unlock();

        if (!logEntry.isEmpty()) {
            QFile file("application.log");
            if (file.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) {
                QTextStream stream(&file);
                stream << logEntry << "\n";
                file.close();
            }
        }
    }
}

3. Utilisation dans l'application

Pour utiliser ce système, il suffit d'instancier le logger au démarrage de l'application et de démarrer son cycle de vie. On peut ensuite appeler la méthode d'écriture depuis n'importe quel endroit du code.

#include <QCoreApplication>
#include "loggerthread.h"

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);

    // Initialisation du service de log
    LoggerThread *logger = new LoggerThread();
    logger->start();

    // Exemple d'utilisation
    logger->postMessage("Initialisation du système terminée.");
    logger->postMessage("Connexion à la base de données établie.");

    int result = app.exec();

    // Nettoyage en fin de programme
    delete logger;
    return result;
}

Analyse des composants clés

Le fonctionnement repose sur trois piliers fondamentaux :

  • Le Mutex (m_bufferLock) : Garantit que deux threads ne modifient pas la liste de messages simultanément, évitant ainsi les corruptions de mémoire.
  • La File d'attente (m_logsQueue) : Sert de zone tampon (buffer) entre la production de logs rapides et l'écriture disque plus lente.
  • Le Sémaphore (m_signalInput) : C'est le moteur de synchronisation. Contrairement à une boucle while(true) classique qui consommerait 100% d'un cœur CPU, le sémaphore met le thread en sommeil tant qu'il n'y a rien à traiter.

Étiquettes: Qt C++ multithreading QThread logging

Publié le 4 septembre à 18h26