Présentation du multithreading

Qu'es ce c'est

Un thread est une succession de commandes exécuté dans l'ordre pour aboutir à un résultat. Par exemple,

int main(int argc, char * argv[])
{
        int valeur;
        std::cin >> valeur;
        std::cout << 2* valeur;
        return 0;
}

Le programme créé un thread qui va exécuter cette suite d'instruction. Ce thread est communément appelé le thread principal. Un programme multithread est donc simplement un programme qui va exécuter plusieurs threads en parallèle.

De manière plus précise, un programme multithread créé plusieurs threads qui vont se partager les ressources CPU d'un PC pour s'exécuter. La gestion de ces ressources est effectuée par l'OS. Sur un PC mono-coeur, les threads sont exécutés par le même processeur. Les threads ne sont donc pas vraiment exécutés en parallèle : l'OS va partager le processeur en exécutant des morceaux de chaque thread les uns après les autres. Sur un PC multicoeur, cette exécution est partagée par tous les processeurs.

Il est aussi important de savoir que :

  • chaque thread as son propre file d'éxécution, un ensemble de registre et une pile mémoire non partagé.
  • la mémoire dynamique (alloué sur le tas) est par contre commune à tout les threads.

As ton vraiment besoin du multithreading?

La réponse n'est pas aussi évidente… Le multithreading n'est pas magique. Il est comme la force. L'utilisation de multithread n'est pas toujours égale à un gain de performance. Au contraire, elle complexifie le code et augmente le nombre de bugs potentiel. Par exemple, faire des copie de fichier en parallèle par plusieurs threads. Le point bloquant est les accès disques. Le multithreadding n'est pas spécialement pertinent.

Avant d'utiliser le mutithreading, il faut mieux comprendre pourquoi!! Mais seule l'expérience permet d'y répondre. Il est toujours conseillé d'utiliser le multithreading en dernier recours. Surtout lorsque l'on débute. La règle générale est : moins de threads moins de bugs. Qt, par sont fonctionnement permet quelques alternative qui évite l'utilisation de multithreading.

Le besoin de multithreading peux se découper en deux cas principale :

Faire fonctionner plusieurs objet en parallèle.

  • Si leurs exécution bloque le thread, il faut faire un thread pour chaque objet.
  • S'ils exploitent l'eventloop de Qt, il est possible de créer un nombre restreins de thread et de répartir leurs exécution sur ces threads.

Pour la rapidité. Il faut savoir que chaque thread utilisé va ajouter du temps sur l’exécution de la tâche… Pour faire simple

  • T le temps d'une tâche.
  • t le temps ajouté par l'utilisation d'un thread.
  • N nombre de thread.

Donc quand on exécute la tâche sur N threads, le temps effectif est T+N*t. Mais comme ce temps est répartie sur N threads, le temps d'un points de vue utilisateur est T / N + t. Si on utilise trop de thread que va t'il se passer? t va augmenter et à un moment on aura T < T / N + t On aura donc rien gagner et même perdu.

Définition

Voici quelques définitions à connaître avant d'aller plus loin dans ce tutoriel.

File d'exécution : succession d'instructions exécutées.

Event loop : Une eventloop est une boucle infinie qui exécute successivement une liste d'évènements qui évolue au cours du temps. Elle correspond à l'exécution d'un thread. Un évènement peut-être comparé à des callbacks qui sont appelés régulièrement au cours de l'exécution du programme. Le principe est assez simple :

  • Au cours de l'exécution du programme, des évènements sont ajoutés dans la liste.
  • L'eventloop exécute les uns après les autres les évènements de la liste et les enlève.

Accès concurrent : un accès concurrent est un accès à une même ressource (mémoire, instance d'une classe, pointeur,…) par plusieurs threads. Ceci est le principale problème du multithreading. Une ressource accédée par plusieurs threads peut générer des problèmes insoupçonné. Par exemple, un pointeur est partagé entre plusieurs threads. Le premier thread effectue un traitement sur le pointeur. Au même instant, un second thread détruit ce pointeur pour le réinitialiser ⇒ le premier thread utilise un pointeur non valide ⇒ crash. Il faut donc protéger ces accés.

Re-entrant : De manière simplifié pour Qt, une fonction est dite reentrante si l'appel à cette fonction peut être appelée par différent threads s'il utilise leur propre données. Une fonction appartenant à une classe, peut être appeler par différente thread uniquement si l'instance de la classe est différente pour chaque thread.

Thread safe : une fonction est dite threadsafe si l'appel à cette fonction peut être appelé par différente thread sur une donnée partagée entre eux. Contrairement à la notion de re-entrance, une fonction appartenant à une classe peut être appelée par différents threads avec une même instance de la classe.

Opération Atomique : Une Opération Atomique est une opération complex dont l'exécution ne peux être bruité par un accès concurrent par les autres threads. On peut dire que l'Opérateur est thread-safe.

Ressources : pour ce tutoriel, une ressources peut être une instance de classes, une portion de code, un type primitif,….

Section critique : une section critique est un morceau de code partagé entre plusieurs threads et qui doit être exécuté par un seule thread à la fois.

Durée de vie : la durée de vie d'une instance et la portion de code dans laquelle cette instance existe. Par exemple un membre d'un classe à la même durée de vie que la classe et une instance d'une fonction ou d'un bloque {} à la durée de vie de cette fonction ou de ce bloque.

void f()
{
     A a;
    ...
    if(..)
    {
        B b;
        ...
        //fin de la durée de vie de b
    }
 
//fin de la durée de vie de a
}

Notion importante

Synchrone : une exécution synchrone d'une classe signifie que l'appel as une de ses fonctions est immédiate et que le résultat voulue est obtenue une fois que la fonction exécuté. On peut aussi entendre parler de fonction bloquante.

Asynchrone : une exécution asynchrone d'une classe, signifie que son exécution est planifier et que le résultat sera obtenue à un autre instant. Avec Qt, cette planification s'effectue par une eventloop. On peut aussi entendre parler de fonction non-bloquante. Appartenance à un thread: toutes les classes héritant de QObject, appartiennent au thread qui la créée, et doivent uniquement être utiliser dans ce thread.

Emission d'un signal : les signaux de Qt sont thread safe et peuvent être utiliser par n'importe quelle thread.

Exécution d'un slot : l'exécution en multithreading d'un slot dépend du type de connexion. En règle générale, pour éviter tout problèmes d'accès concurrent, un slot est exécuté dans le thread auquel appartient l'instance de sa classe.

Connexion : une connexion entre threads peut être de différent type :

  • Connexion directe : l'émission d'un signal appelle directement un slot de manière synchrone. Ceci peut créer des accès concurrents si l'instance du signal et du slot appartient à des QThreads différents.
  • Connexion par eventloop : l'émission d'un signal créé un événement qui est ajouté à l'eventloop du QThread auquel appartient l'instance du slot. Le slot est ainsi exécuté asynchronement. Les paramètres du signal sont copié, même les références constante. Les références sont par contre interdite.
  • Connexion automatique : c'est le mode par défaut qui choisira entre les deux précédents modes de connexion. Si l'instance du signal et du slot appartient au même thread, la connexion sera directe, sinon par eventloop. Il est fortement conseillé d'utiliser uniquement ce type de connexion.
  • Connexion bloquée : c'est un mixte entre la connexion directe et la connexion par eventloop. L'émission d'un signal créé un évènement qui est ajouté à l'eventloop du thread auquel appartient l'instance du slot et bloque sa thread jusqu'à la fin de l'exécution du slot. Du au bloquage du thread, ce type de connexion devrait être uniquement utilisé entre deux threads distincte et, mal utilisé, peut produire un blocage infinie des threads mise en jeux. Attention à son utilisation.

Alternative de Qt

Avant de passer au multithreading, il existe quelques alternatives qui est bon de connaître. Mais qui ont leurs limites.

Classe asynchrone

Pour éviter l'utilisation abusive de thread, beaucoup de classe Qt ont un fonctionnement asynchrone( QFtp, QProcess,…). Pour cela, ces classes utilisent l'eventloop pour planifier leur exécution en parallèle et ainsi partager le thread. C'est une méthode qui permet bien souvent d'éviter la création de thread secondaire inutile. Par exemple, la récupération d'un fichier par FTP :

  • De manière synchrone, une fois le téléchargement demander, on attend la fin du téléchargement avant de passer à la suite. Ceci bloque l'exécution du thread uniquement pour l'attente de cette réception. Pour palier à cela, il faut créé un thread secondaire. Ceci complexifie malheureusement le code.
  • Avec Qt, l'objet QFtp est asynchrone. Une fois la demande effectué, il n'y as aucun besoin d'attendre ou de créer de thread. Le téléchargement est effectué en parallèle et un signal est émit une fois le téléchargement fini.

Ainsi, l'utilisation de classe asynchrone évite la création d'un thread secondaire et évite de compliquer le code inutilement.

Exécution de l'eventloop

Une autre manière simple d'éviter l'utilisation de thread secondaire lors du traitement lourd ponctuelle, est d'exécuter, de temps à autre, l'exécution de l'eventloop du Thread principale avec la fonction static QCoreApplication::processEvents(). (lien faq http://qt.developpez.com/faq/?page=qt4Gui#de-geler-ihm) Il faut toute fois éviter d'appeler trop souvent cette méthode et il est assez difficile d'évaluer l'endroit le plus adapté pour l'appeler.

participant "calculer nombre \npremier" as A
participant "event loop" as E

loop
A->A: action
A->E: run eventloop
end
QVector<int> maClass::monTraitementLourd(int max)
{
    QVector<int> nombrePremier;
    nombrePremier <<1 << 2;
    for (int i = 3  ;  i < max;  i+=2)
   {
          //exécution de l'eventloop.
          QCoreApplication::processEvents();
          bool estNombrePremier(true);
          for (int j =1 ;  j < nombrePremier.size(); ++j)
         {
                if( i % nombrePremier[j] == 0)
                {
                    estNombrePremier = false;
                    break;
                }
         }
        if (estNombrePremier)
        {
                nombrePremier << i;
        }
   }
   return    nombrePremier;
}

La classe QProgressDialog permet de simuler ce fonctionnement en affichant une dialog contenant une progressBar. Pour cela il suffit de la rendre modal et d'appeler régulière la fonction setValue, qui va mettre a jour l'évolution de la progressBar et exécuter l'eventloop au besoin pour éviter l'effet “geler” de l'ihm.

participant "calculer nombre \npremier" as A
participant "progress bar" as P
participant "event loop" as E

loop
A->A : action
A->P : next step 
alt si besoin
P->E : run eventloop
end
end
QVector<int> maClass::monTraitementLourd(int max)
{
    QVector<int> nombrePremier;
    nombrePremier <<1 << 2;
 
    //creation d'un progressdialog modale
    QProgressDialog progress("Calcule de nombre premier", Qstring(), 3, max);
    progress.setWindowModality(Qt::WindowModal);
 
 
    for (int i = 3  ;  i < max;  i+=2)
   {
          //Met à jour la progressbar et exécution de l'eventloop.
          progress.setValue(i);
 
         if (progress.wasCanceled())
         break;
 
 
         bool estNombrePremier(true);
         for (int j =1 ;  j < nombrePremier.size(); ++j)
         {
                if( i % nombrePremier[j] == 0)
                {
                    estNombrePremier = false;
                    break;
                }
         }
         if (estNombrePremier)
         {
                nombrePremier << i;
         }
   }
   return    nombrePremier;
}

Utiliser l'eventloop

La dernière est de découper le traitement en sous partie et d'utiliser l'eventloop pour son exécution. Pour cela, il est possible d'utiliser :

  • static QCoreApplication::postEvent() : poste un événement pour un objet donné. Ceci implique la création de ses propres évènements.
class calculNombrePremier : public QObject
{
    //Q_OBJECT
    QVector<int> m_nombrePremier;
    int m_max;
 
    struct myEvent : public QEvent
    {
        static const QEvent::Type type;
        const int i;
        myEvent(int i):QEvent(type),i(i) {}
 
    };
 
public :
    void lancerCalcul(int max)
    {
         m_nombrePremier.clear();
         m_max = max;
         m_nombrePremier <<1 << 2;
 
        //post un evenement dans l'eventloop pour exécuter le slot
         QCoreApplication::postEvent(this,new myEvent(3));
 
 
    }
    QVector<int> nombrePremier() {return m_nombrePremier;}
signals :
    // signal emit une fois le nombre max atteint.
     void finCalcul();
protected :
     bool event(QEvent *ev)
     {
        if(ev->type() == myEvent::type)
        {
 
            int nombreCourant = ((myEvent *)ev)->i;
            bool estNombrePremier(true);
            for (int j =1 ;  j < m_nombrePremier.size(); ++j)
            {
                   if( nombreCourant % m_nombrePremier[j] == 0)
                   {
                       estNombrePremier = false;
                       break;
                   }
            }
           if (estNombrePremier)
           {
                   m_nombrePremier<< nombreCourant ;
           }
           //incrément du nombre courant pour le prochaine teste
           nombreCourant += 2;
 
           if(nombreCourant <=m_max)
              {
              //on post un evenement dans l'eventloop pour exécuter le slot
                  QCoreApplication::postEvent(this,new myEvent(nombreCourant));
              }
              else
              {
                  qDebug() << m_nombrePremier;
                  //sinon on as finie le traitement et on emet un signal.
                 // emit finCalcul();
              }
              return true;
 
        }
        return QObject::event(ev);
    }
 
};
 const QEvent::Type calculNombrePremier::myEvent::type = (QEvent::Type)QEvent::registerEventType ();
  • static QTimer::singleShot() : permet d'exécuter un slot dans n milli-second. Cette méthode, plus simple à mettre en place, permet par exemple tranformé un bloucle en une fonction appelé régulièrement par l'eventloop.
class calculNombrePremier : public QObject
{
    Q_OBJECT
    QVector<int> m_nombrePremier;
    int m_max;
    int nombreCourant;
public :
    void lancerCalcul(int max);
    {
         m_nombrePremier.clear();
         m_max = max;
         nombrePremier <<1 << 2;
         nombreCourant = 3;
        //post un evenement dans l'eventloop pour exécuter le slot
        QTimer::SingleShot(0,this,SLOT(prochainTest()));
 
    }
    QVector<int> nombrePremier() {return m_nombrePremier;}
signals :
    // signal emit une fois le nombre max atteint.
     void finCalcul();
private slots:
   {
        //test si le nombre courant est premier
         bool estNombrePremier(true);
         for (int j =1 ;  j < m_nombrePremier.size(); ++j)
         {
                if( nombreCourant % m_nombrePremier[j] == 0)
                {
                    estNombrePremier = false;
                    break;
                }
         }
        if (estNombrePremier)
        {
                m_nombrePremier<< nombreCourant ;
        }
        //incrément du nombre courant pour le prochaine teste
        nombreCourant += 2;
 
       //tans que le nombre courant est inférieur au nombre max
     if(nombreCourant <=m_max)
        {
        //on post un evenement dans l'eventloop pour exécuter le slot
            QTimer::SingleShot(0,this,SLOT(prochainTest()));
        }
        else
        {
            //sinon on as finie le traitement et on emet un signal.
            emit finCalcul();
        }
   }
 
}

* void timerEvent(QTimerEvent*),startTimer et killtimer :

Pourquoi et comment protéger les données

Le gros problème du multithreading est le partage de ressource entre plusieurs threads. Voici different exemple :

  • pointeur : à un instant t, un thread va utiliser le pointeur et un autre thread va le détruire pour le réallouer ⇒ accés mémoire faux.
  • type primitif : à un instant t, un thread A va incrémenter la valeur et un autre thread B va le décrémenter ⇒ impossible de prévoir le résultat. Pourquoi, un cas possible : A et B récupère la valeur au même instant. A incrémente la valeur et la met à jour, mais B à récuperer la valeur précédente. B décrémente la valeur et la met à jour ⇒ l'incrémentation de A n'est pas pris en compte par B ⇒ la valeur est fausse.
  • une instance de classe : un buffer est partagé par deux threads. A un instant t, la thread A ajoute des données au buffer et la thread B récupère les données. Le thread A augementer le buffer pour ajouter des données et produite une desallocatopn/allocation de la memoire ⇒ Sans protection le thread B utilise la mémoire désalloué ⇒ bug mémoire
  • Race condition : une condition partagé entre plusieurs thread. Tout comme l'exemple du type primitif, rien ne certifi que le test éffectué par plusieur thread en mlêm temps sera correcte.

Il existe une infinité de problème lié à ces accès concurrent. Il faut donc les protéger.

Définition

dead lock

le dead lock est un conséquence de l'utilisation de mutex qui va produire un bloquage de un ou plusieur threads indéfiniment. Par exemple, un thread bloque un mutex, mais pour une raison ou une autre, la libération n'est pas effectué. Si ce thread ou un autre essai de bloquer se mutex, il se trouve en attente de libèration pour le bloquer. Seulement comme le mutex n'as pas été précédement libéré, l'attente est infinie et le thread est bloqué. Un autre exemple, le partage de deux ressources entre deux threads. Le premier bloque les mutex dans un ordre et le second dans l'autre ordre. 0 un même instant les deux threads essaie de bloquer les deux mutex. Lorque le premier thread essaie de bloquer le deusième mutex et se retrouve bloqué jusqu'à la libération car l'utre thread le bloque déjà. Le deusième thread essai de bloquer l'autre mutex et se retrouve dans la mêm configuration. Les deux threads se sont bloqué mutuellement sans pouvoir se débloquer. deadlock.cpp

#include <QtCore>
QMutex m1;
QMutex m2;
 
/*thread A : bloque m1 puis m2*/
class threadA : public QThread
{
 
protected :
    void run()
    {
 
        forever
        {
 
            qDebug() << "Thread A : essaie de bloquer m1";
            m1.lock();
            qDebug() << "Thread A : m1 bloque";
            qDebug() << "Thread A : essaie de bloquer m2";
            m2.lock();
            qDebug() << "/******Thread A as bloque les deux mutex****/" << endl;
 
            m1.unlock();
            m2.unlock();
        }
    }
};
 
/*thread B : bloque m2 puis m1*/
class threadB : public QThread
{
 
protected :
    void run()
    {
        forever
        {
 
            qDebug() << "\tThread B : essaie de bloquer m2";
            m2.lock();
            qDebug() << "\tThread B : m2 bloque";
            qDebug() << "\tThread B : essaie de bloquer m1";
            m1.lock();
            qDebug() << "\t/*****Thread B as bloque les deux mutex****/" << endl;
 
            m2.unlock();
            m1.unlock();
        }
    }
};
int main(int argc, char **argv)
{
    //creation et lancement des threads
    threadA a; a.start();
    threadB b; b.start();
 
    //attente de la fin d'execution des deux threads
    a.wait(); b.wait();
}

Vous constaterez que le deadlock n'apparait toujours au mêm instant.

Live lock

le live lock est un conséquence de l'utilisation du trylock qui peut généré qu'un thread n'est jamais ou difficilements les ressources qu'il as besoin. Par exemple, pour éviter le deadlock de l'exemple précédent, un trylock est utilisé pour essayer de bloquer le deusième mutex par les threads. Comme les deux thread essaie de bloquer les deux mutex en même temps, le trylock echoué régulièrement. Et les deux therad ont besoin de faire un grand nombre d'essai avant de réussir à bloque les deux mutex.

live-lock.cpp

#include <QtCore>
QMutex m1;
QMutex m2;
 
/*thread A : bloque m1 puis m2*/
class threadA : public QThread
{
 
protected :
    void run()
    {
        int nbTest (0);
        forever
        {
 
            m1.lock();
            if (m2.tryLock())
            {
                qDebug() << "/***Thread A bloque les deux mutex apres "<<nbTest<<" *******/" << endl;
                m2.unlock();
                nbTest = 0;
            }
            else
            {
                ++nbTest;
            }
 
            m1.unlock();
 
        }
    }
};
 
/*thread B : bloque m2 puis m1*/
class threadB : public QThread
{
 
protected :
    void run()
    {
        int nbTest (0);
        forever
        {
 
            m2.lock();
            if (m1.tryLock())
            {
                qDebug() << "\t/***Thread B  bloque les deux mutex aprs "<<nbTest<<" *******/" << endl;
                m1.unlock();
                nbTest = 0;
            }
            else
            {
                ++nbTest;
            }
 
            m2.unlock();
        }
    }
};
int main(int argc, char **argv)
{
    //creation et lancement des threads  
    threadA a; a.start();
    threadB b; b.start();
 
    //attente de la fin d'execution des deux threads
    a.wait(); b.wait();
}

Pour pourrez voir le nombre de teste avant réussite qui peut être trés élevé. Dans certaine condition ce nombre pourrais devenir infinie.

QMutex et QMutexLocker

Un mutex est une classe therad safe qui permet de savoir si une ressource est utilisée ou non par un autre thread et permet de résoudre le problème d'accés concurrent. La classe Qt correspodante est le QMutex. Cette classe possède divers fonctionnalités :

  • lock() : bloque le mutex pour informer l'utilisation d'une ressource par un thread. Si le mutex est déja bloqué, cette fonction stop le thread demandeur jusqu'à ce qu'il puisse le bloquer.
  • unlock() : libère le mutex.
  • tryLock() : contrairement au lock(), cette fonction essaie de bloquer le mutex et retourne vrai s'il as réussi. Cette fonction peut prendre en paramètre un temps maximum authorisé pour réussir à bloquer le mutex. Ceci permet de pas bloquer inutilement un thread si une ressource est déjà utilisé.

De plus, lors de sa création, il est possible de spécifier s'il as un comportement récursive ou non récursive. Le comportement récursive, signifie qu'un thread peut locker plusieur fois un mutex. Le mutex sera libéré àpres le même nombre de libération. Contrairement au comportement non récursive qui ne permet à un thread que de le bloquer qu'une fois. Pour des raisons de performance et comme il est plustôt rare qu'un thread doit bloquer plusieurs fois un même mutex, le comportement par défaut est le non récursive.

#include <QtCore>
QMutex mRecursive( QMutex::Recursive);
QMutex mNonRecursive;
 
/*thread A : bloque récursivement un mutex et le libère*/
class threadA : public QThread
{
    QMutex &m;
public :
    threadA(QMutex &mutex):m(mutex){}
protected :
    void run()
    {
        int nbLocked =0;
        /*bloque le mutex recursivement tout les 500ms*/
        for (int i = 0 ; i<5; ++i)
        {
            qDebug()<< "Thread A : essai de bloquer le mutex";
            m.lock();
            ++nbLocked;
            qDebug()<< "Thread A : mutex bloque "<<nbLocked <<"fois";
 
            msleep(500);
        }
        /*libere le mutex */
        for (int i = 0 ; i<5; ++i)
        {
            m.unlock();
            --nbLocked;
            qDebug()<< "Thread A : mutex libere - mutex encore bloque "<<nbLocked <<"fois";
        }
    }
};
 
/*thread A : bloque le mutex*/
class threadB : public QThread
{
    QMutex &m;
public :
    threadB(QMutex &mutex):m(mutex){}
protected :
    void run()
    {
        /*pour etre sure que l'autre thread as bloquer le mutex*/
        sleep(1);
        qDebug()<< "\tThread B : essai de bloquer le mutex";
        /*bloque le mutex - attente de la liberation*/
        m.lock();
        qDebug()<< "\tThread B : mutex bloquer";
        m.unlock();
    }
};
int main(int argc, char **argv)
{
    qDebug() <<"/****** TEST D'UN MUTEX RECURSIVE   *********/";
    {
        //creation et lancement des threads
        threadA a(mRecursive); a.start();
        threadB b(mRecursive); b.start();
 
        //attente de la fin d'execution des deux threads
        a.wait(); b.wait();
    }
 
    qDebug() <<endl;
    qDebug() <<"/****** TEST D'UN MUTEX NON RECURSIVE   ****/";
    {
        //creation et lancement des threads
        threadA a(mNonRecursive); a.start();
        threadB b(mNonRecursive); b.start();
 
        //attente de la fin d'execution des deux threads
        a.wait(); b.wait();
    }
   return 0;
}

La première partie montre le fonctionnement du mutex recursive : le therad A va bloquer le mutex recursivement et le libère. Le thread B essaie de bloquer le mutex et va ainsi attend la libération du mutex pour le bloquer Le second montre le fonctionnement du mutex non recursive : le therad A va bloquer le mutex une première fois sans problème. Le deusième bloque sur le mutex va bloquer le thread A car le mutex est déjà bloqué. Le thread B essaie de bloquer le mutex. Comme le thread est bloqué, il ne pourra jamais libèrer le mutex. Les deux thread sont bloqué. Ceci est plus communément appeler dead lock. En mode debug, Qt vous écrira une jolie erreur dans la console “QMutex::lock: Deadlock detected in thread …”

Pour simplifier la manipulation des mutex est éviter une libération oublié, Qt fournie la class QMutexLocker représentant une implementation RAII pour le mutex, qui va bloquer le mutex lors de ca création et le libérer lors de sa destruction. Il est aussi possible de libérer et rebloquer le mutex pendant sa durée de vie.

QMutex lock;
QMutex lock2;
 
void f()
{
 //locker bloque le mutex lock
 QMutexLocker locker(lock);
 // on bloque le mutex lock2
 lock2.lock();
 
 //fonction qui génère un exeption
 uneFonction();
 
 // fonction non appelée suite à l'exception
 lock2.unlock();
 
 // locker est détruit et va libérer lock
 // malheureusement lock2 est toujours bloqué
}

QReadWriteLock, QReadLocker et QWriteLocker

Accés en lecture : accés qui ne va produire aucune modification de la ressource. Accés en écriture: accés qui peut produire des modifications de la ressource.

L'accés concurrent à une données est un problème uniquement si l'un des accés est en écriture. L'accés current en lecture par plusieur thread ne pause aucun problème. Le QReadWriteLock, est un mutex qui va prendre en compte cette propriété. Ainsi, il peut être bloqué par plusieurs threads simultabément pour des accés en lecture et bloquer par un seule thread pour un accés en écriture. Comme QMutex, cette classe fournit une base :

  • lockForRead() : bloque le mutex en lecture
  • lockForWrite() : bloque le mutex en écriture
  • unlock() : libère le mutex
  • tryLockForRead() : essaie de bloquer le mutex en lecture. Possibilité de mettre un timeout()
  • tryLockForWrite() : essaie de bloquer le mutex en écriture. Possibilité de mettre un timeout()

De plus Qt fournie aussi deux classes similaire à QMutexLocker pour simplifier sa manipulier :

  • QReadLoker : manipulation du QReadWriteLock en lecture
  • QWriteLoker : manipulation du QReadWriteLock en écriture
 1: #include <QtCore>
 2: QReadWriteLock lock;
 3: QVector<int> vect(10);
 4:
 5:
 6: /*thread A : affiche le vect ou l'initialise avec une suite n = n+1*/
 7: class threadA : public QThread
 8: {
 9:
10: protected :
11:     void run()
12:     {
13:
14:         forever
15:         {
16:             //affiche ou initialise vect de maniere aleatoire
17:             if (qrand()%2)
18:             {
19:                 qDebug() << "Thread A essai lecture  ";
20:                 //lock en lecture
21:                 QReadLocker rl(&lock);
22:                 QString s;
23:
24:                 foreach(int i ,vect)
25:                 {
26:                     s += QString::number(i) + " ";
27:                 }
28:
29:                 qDebug() << "Thread A : " <<s;
30:                 msleep(100);
31:             }
32:             else
33:             {
34:                 qDebug() << "Thread A essai ecriture  ";
35:                 //lock en ecriture
36:                 QWriteLocker wl(&lock);
37:
38:                 int id = 0;
39:                 for( int i = 0 ; i < vect.size() ; ++i)
40:                 {
41:                     vect[i] = id++;
42:                 }
43:                 qDebug() << "Thread A as ecrit";
44:                 msleep(100);
45:             }
46:         }
47:     }
48: };
49:
50: /*thread A : affiche le vect ou l'initialise avec une suite aléatoire*/
51: class threadB : public QThread
52: {
53:
54: protected :
55:     void run()
56:     {
57:         forever
58:         {
59:
60:             if (qrand() % 2)
61:             {
62:                 qDebug() << "\tThread B essai lecture  ";
63:                 //lock en lecture
64:                 QReadLocker rl(&lock);
65:                 QString s;
66:                 foreach(int i ,vect)
67:                 {
68:                     s += QString::number(i) + " ";
69:                 }
70:                 qDebug() << "\tThread B : " <<s;
71:                 msleep(100);
72:             }
73:             else
74:             {
75:                 qDebug() << "\tThread B essai ecriture  ";
76:                 //lock en ecriture
77:                 QWriteLocker wl(&lock);
78:
79:                 for( int i = 0 ; i < vect.size() ; ++i)
80:                 {
81:                     vect[i] = qrand()%100;
82:                 }
83:                 qDebug() << "\tThread B as ecrit";
84:                 msleep(100);
85:             }
86:         }
87:     }
88: };
89: int main(int argc, char **argv)
90: {
91:     //creation et lancement des threads
92:     threadA a; a.start();
93:     threadB b; b.start();
94:
95:     //attente de la fin d'execution des deux threads
96:     a.wait(); b.wait();
97: }

Semaphore

Le sémaphore est une généralisation du mutex. Contrairement au mutex qui sert à protéger une seule ressource, le semaphore est une protection d'un ensemble de ressources de même type. on retrouve des notions équivalente :

  • aquire : aquisition de N ressources. Fonction bloquante jusqu'à la possibilité d'aquerire N ressources
  • release : libération de N ressources. Si l'on libère plus de ressources que convenue, le nombre maximum de ressources protégées par le sémaphore augmente.
  • tryAcquire : essaie d'aquisition de N ressources.

Contrairement au mutex, la manipulation d'un sémaphore est répartie entre toute les threads. C'est à dire qu'un thread peut demander l'aquisition et un autre thread la libèration. Cette méthode, plus difficile à comprendre, est potentiellement plus rapide, car l'accés en lecture/écriture à l'ensemble des ressources peut être différente pour chaque ressources.

Exemple d'un buffer circulaire entre deux thread où la ressource correspond à un élément du buffer. Pour cela on va utiliser deux sémaphores pour connaitre le nombre de “ressources libres” et de “ressources utilisées”:

  • ressourceUtilisable : Sémaphore qui permet d'aquérire des ressources libre que l'on peut utiliser
  • ressourceLibrable : Sémaphore qui permet d'aquérire des ressources utilisé que l'on peut liberer

Les deux thread vont se partager ces sémaphores pour protéger les accés au buffer circulaire :

  • thread A :
    1. Acquisition de ressource que l'on peut utiliser ⇒ on veut écrire N données dans le buffer
    2. écriture des données dans le buffer
    3. Mise a jour du nombre de ressources libérable ⇒ N données ont été écrit
  • thread B :
    1. Acquisition de ressource que l'on peut libérer ⇒ on veut lire N données dans le buffer
    2. Lecture et traitement des données dans le buffer
    3. Mise a jour du nombre de ressources Utilisable ⇒ N données ont été lue

Le point important sont les étapes 1 et 3 des thread qui s'oppose!!! ce qui permet la gestion du bocage des threads Bloquage de A

  1. A veut écrire N données dans le buffer et demande une aquisition N ressources à ressourceUtilisable , seulement dans le buffer il reste n'y as pas assez de place et ressourceUtilisable bloque A
  2. B lie X données et met à jour ressourceUtilisable en lui indiquant que X ressources de plus sont libre. Dés qu'il y as assez de ressources demandé par A, A se débloque

Bloquage de B

  1. B veut Lire N données dans le buffer et demande une aquisition N ressources à ressourceLiberable , seulement dans le buffer il n'y as pas assez de données écrite et ressourceLiberable bloque B
  2. A Ecrit X données et met à jour ressourceLiberable en lui indiquant que X ressources de plus sont libre. Dés qu'il y as assez de ressources demandé par B, B se débloque
  1: #include <QtCore>
  2: #include <iostream>
  3:
  4: //taille du buffer circulaire
  5: const int bufferSize = 1000;
  6: //buffer circulaire de taille fixes
  7: int   buffer[bufferSize];
  8:
  9: //booleen pour stopper l'execution aprés l'appuie de la touche enter
 10: bool  stopThread = false;
 11:
 12: //nombre de ressources utilisable
 13: //limite à la taille du buffer circulaire
 14: QSemaphore ressourceUtilisable  (bufferSize);
 15:
 16: //nombre de ressources librable.
 17: //Au debut, aucune donnees de peut être libérées
 18: QSemaphore ressourceLiberable    ;
 19:
 20: //Demande à ressourceUtilisable la possibiliter d'utiliser n ressources
 21: void AcquisitionNRessources(qint32 n)
 22: {
 23:     ressourceUtilisable.acquire(n);
 24: }
 25: //Mise a jour du nombre de ressources liberable
 26: void NRessourcesUtilisees(qint32 n)
 27: {
 28:     ressourceLiberable.release(n);
 29: }
 30:
 31: //thread A : écrit dans le buffer
 32: //Aquit N ressources
 33: //Ecrire dans le buffer
 34: //Met à jour le nombre de ressources liberable
 35: class threadA : public QThread
 36: {
 37:
 38: protected :
 39:     void run()
 40:     {
 41:         qsrand( QTime().secsTo(QTime::currentTime()) );
 42:         int idBuffer(0);
 43:         while(!stopThread)
 44:         {
 45:              int nbRessource = 1 + qrand() % (bufferSize / 10);
 46:              qDebug()<<"Thread A va aquerir "<<nbRessource<<" ressources";
 47:              AcquisitionNRessources(nbRessource);
 48:              qDebug()<<"Thread A ecrit dans le buffer ["
 49:                         <<idBuffer<<" , "<< (idBuffer + nbRessource) % bufferSize << "]";
 50:              for (int i = 0; i < nbRessource; ++i)
 51:              {
 52:                 buffer[idBuffer] = qrand() % 100;
 53:                 idBuffer = (idBuffer +1) % bufferSize;
 54:              }
 55:              msleep(500 );
 56:              qDebug()<<"Thread A met a jour le nombre de ressources liberables";
 57:              NRessourcesUtilisees(nbRessource);
 58:         }
 59:     }
 60: };
 61:
 62: //Demande à ressourceLiberable la possibiliter de liberer n ressources
 63: void LibererationNRessources(qint32 n)
 64: {
 65:     ressourceLiberable.acquire(n);
 66: }
 67:
 68: //Mise a jour du nombre de ressources utilisable
 69: void NRessourcesLiberees(qint32 n)
 70: {
 71:     ressourceUtilisable.release(n);
 72: }
 73:
 74: // Thread B :lit dans le buffer
 75: //Libere N ressources
 76: //lit dans le buffer
 77: //Met à jour le nombre de ressources utilisable
 78: class threadB : public QThread
 79: {
 80: protected :
 81:     void run()
 82:     {
 83:         qsrand( QTime().secsTo(QTime::currentTime()) );
 84:         int idBuffer(0);
 85:         while(!stopThread)
 86:         {
 87:
 88:             int nbRessource =1+ qrand() % (bufferSize / 10);
 89:
 90:              qDebug()<<"\tThread B va liberer "<<nbRessource<<" ressources";
 91:              LibererationNRessources(nbRessource);
 92:
 93:               qDebug()<<"\tThread B lit dans le buffer ["
 94:                         <<idBuffer<<" , "<< (idBuffer + nbRessource) % bufferSize << "]";
 95:              for (int i = 0; i < nbRessource; ++i)
 96:              {
 97:
 98:                 idBuffer = (idBuffer +1)%bufferSize;
 99:              }
100:              msleep(500 );
101:
102:              NRessourcesLiberees(nbRessource);
103:              qDebug()<<"\tThread B as libere "<<nbRessource<<" ressources";
104:         }
105:     }
106: };
107: int main(int , char **)
108: {
109:     //creation et lancement des threads
110:     threadA a; a.start();
111:     threadB b; b.start();
112:
113:     std::string s;
114:     std::getline(std::cin , s);
115:     stopThread = true;
116:     //attente de la fin d'execution des deux threads
117:     a.wait(); b.wait();
118: }

On constate que les deux threads utilisent à chaque instant des zones totalement différente du buffer circulaire et qu'ils s'éxécute bien en parralèlle.

Remarque : Le sémaphore est sousmis au même type problème du multi threading.Dans l'exemple, l'aquisition de ressources est limitées pour éviter un l'interblocage des deux threads :

  • à un instant t, A demand Na ressources et B demande Nb ressources.
  • si Na+Nb > taille du buffer, la quatité de ressources que A et B essaie d'exploiter est supèrieur à la capacité du buffer et les deux threads vont se bloquer multuellement.

Pour tester, il vous suffit de remplacer les lignes int nbRessource =1+ qrand() % (bufferSize / 10); par int nbRessource =1+ qrand() % bufferSize ; Au lancement vous verrez trés vite que les deux threads se retrouvent bloqués.

QWaitCondition

QThread

En sois même, QThread n'est pas un thread. QThread est une classe qui sert d'interface à un thread. C'est lui qui lance le thread et le manipule (connaitre son état, change sa priorité,…). Une instance de QThread n'est donc pas exécuté dans le thread. Excepté la fonction run. En effet lorsque QThread démarre un thread, il lui passe sa fonction run en paramètre pour lui faire exécuter. Le context effectif du thread se situe entre le début et la fin de la fonction run. Les principales fonctions sont :

  • void run() : méthode à réimplémenter qui sera exécute dans le thread.
  • int exec() : lance l'eventloop. Doit être appelé dans le run.
  • bool wait ( unsigned long time = ULONG_MAX ) : attend la fin de l'exécution d'un thread. Par défaut le timeout est infini.
  • void exit ( int returnCode = 0 ) : stop l'eventloop avec un code de retour.
  • [slot] void start () : lance un thread.
  • [slot] void quit () : appel exit(0).
  • [slot] void terminate() : stop brutalement le thread. A NE PAS UTILISER !!!!!

Cette classe permet aussi d'accéder à quelques information très pratique par quelques fonctions static :

  • currentThread et currentThreadId : permet de reconnaitre le thread courant
  • idealThreadCount : le nombre idéale par rapport à la machine
  • yieldCurrentThread : propose à l'OS de laisser les autres threads s’exécuter si besoin. Cela permet au thread de partager les ressources CPU qu'il utilise.
  • usleep,msleep et sleep : endors le thread pendant un temps donné.

Utilisation simplifié

C'est le fonctionnement de base d'un thread. Le thread ne fait qu'exécute la fonction run du QThread. Pour cela il suffit de créé une classe héritant du QThread et de redéfinir la méthode run.

Comme run fait partie de QThread, on peut accéder à ses fonction. Malheureusement comme le QThread ne s’exécute pas dans le thread, utiliser ces fonctions peuvent créer des accès concurrent. Ceci est donc à éviter voir à proscrire. Toute fois, il faut se rappeler que les signaux sont thread-safe et peuvent donc être exécuter par n'importe quel thread!! On peut donc utiliser les signaux définie pas la classe héritant de QThread pour emettre des signaux à partir su thread. Ce qui ne manque pas d'intêret

Pour résumer il faut éviter d'utiliser les fonctions du QThread dans le thread à l'éxécption des signaux qui eux sont thread-safe!!!.

Le calcul de nombre premier peut ainsi devenir

 1: class monThread : public QThread
 2: {
 3: Q_OBJECT
 4: int m_max;
 5: public :
 6: monThread(int max) : m_max(max){};
 7:
 8: signals :
 9:    //valeur entre 0 et 1 indiquant l'avancement du calcul
10:    void avancement(double);
11:    //Les nombre premier trouvés
12:    void resultat(const QVector<int> &);
13:
14:    protected :
15:    void run()
16:    {
17:        QVector<int> nombrePremier;
18:        nombrePremier <<1 << 2;
19:
20:       //copie de la valeur max.
21:       int max (m_max)
22:       for (int i = 3  ;  i < max;  i+=2)
23:       {
24:
25:             bool estNombrePremier(true);
26:             for (int j =1 ;  j < nombrePremier.size(); ++j)
27:             {
28:                 if( i % nombrePremier[j] == 0)
29:                 {
30:                     estNombrePremier = false;
31:                     break;
32:                 }
33:             }
34:            if (estNombrePremier)
35:            {
36:                 nombrePremier << i;
37:            }
38:
39:           emit avancement (1. * i / m_max);
40:       }
41:       emit avancement (1.);
42:       emit resultat(nombrePremier);
43:    }
44: }

EventLoop

Lors de son instanciation, un QObject est associé avec le QThread qui le construit. Ceci implique que cette objet devrait être utilisé uniquement pas ce QThread!!! Cette propriété est très importante surtout pour le système de signal/slot. En effet, lorsque la connexion est en mode automatique, si l'emmeteur et le receveur se sont pas associé avec le même QThread, un évènement est ajouté à l'eventloop du QThread associé au receveur. Ainsi l'exécution su slot sera sans le bon thread.

Il suffit alors de créer un QThread où tous les QObjects sont créent au début du run et terminé par l'appel à exec() pour lancer l'eventloop.

class monThread : public QThread
{
...
 
protected :
   void run()
   {
     //création des QObject;
     ...
     //création des connection;
     ...
     //lancement de l'eventloop
     exec()
 
   }
};

Par cette méthode, la création des connexion n'est pas évidente lorsque l'on doit connecté des QObject du thread avec des QObject externe et amène souvent à des erreur de programmation. L'association d'un QObject avec un QThread peut être modifié grâce à la méthode QObject::moveToThread(QThread *) qui peut simplifier grandement les choses et même rendre l'utilisation d'eventloop-QThread encore plus puissante.

EventLoop, QObject et moveToThread

signal - slot

L'utilisation d'une eventloop+QThread et de moveToThread permet de simplifier la connexion entre plusieurs QObjects et d'utiliser les QThread comme des espaces d'exécution des QObject. Pour cela il faut :

  • Lancer l'eventloop (appel exec()) dans le run des QThread. Depuis Qt 4.4, QThread lance par défaut une eventloop.
  • Implémenter vos traitements dans un QObject.
  • Utiliser moveToThread pour spécifier le thread d’exécution au QObject.

Les QObject utilisés doivent avoir certaine propriété :

  • les fonctions appelées entre les thread sont des slots qui ne retournent rien. Ils sont appelées aux travers d’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ée par un signal
  • Toutes les instances de QObject interne doivent avoir la classe comme parent (Ceci permet que tous ses QObject change de thread lors du moveToThread)

Cette façon de faire as aussi d’autre avantage :

  • Travailler en mono thread et pouvoir tester son code au maximum avant de passer au multi-threading.
  • Limite le nombre de thread à créer.
  • Équilibrage des charges sur les thread plus facile
  • Evite l’utilisation de mutex & co dans une majorité des cas.
  • Développement multi-thread assez propre et simplifié.

Astuce : 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. Il vous faut créer un QObject que ne définie que des signaux et dont la classe est déclaré comme amis (friends). Les signaux ont la même signature que les slots de la classe. Il suffit alors :

  • Connecter les signaux de cette classe avec les slots correspondants.
  • Vérifier à chaque début de slots du QObject, si le thread d’exécution et celui dont le QObject dépend. Si différent, appeler le signal correspondant définie 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 click sur l’image lnce une génération.

#include <QtGui>
#include <complex>
const int R_2 = 100000;
const int MAX_TEST = 255;
const int IMG_W = 800;
const int IMG_H = 800;
 
class Generator;
//class interne à generator. Utilisé pour déplacer l'appel d'un slot dans le bon thread.
class GMoveCallToThread : public QObject{
    Q_OBJECT
public :
        GMoveCallToThread(QObject * parent) :QObject(parent) {}
    //réplique les slots de Generator sous forme de signal
signals :
    void generateImage(const std::complex<double> & cc);
    void useThread(QThread *t);
    //generator doit pouvoir utiliser directement les signaux
    friend class Generator;
};
 
 
 
//class qui génère une image.
class Generator : public QObject
{
    Q_OBJECT
 
    //Instance utilisé pour déplacer l'appel d'un slot dans le bon thread.
    GMoveCallToThread  * m_moveCallToThread;
    //indique si un traitement est en cours ou non
    bool m_running;
 
    public :
    Generator(QObject * parent = 0):QObject(parent),m_moveCallToThread(new GMoveCallToThread(this)),m_running(false) {
        //on enregistre la classe std::complex<double>
        //pour l'utilisation des signal/slot en multi-thrread
        qRegisterMetaType<std::complex<double> >("std::complex<double>");
 
        //on connecte les slots avec leurs signaux correspondant
        connect(m_moveCallToThread,SIGNAL(generateImage(const std::complex<double> &)),this,SLOT(generateImage(const std::complex<double> &)));
        connect(m_moveCallToThread,SIGNAL(useThread(QThread *)),this,SLOT(useThread(QThread *)));
    };
 
    //spécifie si un traitement est en cours.
    bool running() {return m_running;}
 
public slots :
    //genère une image de julian avec cc comme nombre constant.
    void generateImage(const std::complex<double> & cc)
    {
        //Si l'appel est dans le mauvais thread,
        //on déplace l'appel vers le bon thread.
        if(QThread::currentThread() != thread())
        {
            m_moveCallToThread->generateImage(cc);
            return;
        }
        m_running = true;
 
        //Calcule de l'image de julian
        QImage img(IMG_W,IMG_H,QImage::Format_ARGB32);
        img.fill(0);
 
        int max = 0;
        QTime t;t.start();
        for(int i = 0; i < IMG_H / 2; ++i)
        {
            for(int j = 0; j < IMG_W; ++j)
            {
                std::complex<double> c = cc ;
                std::complex<double> z (
                                -2. + 4. * j / (IMG_W - 1),
                                 2. - 4. * i / (IMG_H - 1) );
                int nbTest = 0;
                while(std::abs(z) < R_2 && nbTest++ < MAX_TEST )
                {
                    z = z*z + c;
                }
                if(nbTest< MAX_TEST )
                {
                    if (nbTest > max) max = nbTest;
                    img.setPixel(j, i, nbTest);
                    img.setPixel((IMG_W - 1 - j) , (IMG_H - 1 - i) , nbTest);
                }
            }
            //si 40 ms est passé on envoye une image temporaire
            if(t.elapsed()>40)
            {
                emit tmpresult(img,max);
                t.restart();
            }
        }
        //envoie l'image finale
        emit result(img,max);
        m_running = false;
    }
 
    //change l'appartenance du générator à un thread.
    void useThread(QThread *t)
    {
        //Si l'appel est dans le mauvais thread,
        //on déplace l'appel vers le bon thread.
        if(QThread::currentThread() != thread())
        {
            m_moveCallToThread->useThread(t);
            return;
        }
        moveToThread(t);
    }
signals:
    //envoie de l'image finale.
    void result(const QImage &,int);
    //envoie de l'image intermédiaire.
    void tmpresult(const QImage &,int);
};
 
 
 
 
 
 
 
 
//widget d'affichage.
class maWidget : public QWidget
{
    Q_OBJECT
    //affiche la valeur complex en fonction de la position de la souris.
    QLabel * m_c;
    //affiche l'image
    QLabel * m_imgDisplay;
 
    //complexe d'initialisation utilisé pour la génération
    std::complex<double> m_z0;
    //Generateur.
    Generator m_gen;
    //thread secondaire.
    QThread m_thread;
 
public :
    maWidget()
    {
        QVBoxLayout * layout = new QVBoxLayout(this);
        {
            //checkbox permettant de spacifie rle thread d'éxécution du gnérateur
            QCheckBox * cb = new QCheckBox("MoveToThread");
            connect (cb,SIGNAL(toggled(bool)),this,SLOT(moveThread(bool)));
            layout->addWidget(cb);
 
            m_c = new QLabel("<b>Cliquer sur l'image</b>");
            layout->addWidget(m_c);
 
            m_imgDisplay = new QLabel();
            m_imgDisplay->setMouseTracking(true);
            m_imgDisplay->setPixmap(QPixmap(IMG_W,IMG_H));
            m_imgDisplay->installEventFilter (this);
            layout->addWidget(m_imgDisplay);
            connect(&m_gen,SIGNAL(result(const QImage &,int)), this, SLOT(displayResult(const QImage &,int)));
            connect(&m_gen,SIGNAL(tmpresult(const QImage &,int)), this, SLOT(displayIntermediaire(const QImage &,int)));
        }
        //on lance le thread secondaire.
        m_thread.start();
    }
    //filter les evenement souris du label qui affiche l'image
    bool eventFilter(QObject *obj, QEvent *event)
    {
        if(event->type() == QEvent::MouseMove)
        {
            //si la souris bouge, on affiche la valeur complex corespondante à la position de la souris.
            QMouseEvent * me = (QMouseEvent *)event;
            std::complex<double> c(-2. + 4.* me->x()/(IMG_W - 1),2. - 4.* me->y()/(IMG_H - 1));
            m_c->setText(QString("<b>Cliquer sur l'image</b> : { %1 , %2 }").arg(c.real()).arg(c.imag()));
        }
        else if(event->type() == QEvent::MouseButtonRelease && !m_gen.running())
        {
            //Si le boutton est relaché et que le générateur est inactif, on lance une nouvelle génération.
            QMouseEvent * me = (QMouseEvent *)event;
            m_z0 = std::complex<double> (-2. + 4.* me->x() / (IMG_W - 1),
                                          2. - 4.* me->y() / (IMG_H - 1));
            m_gen.generateImage(m_z0);
        }
        return QObject::eventFilter(obj, event);
    }
 
    ~maWidget()
    {
        //on stop le thread et on attend sa fin.
        m_thread.quit ();
        m_thread.wait();
    }
 
//convertie une image reçu en QPixmap à afficher.
QPixmap imgtoPixmap(QImage img,int max)
{
    //Creation d'une LUT pour convertir les valeurs uint des pixels de img en couleur.
    QColor c(Qt::blue);
    QVector<quint32> LUT(max+1);
    LUT[0] = qRgba(0,0,0,255);
    for(int i =1; i < LUT.size();++i) {
        double p =  pow(double(i) / max, .9);
        if(p>1.) p =1.;
        c.setHslF(c.hueF(), c.saturationF(), pow(p,.5));
        LUT[i] = c.rgba();
    }
 
    //Application de la LUT sur les pixel
    for(int i = 0; i < img.height(); ++i)
    {
        for(int j = 0; j < img.width(); ++j)
        {
            img.setPixel(j,i, LUT[img.pixel(j,i)]);
        }
    }
    //Affiche la valeur complex d'initialisation utilisée.
    {
        QPainter p(&img);
        p.setPen(Qt::red);
        p.drawText(5,15,QString("{ %1 , %2 }").arg(m_z0.real()).arg(m_z0.imag()));
    }
    return QPixmap::fromImage(img);
}
 
public slots :
    //affiche l'image résultat reçu.
    void displayResult(const QImage & img,int max)
    {
        m_imgDisplay->setPixmap(imgtoPixmap(img, max));
    }
 
    //affiche l'image intermédiaire reçu.
    void displayIntermediaire(const QImage & img,int max)
    {
        m_imgDisplay->setPixmap(imgtoPixmap(img, max));
    }
 
    //change le thread du générateur.
    void moveThread(bool b)
    {
        if(b)
        {
            m_gen.useThread(&m_thread);
        }
        else
        {
             m_gen.useThread(thread());
        }
    }
};
 
#include "main.moc"
int main(int argc, char *argv[])
{
    //initialisation de la graine aléatoire.
    qsrand(QTime::currentTime().msecsTo(QTime()));
    QApplication a(argc, argv);
    maWidget w;
    w.show();
 
    return a.exec();
}

Remarque : on pourrais ê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 poserai 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.

les events

les states machines

QThread Pool et QRunnable

Lorsque que l'on doit éxécuter de petit morceau de code très régulièrement, on essaie d'éviter la création de thread et de réutiliser les threads. Pour cela Qt propose les classes :

  • QRunnable : correspond au code à exécuter.
  • QThreadPool : permet de créer/supprimer des threads et gère une file d'attente de QRunnable.

Lorsque l'on veux exécuter du code dans un thread d'un QThread pool, il faut créé un QRunnable et de ré-implémenter la fonction run.

class monCode : public QRunnable
{
   protected :
   void run()
   {
      //le code à éxécuter
   }
};

Par défaut un QRunnable doit être instancié par un new et est détruit à la fin de leur exécution. Toute fois, QRunnable possède la fonction setAutoDelete qui permet de spécifier que c'est vous qui vous chargez de sa destruction (Attention à sa durée de vie)

Pour éxécuter un QRunnable QThreadPool propose deux fonctions :

  • start(QRunnable*, int priority = 0) : ajoute le QRunnable dans la file d'attente. Le paramètre priority permet de spécifier la priorité d'éxécution.
  • tryStart(QRunnable*) : essaie de lancer un QRunnable. Si aucun thread n'est libre le runnable n'est pas éxécuté et la fonction retourne faux.

Code d'exemple

QtConcurrent

http://doc.qt.nokia.com/4.7-snapshot/qtconcurrent.html Lors du lancement d'un application, Qt alloue un pool de thread pas défaut adapté au nombre de coeur de la machine. Les QtConcurrents sont des algorithms style STL qui parallélise leurs exécutions grâce au pool de thread globale. C'est une API de haut niveau qui permet de faire abstraction des threads. Ils sont adaptés à un besoin ponctuelle de thread. Par exemple la création de preview d'un ensemble d'image. Pour bien utiliser les QtConcurrent il y as quelques classes importante à connaitre :

  • QFuture : cette classe permet de savoir si l’exécution de l'algorithme est termine et de récupérer le résultat.
  • QFutureIterator : iterateur sur les valeurs résultat stocké dans un QFuture
  • QFutureSynchronizer : synchronize la fin d'une liste de QFuture. Sert de points de synchronisation.
  • QFutureWatcher : Ajoute une interface signal/slot à un QFuture
  • QtConcurrent::Exception : une exception qui est transféré entre les thread et permet d'être catcher dans le thread appelant.

Dans la version 4.7 on trouve principalement trois type d'algorithmes.

Les algorithmes qui filtre les éléments d'une séquence (QVector,QList,std::vector,…) par un prédicat.

  • QtConcurrent::filter : enlève les éléments de la séquence.
#include <QtCore>
 
struct predicat
{
    bool operator()(const QString &s) {
        return s[0].isUpper();
    }
};
 
int main(int argc, char *argv[])
{
    QCoreApplication app(argc,argv);
    QStringList myList =  QStringList() <<"foo"<<"Bar"<<"DVP"<<"qt";
    qDebug() <<"avant : " << myList;
    QFuture<void> f = QtConcurrent::filter(myList,predicat());
    f.waitForFinished();
    qDebug() <<"apres : " << myList;
    return 0;
}
  • QtConcurrent::filtered : copie les éléments d'une séquence ou sous séquence(rang d'itérateur) vérifiant le prédicat.
#include <QtCore>
 
struct predicat
{
    bool operator()(const QString &s) {
        return s[0].isUpper();
    }
};
 
int main(int argc, char *argv[])
{
 
    QCoreApplication app(argc,argv);
    QStringList myList =  QStringList() <<"foo"<<"Bar"<<"DVP"<<"qt";
    qDebug() <<"avant : " << myList;
    QFuture<QString> f = QtConcurrent::filtered(myList,predicat());
    f.waitForFinished();
    qDebug() <<"res : " << f.results();
    return 0;
}
  • QtConcurrent::filteredReduced : regroupe les éléments d'une séquence ou sous séquence(rang d'itérateur) vérifiant le prédicat vers un seul objet. Cette méthode utilise un deusième foncteur pour le regroupement.
#include <QtCore>
 
struct predicat
{
    bool operator()(const QString &s) {
        return s[0].isUpper();
    }
};
 
struct reduce
{
    void operator()(QString &res, const QString &s) {
       res.append(" ").append(s);
    }
};
int main(int argc, char *argv[])
{
 
    QCoreApplication app(argc,argv);
    QStringList myList =  QStringList() <<"foo"<<"Bar"<<"DVP"<<"qt";
    qDebug() <<"avant : " << myList;
    QFuture<QString> f = QtConcurrent::filteredReduced<QString>(myList,predicat(),reduce());
    f.waitForFinished();
    qDebug() <<"res : " << f.result();
    return 0;
}

Les algorithmes qui applique un foncteur sur les éléments d'une séquence (QVector,QList,std::vector,…).

  • QtConcurrent::map : applique un foncteur sur les éléments d'une séquence ou sous séquence(rang d'itérateur). Ressemble à un std::for_each.
#include <QtCore>
 
struct foncteur
{
    void  operator()( QString &s) {
        s = s.toUpper();
    }
};
 
int main(int argc, char *argv[])
{
 
    QCoreApplication app(argc,argv);
    QStringList myList =  QStringList() <<"foo"<<"Bar"<<"DVP"<<"qt";
    qDebug() <<"avant : " << myList;
    QFuture<void> f = QtConcurrent::map(myList,foncteur());
    f.waitForFinished();
    qDebug() <<"apres  : " <<myList;
    return 0;
}
  • QtConcurrent::mapped : applique un foncteur sur les éléments d'une séquence ou sous séquence(rang d'itérateur) et créé une séquence avec les résultats du foncteur. Ressemble à un std::transforme
#include <QtCore>
 
struct foncteur
{
    typedef QString result_type;
    QString  operator()(const QString &s) {
       return s.toUpper();
    }
};
 
int main(int argc, char *argv[])
{
 
    QCoreApplication app(argc,argv);
    QStringList myList =  QStringList() <<"foo"<<"Bar"<<"DVP"<<"qt";
    qDebug() <<"avant : " << myList;
    QFuture<QString> f = QtConcurrent::mapped(myList,foncteur());
    f.waitForFinished();
    qDebug() <<"results  : " <<f.results();
    return 0;
}
  • QtConcurrent::mappedReduced : applique un foncteur sur les éléments d'une séquence ou sous séquence(rang d'itérateur) et regroupe les résultats du foncteur vers un seul objet. Cette méthode utilise un deuxième foncteur pour le regroupement.
#include <QtCore>
 
struct foncteur
{
    typedef QString result_type;
    QString  operator()(const QString &s) {
       return s.toUpper();
    }
};
 
struct reduce
{
    void operator()(QString &res, const QString &s) {
       res.append(" ").append(s);
    }
};
int main(int argc, char *argv[])
{
 
    QCoreApplication app(argc,argv);
    QStringList myList =  QStringList() <<"foo"<<"Bar"<<"DVP"<<"qt";
    qDebug() <<"avant : " << myList;
    QFuture<QString> f = QtConcurrent::mappedReduced<QString>(myList,foncteur(),reduce());
    f.waitForFinished();
    qDebug() <<"result  : " <<f.result();
    return 0;
}

Le dernier est l'algorithmes QtConcurrent::run qui permet d’exécuter une méthode ou une méthode d'une instance dans un thread. Il est aussi possible de passer des paramètres à la méthodes.

Toute ces méthodes retournent un QFuture. Toute fois, les algorithmes qui parcoure une séquence ou sous séquence (rang d'itérateur) propose des version alternative nommé blokingXXX avec XXX le nom de l'algorithme. Ces versions attentent la fin de l’exécution de l’algorithme et retourne le résultat.

Voici un exemple complet

#include <QtGui>
#include <complex>
#include <algorithm>
 
const int R_2 = 100000;
const int MAX_TEST = 255;
const int IMG_W = 800;
const int IMG_H = 800;
 
struct zone
{
    QSize  pixelZone;
    QRect  subPixelZone;
 
    zone( const QSize   &pixelZone,
          const QRect  & subPixelZone)
     :pixelZone(pixelZone),subPixelZone(subPixelZone)
    {}
    zone( const zone & z)
     :pixelZone(z.pixelZone),subPixelZone(z.subPixelZone)
    {}
    zone(){}
};
 
struct zoneInfo
{
    zone  z;
    int   max;
    zoneInfo( const zone & z,int max)
     :z(z),max(max)
    {}
    zoneInfo():max(0){}
};
struct Operator
{
typedef  std::pair<QImage,zoneInfo> result_type;
Operator(const std::complex<double> &c ):c(c){}
 
 std::complex<double> c ;
std::pair<QImage,zoneInfo> operator()(const zone &z)
{
    QImage img(z.subPixelZone.size(),QImage::Format_ARGB32);
    int max = 0;
    const int ib = z.subPixelZone.top();
    const int ie = z.subPixelZone.bottom()+1;
    const int jb = z.subPixelZone.left();
    const int je = z.subPixelZone.right()+1;
 
    for(int i = ib; i < ie; ++i)
    {
        for(int j = jb; j < je; ++j)
        {
           std::complex<double> z (
                           -2. + 4. * j / (IMG_W - 1),
                            2. - 4. * i / (IMG_H - 1) );
           int nbTest = 0;
           while(std::abs(z) < R_2 && nbTest++ < MAX_TEST )
           {
               z*=z;
               z+=c;
 
           }
           if(nbTest< MAX_TEST )
           {
               if (nbTest > max) max = nbTest;
               img.setPixel(j-jb, i-ib, nbTest);
           }
           else
           {
               img.setPixel(j-jb, i-ib, 0);
           }
       }
 
    }
    return std::make_pair(img,zoneInfo(z,max));
 
}
};
 
 
 
 
 
//widget d'affichage.
class maWidget : public QWidget
{
    Q_OBJECT
    //affiche la valeur complex en fonction de la position de la souris.
    QLabel * m_c;
    //affiche l'image
    QLabel * m_imgDisplay;
 
    //complexe d'initialisation utilisé pour la génération
    std::complex<double> m_z0;
 
    QFutureWatcher<Operator::result_type> m_watcher;
    QList<zone> lz;
    QImage     m_res;
    int max;
    QTimer m_t;
 
public :
    maWidget()
        :m_res(IMG_W,IMG_H,QImage::Format_ARGB32),max(0)
    {
        m_res.fill(0);
        QVBoxLayout * layout = new QVBoxLayout(this);
        {
            m_c = new QLabel("<b>Cliquer sur l'image</b>");
            layout->addWidget(m_c);
 
            m_imgDisplay = new QLabel();
            m_imgDisplay->setMouseTracking(true);
            m_imgDisplay->setPixmap(QPixmap(IMG_W,IMG_H));
            m_imgDisplay->installEventFilter (this);
            layout->addWidget(m_imgDisplay);
            connect(&m_watcher,SIGNAL(resultReadyAt(int)), this, SLOT(addResult(int)));
            connect(&m_watcher,SIGNAL(finished()), this, SLOT(display()));
        }
 
        for (int i =0; i<IMG_H; i+=10)
        {
            for(int j =0; j<IMG_W; j+=10)
            {
                lz << zone(
                            QSize (IMG_H,IMG_W),
                            QRect(j, i, 10, 10)
                           );
            }
 
        }
        std::random_shuffle(lz.begin(),lz.end());
        m_t.setInterval(100);
        m_t.setSingleShot(false);
         connect(&m_t,SIGNAL(timeout()), this, SLOT(display()));
          connect(&m_watcher,SIGNAL(finished()), &m_t, SLOT(stop()));
          connect(&m_watcher,SIGNAL(canceled()), &m_t, SLOT(stop()));
    }
    //filter les evenement souris du label qui affiche l'image
    bool eventFilter(QObject *obj, QEvent *event)
    {
        if(event->type() == QEvent::MouseMove)
        {
            //si la souris bouge, on affiche la valeur complex corespondante à la position de la souris.
            QMouseEvent * me = (QMouseEvent *)event;
            std::complex<double> c(-2. + 4.* me->x()/(IMG_W - 1),2. - 4.* me->y()/(IMG_H - 1));
            m_c->setText(QString("<b>Cliquer sur l'image</b> : { %1 , %2 }").arg(c.real()).arg(c.imag()));
        }
        else if(event->type() == QEvent::MouseButtonRelease)
        {
            m_watcher.cancel();
            m_res.fill(0);
            max = 0;
            m_t.start();
             m_imgDisplay->setPixmap(QPixmap(IMG_W,IMG_H));
            //Si le boutton est relaché et que le générateur est inactif, on lance une nouvelle génération.
            QMouseEvent * me = (QMouseEvent *)event;
            m_z0 = std::complex<double> (-2. + 4.* me->x() / (IMG_W - 1),
                                          2. - 4.* me->y() / (IMG_H - 1));
 
            m_watcher.setFuture(QtConcurrent::mapped(lz,Operator(m_z0)));
        }
        return QObject::eventFilter(obj, event);
    }
 
    ~maWidget()
    {
 
    }
 
//convertie une image reçu en QPixmap à afficher.
QPixmap imgtoPixmap(QImage img,int max)
{
    if(max ==0)
        return QPixmap();
    //Creation d'une LUT pour convertir les valeurs uint des pixels de img en couleur.
    QColor c(Qt::blue);
    QVector<quint32> LUT(max+1);
    LUT[0] = qRgba(0,0,0,255);
    for(int i =1; i < LUT.size();++i) {
        double p =  pow(double(i) / max,.5);
        if(p>1.) p =1.;
        c.setHslF(c.hueF(), c.saturationF(), p);
        LUT[i] = c.rgba();
    }
 
    //Application de la LUT sur les pixel
    for(int i = 0; i < img.height(); ++i)
    {
        for(int j = 0; j < img.width(); ++j)
        {
            img.setPixel(j,i, LUT[img.pixel(j,i)]);
        }
    }
    //Affiche la valeur complex d'initialisation utilisée.
    {
        QPainter p(&img);
        p.setPen(Qt::red);
        p.drawText(5,15,QString("{ %1 , %2 }").arg(m_z0.real()).arg(m_z0.imag()));
    }
    return QPixmap::fromImage(img);
}
 
public slots :
    //affiche l'image résultat reçu.
    void addResult(int id)
    {
    Operator::result_type res = m_watcher.future().resultAt(id);
 
    if(res.second.max > max)
    {
         max = res.second.max;
    }
    for(int i = 0; i < res.second.z.subPixelZone.height(); ++i)
    {
        for(int j = 0; j <  res.second.z.subPixelZone.width(); ++j)
        {
            m_res.setPixel(res.second.z.subPixelZone.left()+j,
                               res.second.z.subPixelZone.top()+i,
                               res.first.pixel(j,i));
        }
    }
    }
 
    //affiche l'image intermédiaire reçu.
    void display()
    {
 
        m_imgDisplay->setPixmap(imgtoPixmap(m_res, max));
    }
 
 
};
 
 
 
 
 
 
//convertie une image reçu en QPixmap à afficher.
QPixmap imgtoPixmap(QImage img,int max)
{
    //Creation d'une LUT pour convertir les valeurs uint des pixels de img en couleur.
    QColor c(Qt::blue);
    QVector<quint32> LUT(max+1);
    LUT[0] = qRgba(0,0,0,255);
    for(int i =1; i < LUT.size();++i) {
        double p =  double(i) / max;
        if(p>1.) p =1.;
        c.setHslF(c.hueF(), c.saturationF(), p);
        LUT[i] = c.rgba();
    }
 
    //Application de la LUT sur les pixel
    for(int i = 0; i < img.height(); ++i)
    {
        for(int j = 0; j < img.width(); ++j)
        {
            img.setPixel(j,i, LUT[img.pixel(j,i)]);
        }
    }
    return QPixmap::fromImage(img);
}
 
#include "main.moc"
int main(int argc, char *argv[])
{
    //initialisation de la graine aléatoire.
        qsrand(QTime::currentTime().msecsTo(QTime()));
        QApplication a(argc, argv);
        maWidget w;
        w.show();
 
        return a.exec();
}

Autres Outils pour le Multithreading en Qt

Il existe encore trois petite classe qui intéressera les plus expérimenté :

  • QAtomicInt :classe qui fournie un ensemble d'opérateur atomique sur un int.
  • QAtomicPointeur :classe qui fournie un ensemble d'opérateur atomiqe sur un pointeur.
  • QThreadLocale : stocke une données différent pour chaque thread.

melange de thread

Conclusion

 
start.txt · Dernière modification: 2011/03/16 09:15 par yan