Catégorie : Programmation     Tags :      

    Retrouvez cet article dans : Linux Magazine 88

    À l’heure où les deux principaux fondeurs de processeurs produisent et annoncent des puces intégrant toujours plus de cœurs – hier deux, aujourd’hui quatre, demain huit ? – encore bien peu de logiciels tirent pleinement parti de cette puissance nouvellement disponible. Nous allons voir ici comment la bibliothèque Qt nous simplifie la tâche pour exploiter, justement, le multitâche au sein d’un programme.

    Un petit mot avant d’aborder notre sujet. Au moment où cet article est rédigé, TrollTech a diffusé une Technologie Preview (une « bande-annonce technologique ») de la version 4.2.0 de Qt. Celle-ci avance rapidement et devrait être disponible avant la fin de l’année (c’est actuellement la version utilisée par KDE4), mais tout n’est pas encore figé. En attendant la version définitive, nous nous appuierons sur la version 4.1.4 de Qt.

    Exemple sans interface graphique

    Bien que cela traduise un manque total d’imagination, considérons un programme qui calcule une image fractale, dans un premier temps sans aucun affichage. Le résultat du calcul sera simplement stocké dans un tableau interne. Pour cela, nous allons supposer disposer de quelques classes :

    • une classe abstraite Fractal, représentant l’équation d’un ensemble fractal, concrétisée par la classe Mandelbrot ;
    • une classe Fractal_Image, contenant le tableau du nombre d’itérations nécessaires pour obtenir une divergence pour chaque pixel.

    Cette structure est tout à fait similaire à celle de l’article consacré au parallélisme en Ada, dans Linux Magazine 87. Ainsi un programme élémentaire calculant une image complète (mais sans l’afficher pour l’instant) pourrait ressembler à ceci :

        1 #include <iostream>
        2 #include <complex>
        3 #include <QCoreApplication>
        4 #include <QTime>
        5 #include «mandelbrots.h»
        6 #include «fractal_images.h»
        7 int main(int argc, char* argv[])
        8 {
        9    QCoreApplication qcpp(argc, argv);
       10    Mandelbrot mandel(&qcpp);
       11    Fractal_Image fract_img(1024, 768, &qcpp);
       12    fract_img.Set_Fractal(&mandel);
       13    fract_img.Set_Domain(
       14          std::complex<double>(-1.18764, -0.30278),
       15          0.00283869);
       16    QTime chrono;
       17    chrono.start();
       18    for(unsigned y = 0; y < fract_img.height(); ++y)
       19        for(unsigned x = 0; x < fract_img.width(); ++x)
       20          fract_img.Compute_Pixel(x, y);
       21    int e = chrono.elapsed();
       22    std::cout << «Time: « << e/1000.0 << «\n»;
       23 }

    Pour fixer les idées, ce programme s’exécute en environ 15,67 secondes. Afin de tirer parti d’un système multiprocesseur (ou disposant de l’HyperThreading ou disposant d’un multi-cœur), on se propose de décomposer le calcul de l’image. Par exemple, un premier thread calculera la moitié supérieure, tandis qu’un second calculera la moitié inférieure.
    Au sein de Qt, un thread est représenté par une instance de la classe QThread (ou plutôt d’une classe dérivée). Les instructions à exécuter doivent être placées dans la méthode virtuelle (protégée) run(), le thread étant effectivement démarré par un appel à la méthode start(). Première règle : vous ne pouvez pas créer une instance de QThread (ou d’une classe dérivée) sans avoir préalablement créé une instance de QCoreApplication (ou d’une classe dérivée, comme QApplication).
    Voici la déclaration de notre type de thread :

        1 #ifndef __FRACT_THREADS_H__
        2 #define __FRACT_THREADS_H__ 1
        3 #include <QThread>
        4 class Fractal_Image;
        5 class Fract_Thread: public QThread
        6 {
        7    Q_OBJECT
        8    public:
        9       Fract_Thread(Fractal_Image* img,
       10                    QObject* parent = 0);
       11       void Set_Interval(const unsigned min_y,
       12                         const unsigned max_y);
       13    protected:
       14       virtual void run();
       15    private:
       16       Fractal_Image* _img;
       17       unsigned       _min_y;
       18       unsigned       _max_y;
       19 }; // class Fract_Thread
       20 #endif // __FRACT_THREADS_H__

    Remarquez la présence de la macro Q_OBJECT (ligne 7) ainsi que d’un objet parent passé au constructeur (ligne 10) : QThread dérive en effet de la classe QObject, ce qui ouvre la porte à quelques possibilités intéressantes que nous verrons plus loin.
    L’implémentation associée est très simple :

        1 #include «fractal_images.h»
        2 #include «fract_threads.h»
        3 Fract_Thread::Fract_Thread(Fractal_Image* img,
        4                            QObject* parent):
        5    QThread(parent),
        6    _img(img),
        7    _min_y(0),
        8    _max_y(img->height()-1)
        9 {
       10 }
       11 void Fract_Thread::Set_Interval(const unsigned min_y,
       12                                 const unsigned max_y)
       13 {
       14    _min_y = min_y;
       15    _max_y = max_y;
       16 }
       17 void Fract_Thread::run()
       18 {
       19    for(unsigned y = _min_y; y <= _max_y; ++y)
       20       for(unsigned x = 0; x < _img->width(); ++x)
       21          _img->Compute_Pixel(x, y);
       22 }

    On retrouve lignes 19 à 21 les deux boucles imbriquées calculant une portion de l’image. L’utilisation de cette classe est tout aussi simple dans ce cas quasiment trivial, la fonction main() ne devant être modifiée qu’au moment d’effectuer le calcul, c’est-à-dire juste après l’appel à Set_Domain() :

    ...
        7 #include “fract_threads.h”
    ...
       17    Fract_Thread th1(&fract_img, &qcpp);
       18    Fract_Thread th2(&fract_img, &qcpp);
       19    th1.Set_Interval(0, fract_img.height()/2-1);
       20    th2.Set_Interval(fract_img.height()/2,
       21                     fract_img.height()-1);

    On crée ici deux threads, sous la forme de deux instances de la classe Fract_Thread parentées à l’instance de l’application. À ce moment, les flots d’instructions contenus dans la méthode run() ne sont pas encore démarrés, les threads sont suspendus.

       22    QTime chrono;
       23    chrono.start();
       24    th1.start();
       25    th2.start();
       26    th1.wait();
       27    th2.wait();
       28    int e = chrono.elapsed();
       29    std::cout << «Time: « << e/1000.0 << «\n»;
       30 }

    Les deux threads sont démarrés par l’invocation de la méthode start() de QThread (lignes 23 et 24). Cette méthode retourne (presque) immédiatement le code contenu dans la méthode run() étant exécuté parallèlement au programme principal. start() prend un paramètre optionnel permettant de fixer la priorité du thread nouvellement lancé.
    Ensuite, on attend simplement la fin du calcul, c’est-à-dire que les deux threads achèvent leur exécution, ce qui survient lorsque la méthode run() se termine. L’attente est réalisée par la méthode wait() de QThread, qui va bloquer l’appelant jusqu’à ce que le thread concerné ne se termine. Un paramètre optionnel permet de limiter la durée de l’attente (exprimée en millisecondes), wait() retournant true ou false selon que le thread s’est effectivement terminé ou non.
    Dans cet exemple, la variable fract_img est partagée par les deux threads, procédé en général délicat en programmation parallèle. Or, aucun verrou n’est utilisé : cela fonctionne correctement « simplement » parce qu’il se trouve qu’à aucun moment, les deux threads ne vont accéder à la même zone mémoire. Nous verrons plus loin comment synchroniser (sérialiser) des accès concurrents.
    Ce programme s’exécute en environ 13,14 secondes, soit un gain de 16 %. Cela peut paraître faible, mais cela s’explique par le fait que le matériel utilisé ne dispose de rien de mieux que l’HyperThreading, qui ne fait que simuler deux processeurs là où il n’en existe qu’un seul.

    Ajout d’une interface

    Deuxième règle imposée par les threads dans Qt : seul le flot d’instructions principal, c’est-à-dire celui de la fonction main() et dans lequel est créée une instance de QApplication, est autorisé à effectuer des opérations graphiques. En clair, cela signifie que vous ne pouvez pas créer ou manipuler des widgets directement depuis la méthode run() de votre classe dérivée de QThread.
    Par contre, vous pouvez émettre et recevoir des signaux, ainsi qu’envoyer et recevoir des événements, QThread dérivant de QObject. C’est ainsi que nous allons pouvoir communiquer entre l’interface graphique et la partie purement calculatoire.
    Dans un premier temps, nous allons nous limiter à deux threads, l’application se limitant à un bouton pour lancer le calcul et à l’affichage de l’image résultante. La classe Fract_Thread est légèrement modifiée de façon à émettre un signal dès qu’une ligne est calculée, d’abord dans la déclaration :

    ...
       13    protected:
       14       virtual void run();
       15    signals:
       16       void drawLine(unsigned y);
       17    private:
    ...

    Puis dans l’implémentation de la méthode run() :

     ...
       18 void Fract_Thread::run()
       19 {
       20    for(unsigned y = _min_y; y <= _max_y; ++y)
       21    {
       22       for(unsigned x = 0; x < _img->width(); ++x)
       23          _img->Compute_Pixel(x, y);
       24       emit drawLine(y);
       25    }
       26 }

    Voyons maintenant l’affichage, réalisé par un simple widget déclaré ainsi :

        1 #ifndef __GUI_FRACT_H__
        2 #define __GUI_FRACT_H__ 1
        3 #include <QFrame>
        4 #include <QTime>
        5 class QPushButton;
        6 class QLabel;
        7 class QPixmap;
        8 class Fractal_Image;
        9 class GuiFract: public QFrame
       10 {
       11    Q_OBJECT
       12    public:
       13       GuiFract(Fractal_Image* fi,
       14                QWidget* parent = 0);
       15    public slots:
       16       void calc();
       17    protected slots:
       18       void drawLine(unsigned y);
       19       void threadFinished();
       20    private:
       21       Fractal_Image* _fract;
       22       QPushButton*   _bt_calc;
       23       QLabel*        _lbl_pix;
       24       QPixmap*       _pix;
       25       QTime          _chrono;
       26 }; // class GuiFract
       27 #endif // __GUI_FRACT_H__

    Le slot calc() (ligne 16) sera invoqué par le bouton. Le slot drawLine() ligne 18 sera invoqué par le signal éponyme de la classe Fract_Thread, tandis que le slot threadFinished() ligne 19 réagira à l’émission du signal finished(), défini dans la classe QThread, émis lorsqu’un thread a terminé son exécution – c’est-à-dire, lorsque la méthode run() est terminée. Voyons comment tout cela est implémenté :

        1 #include <iostream>
        2 #include <QtGui>
        3 #include <QVBoxLayout>
        4 #include <QPushButton>
        5 #include <QLabel>
        6 #include <QPixmap>
        7 #include <QPainter>
        8 #include «fractal_images.h»
        9 #include «fract_threads.h»
       10 #include «gui_fract.h»
       11 GuiFract::GuiFract(Fractal_Image* fi,
       12                    QWidget* parent):
       13    QFrame(parent),
       14    _fract(fi),
       15    _bt_calc(0),
       16    _lbl_pix(0),
       17    _pix(0)
       18 {
       19    _pix = new QPixmap(_fract->width(), _fract->height());
       20    _pix->fill(Qt::black);
       21    _lbl_pix = new QLabel(this);
       22    _lbl_pix->setPixmap(*_pix);
       23    _lbl_pix->setFixedSize(_pix->size());
       24    _bt_calc = new QPushButton(«Go !», this);
       25    QVBoxLayout* vbox = new QVBoxLayout(this);
       26    vbox->addWidget(_bt_calc);
       27    vbox->addWidget(_lbl_pix);
       28    connect(_bt_calc, SIGNAL(clicked()),
       29            this,     SLOT(calc()));
       30    qRegisterMetaType<unsigned>(«unsigned»);
       31 }

    Le constructeur met simplement en place l’interface après avoir préparé l’instance de QPixmap qui va contenir l’image. Le rôle exact de la fonction générique qRegisterMetaType<>(), ligne 30, sera bientôt explicité.

       32 void GuiFract::calc()
       33 {
       34    _bt_calc->setEnabled(false);
       35    Fract_Thread* th1 = new Fract_Thread(_fract, this);
       36    Fract_Thread* th2 = new Fract_Thread(_fract, this);
       37    th1->Set_Interval(0, _fract->height()/2-1);
       38    th2->Set_Interval(_fract->height()/2,
       39                      _fract->height()-1);

    Le début du slot calc() ressemble au lancement du calcul dans l’exemple sans interface : deux threads sont créés. Mais avant de les lancer, il convient d’établir quelques connexions :

       40    connect(th1,  SIGNAL(drawLine(unsigned)),
       41            this, SLOT(drawLine(unsigned)),
       42            Qt::QueuedConnection);
       43    connect(th2,  SIGNAL(drawLine(unsigned)),
       44            this, SLOT(drawLine(unsigned)),
       45            Qt::QueuedConnection);

    Pour commencer, on connecte le signal de fin de calcul d’une ligne. Remarquez le cinquième paramètre passé à connect(), ici Qt::QueuedConnection. C’est ce paramètre qui permet une communication sans problème entre threads.
    Dans les versions précédentes de Qt (avant la version 4.0), les connexions entre signaux et slots étaient directes, c’est-à-dire que l’émission d’un signal provoquait l’exécution immédiate des slots associés. Cela impliquait que les slots étaient exécutés dans le cadre du thread d’où le signal avait été émis. Ce qui n’allait évidemment pas sans poser des problèmes, étant donné que seul le thread principal est autorisé à effectuer des opérations graphiques.
    Ce mode de connexion existe toujours dans les versions actuelles de Qt, mais la version 4.0 a introduit la connexion différée. Dans ce cas, les slots ne sont invoqués qu’à l’exécution suivante de la boucle d’événements. Celle-ci existe normalement dans le thread principal. C’est donc dans ce cadre que les slots sont exécutés. C’est le sens du paramètre Qt::QueuedConnection. Pour une connexion directe, il faut utiliser Qt::DirectConnection. Essayez le programme en utilisant ce paramètre : normalement, il devrait planter assez rapidement.
    La valeur par défaut de ce cinquième paramètre est Qt::AutoConnection, qui détermine automatiquement quel type de connexion utiliser. Nous verrons en effet plus loin que chaque thread peut, en fait, disposer de sa propre boucle d’événements. Tout dépend alors dans quels threads émetteur et récepteur d’un signal ont été créés : s’il s’agit du même thread, alors la connexion est directe ; sinon, elle est différée.
    La connexion différée se traduit par une sorte de file d’attente des signaux, ce qui implique de devoir mémoriser les éventuelles données échangées – ici, simplement une valeur de type unsigned. Mais de par sa structure, Qt impose que seuls les types connus de son système de « méta-types » (ensembles de mécanismes permettant d’avoir de nombreuses informations sur un type durant l’exécution) ne peuvent être ainsi mémorisés. Le rôle de qRegisterMetaType<>() (invoqué ligne 30) est précisément d’ajouter un type à cette base de données de types interne à Qt : dès lors, nous pouvons échanger des signaux transportant des unsigned entre threads.
    Dans notre situation, le signal drawLine() est émis depuis l’un des deux threads th1 ou th2, mais reçu dans le thread principal, puisque c’est dans celui-ci que sera créée l’instance du widget GuiFract. Spécifier le cinquième paramètre à connect() est donc superflu, comme le montre les deux connexions suivantes :

       46    connect(th1,  SIGNAL(finished()),
       47            this, SLOT(threadFinished()));
       48    connect(th2,  SIGNAL(finished()),
       49            this, SLOT(threadFinished()));
       50    _chrono.start();
       51    th1->start();
       52    th2->start();
       53 }

    Ces connexions nous permettront d’être averti de la terminaison des calculs. Cela fait, les deux threads sont démarrés.

       54 void GuiFract::drawLine(unsigned y)
       55 {
       56    QColor col;
       57    QPainter p(_pix);
       58    for(unsigned x = 0; x < _fract->width(); ++x)
       59    {
       60       unsigned val = _fract->Get_Pixel(x, y);
       61       if ( val >= _fract->maxIters() )
       62          col.setHsv(0, 255, 0);
       63       else
       64          col.setHsv(val % 360, 255, 255);
       65       p.setPen(col);
       66       p.drawPoint(x, y);
       67    }
       68    _lbl_pix->setPixmap(*_pix);
       69    _lbl_pix->update();
       70 }

    Le slot drawLine() se contente de dessiner dans l’image _pix en fonction des informations contenues dans l’instance de Fractal_Image (c’est loin d’être le plus efficace, mais cela permet de ralentir l’exécution suffisamment pour voir quelque chose se passer). On pourrait s’inquiéter du fait que ce slot est déclenché par deux threads parallèles, donc s’attendre à des ennuis en cas d’accès concurrents à l’image. Mais du fait de la connexion différée, ce slot ne sera en fait invoqué qu’au sein du thread principal (par la boucle d’événements) : nous n’aurons donc pas deux actions graphiques simultanées.

       72 void GuiFract::threadFinished()
       73 {
       74    static int counter = 0;
       75    ++counter;
       76    if ( counter == 2 )
       77    {
       78       int e = _chrono.elapsed();
       79       std::cout << «Time: « << e/1000.0 << «\n»;
       80       counter = 0;
       81       _bt_calc->setEnabled(true);
       82    }
       83    Fract_Thread* th = dynamic_cast<Fract_Thread*>(sender());
       84    delete th;
       85 }

    Enfin, nous traitons la terminaison d’un thread. Un simple compteur permet de savoir si les deux se sont terminés, auquel cas, on réactive le bouton et on affiche le temps passé. Pour mémoire, la méthode sender() (de QObject) permet de connaître l’émetteur du signal qui a déclenché le slot.
    Nous avons donc obtenu là un programme graphique utilisant plusieurs threads à relativement peu de frais.

    /img-articles/lm/88/art-12/fig-1.jpg

    Fig. 1 : L’image en cours de calcul

    Ce programme complète les calculs en environ 19,14 secondes, c’est-à-dire à peine mieux qu’une version non parallèle, le dessin et le réaffichage répétés de l’image étant assez coûteux.

    Conditions d’attente

    Tel quel, notre programme n’est pas satisfaisant pour plusieurs raisons :

    • Il serait souhaitable de pouvoir ajuster dynamiquement le nombre de threads.
    • L’utilisateur devrait avoir la possibilité d’interrompre un calcul en cours, par exemple pour se déplacer dans l’ensemble fractal.
    • La fermeture brutale du programme en cours de calcul provoque un plantage, car les threads ne sont pas correctement arrêtés.

    Surtout, les threads sont créés et détruits « à la demande », ce qui n’est pas l’idéal en pratique. Il est généralement plus efficace de créer un certain nombre de threads, puis de les laisser en attente (en sommeil) jusqu’à en avoir besoin.
    Commençons par ce dernier point. L’idée est donc de créer les threads au début du programme, puis de les mettre en sommeil, pour ne les réveiller qu’au moment où on en a besoin. Nous allons pour cela utiliser une condition d’attente au sein d’une classe Fract_Thread modifiée ainsi :

        1 #ifndef __FRACT_THREADS_H__
        2 #define __FRACT_THREADS_H__ 1
        3 #include <QThread>
        4 #include <QMutex>
        5 #include <QWaitCondition>
        6 class Fractal_Image;
        7 class Fract_Thread: public QThread
    ...
       15    public slots:
       16       void startCalc();

    Le slot startCalc() sera utilisé pour réveiller le thread en sommeil et démarrer les calculs.

    ...
       26       QMutex            _mutex;
       27       QWaitCondition    _condition;
       28 }; // class Fract_Thread
       29 #endif // __FRACT_THREADS_H__

    La condition d’attente est représentée par une instance de la classe QWaitCondition. Comme nous allons être amené à modifier certaines données de Fract_Thread depuis différents threads, on utilise une exclusion mutuelle afin d’en synchroniser les accès au moyen de la classe QMutex.
    Voici la méthode run() adaptée à la nouvelle situation :

       22 void Fract_Thread::run()
       23 {
       24    forever
       25    {
       26       _mutex.lock();
       27       _condition.wait(&_mutex);
       28       _mutex.unlock();
       29       for(unsigned y = _min_y; y <= _max_y; ++y)
       30       {
       31          for(unsigned x = 0; x < _img->width(); ++x)
       32             _img->Compute_Pixel(x, y);
       33          emit drawLine(y);
       34       }
       35       emit calcFinished();
       36    }
       37 }

    Le mot forever (ligne 24), introduit par Qt, déclare une boucle infinie – un équivalent de for(;;), mais jugé plus « parlant ». La ligne 27 provoque la mise en sommeil du thread, par la méthode wait() de QWaitCondition. Les accès à cette variable doivent être protégés, aussi cet appel est-il encadré par un verrouillage (ligne 26, méthode lock() de QMutex) et une libération (ligne 28, méthode unlock() de QMutex) de l’exclusion mutuelle. Celle-ci doit également être communiquée à la condition d’attente. Un paramètre optionnel de wait() permet de spécifier une durée d’attente maximale, exprimée en millisecondes.
    Par ailleurs, le code de notre thread est désormais une boucle infinie : le signal finished() n’est donc plus pertinent pour détecter la fin des calculs, aussi a-t-on prévu le signal calcFinished() à cette fin (ligne 35).
    Le contenu du slot startCalc() est tout simple :

       18 void Fract_Thread::startCalc()
       19 {
       20    _condition.wakeOne();
       21 }

    La méthode wakeOne() de QWaitCondition a simplement pour effet de « réveiller » l’un des threads en attente de la condition – en effet, plusieurs threads peuvent attendre la même condition, bien qu’ici nous n’en ayons qu’un seul. Si plusieurs sont en attente, il est impossible de prévoir lequel sera réveillé par wakeOne(). La méthode wakeAll() permet de tous les réveiller, mais là encore dans un ordre indéfini.

    Voyons maintenant les adaptations nécessaires à la classe graphique GuiFract :

        1 #ifndef __GUI_FRACT_H__
    ...
       18    public slots:
       19       void calc();
       20       void initThreads(unsigned nb);
       21    protected slots:
       22       void drawLine(unsigned y);
       23       void threadFinished();
       24    signals:
       25       void startCalc();
       26    private:
    ...
       32       QVector<Fract_Thread*> _threads;
       33 }; // class GuiFract
       34 #endif // __GUI_FRACT_H__

    Dès maintenant, nous prévoyons la possibilité d’avoir plus de deux threads. Ils seront contenus dans un tableau _threads (ligne 32) et initialisés par la méthode initThreads() (ligne 20). Enfin est déclaré le signal startCalc(), qui permettra d’invoquer le slot éponyme de la classe Fract_Thread. Commençons par la création des threads :

       42 void GuiFract::initThreads(unsigned nb)
       43 {
       44    if ( nb < _threads.count() )
       45    {
       46       for(int i = nb; i < _threads.count(); ++i)
       47       {
       48          delete _threads[i];
       49          _threads[i] = 0;
       50       }
       51    }
       52    _threads.resize(nb);

    La taille du tableau est ajustée au nombre demandé, les éléments éventuellement en trop étant détruits. À noter que _threads est de type QVector, qui a comme caractéristique d’initialiser systématiquement les éléments nouveaux à une valeur par défaut, en l’occurrence 0 pour les types de base (entiers, réels, pointeurs). Ce comportement est différent de celui de la classe std::vector de la bibliothèque standard, dans lequel les éléments nouveaux sont laissés à une valeur indéterminée.

       53    unsigned step = _fract->height() / nb;
       54    unsigned rem  = _fract->height() % nb;
       55    unsigned ymin = 0;
       56    unsigned ymax = step+rem-1;
       57    for(int i = 0; i < _threads.count(); ++i)
       58    {
       59       if ( _threads[i] == 0 )
       60       {
       61          _threads[i] = new Fract_Thread(_fract, this);
       62          connect(_threads[i],  SIGNAL(drawLine(unsigned)),
       63                  this,         SLOT(drawLine(unsigned)));
       64          connect(_threads[i],  SIGNAL(calcFinished()),
       65                  this,         SLOT(threadFinished()));
       66          connect(this,        SIGNAL(startCalc()),
       67                  _threads[i], SLOT(startCalc()));
       68          _threads[i]->start();
       69       }
       70       _threads[i]->Set_Interval(ymin, ymax);
       71       ymin = ymax + 1;
       72       ymax = ymin + step - 1;
       73    }
       74 }

    Chacun des threads est ensuite créé (s’il n’existe pas déjà, ligne 61) et dûment connecté (remarquez que nous avons fait cette fois l’économie du paramètre Qt::QueuedConnection). Comme nos threads sont désormais supposés être en attente, ils sont démarrés immédiatement (ligne 68). Pour l’heure, un appel initThreads(2) est simplement ajouté à la fin du constructeur de GuiFract.
    Le slot connecté au bouton et démarrant les calculs doit naturellement être modifié :

       34 void GuiFract::calc()
       35 {
       36    _bt_calc->setEnabled(false);
       37    _pix->fill(Qt::black);
       38    _lbl_pix->setPixmap(*_pix);
       39    _chrono.start();
       40    emit startCalc();
       41 }

    L’émission du signal va induire l’appel aux slots startCalc() des différents threads en sommeil, ce qui va provoquer leur réveil et le déclenchement des calculs, grâce à la connexion réalisée ligne 66 (dans initThreads()).
    Nous évitons donc maintenant des destructions et création de threads à chaque demande de calcul. De plus, nous sommes relativement capables de manipuler un nombre quelconque de threads. Voyons maintenant comment modifier ce nombre dynamiquement et comment arrêter proprement les threads.

    Synchronisations

    Le premier besoin est de pouvoir interrompre les calculs à n’importe quel moment, le second de pouvoir provoquer la terminaison des threads à la demande. Pour cela, on ajoute à la classe Fract_Thread un destructeur, une paire de slots et une paire de booléens :

    ...
        7 class Fract_Thread: public QThread
        8 {
        9    Q_OBJECT
       10    public:
       11       Fract_Thread(Fractal_Image* img,
       12                    QObject* parent = 0);
       13       ~Fract_Thread();
       14       void Set_Interval(const unsigned min_y,
       15                         const unsigned max_y);
       16    public slots:
       17       void startCalc();
       18       void abortCalc();
       19       void terminateCalc();
    ...
       31       bool              _do_abort;
       32       bool              _do_terminate;
       33 }; // class Fract_Thread
       34 #endif // __FRACT_THREADS_H__

     abortCalc() (ligne 18) et terminateCalc() (ligne 19) permettent respectivement d’interrompre un calcul en cours et de provoquer la sortie de la méthode run(), mettant ainsi fin au thread en cours d’exécution. La méthode run(), justement, est modifiée ainsi :

       40 void Fract_Thread::run()
       41 {
       42    forever
       43    {
       44       _mutex.lock();
       45       if ( _do_terminate )
       46       {
       47          _mutex.unlock();
       48          return;
       49       }
       50       _condition.wait(&_mutex);
       51       if ( _do_terminate )
       52       {
       53          _mutex.unlock();
       54          return;
       55       }
       56       _do_abort = false;
       57       _mutex.unlock();

    Pour commencer, l’attente de la condition est encadrée par deux tests pour vérifier si par hasard la fin de l’exécution n’aurait pas été demandée (_do_terminate vaut alors true, nous le verrons bientôt). Si tel est le cas, on sort immédiatement de la méthode run(), terminant ainsi l’exécution du thread. Attention toutefois à déverrouiller l’exclusion mutuelle, afin de ne pas provoquer d’inter-blocage (lignes 47 et 53).

       58       for(unsigned y = _min_y; y <= _max_y; ++y)
       59       {
       60          for(unsigned x = 0; x < _img->width(); ++x)
       61             _img->Compute_Pixel(x, y);
       62          {
       63             QMutexLocker ml(&_mutex);
       64             if ( _do_abort )
       65                break;
       66             if ( _do_terminate )
       67                return;
       68          }
       69          emit drawLine(y);
       70       }
       71       _mutex.lock();
       72       if ( ! _do_abort )
       73          emit calcFinished();
       74       _mutex.unlock();
       75    }
       76 }

    Ensuite des tests sont insérés au sein des boucles de calculs. La lecture des variables _do_abort et _do_terminate doit être protégée, par exemple par une exclusion mutuelle, étant donné qu’elles peuvent être modifiées par un autre thread – typiquement, l’ordre d’arrêt des calculs viendra de l’interface graphique via le thread principal. On peut naturellement reproduire le schéma des lignes 45 à 49 ou 51 à 55 pour assurer la libération de l’exclusion mutuelle, mais nous avons utilisé ici une facilitée offerte par Qt : la classe QMutexLocker. La création d’une instance de cette classe a pour effet de verrouiller l’instance de QMutex qui lui est passée en paramètre (ligne 63), tandis que sa destruction provoquera automatiquement la libération. Le bloc constitué des lignes 62 à 68 n’existe que pour cela : à la sortie de ce bloc, quel qu’en soit le moyen (même une exception), l’instance de QMutexLocker créée ligne 63 sera détruite, ce qui libérera automatiquement l’exclusion mutuelle.
    Cette technique peut paraître ici un peu exagérée, mais elle est extrêmement pratique dans des situations complexes où une fonction étendue présente de nombreux points de sortie.
    Voyons maintenant la méthode permettant d’interrompre un calcul :

       29 void Fract_Thread::abortCalc()
       30 {
       31    _mutex.lock();
       32    _do_abort = true;
       33    _mutex.unlock();
       34 }

    Il s’agit simplement de mettre la donnée _do_abort à true. La méthode terminant le thread est un peu différente :

       35 void Fract_Thread::terminateCalc()
       36 {
       37    _do_terminate = true;
       38    _condition.wakeOne();
       39 }

    Il est parfaitement possible que cette demande de terminaison survienne alors que le thread est en attente de la condition. C’est pourquoi on le réveille après avoir fixé à true la donnée _do_terminate. Si le thread n’est pas en attente, cet appel est sans effet.
    Enfin, le destructeur de Fract_Thread s’assure que tout est en ordre :

       14 Fract_Thread::~Fract_Thread()
       15 {
       16    terminateCalc();
       17    wait();
       18 }

    Rappelez-vous le deuxième programme de cet article : la méthode wait() de QThread, ici invoquée ligne 17, attend que le thread se termine, c’est-à-dire que l’exécution de la méthode run() s’achève. Cet appel est nécessaire après l’invocation de terminateCalc(), car la destruction peut avoir pour origine un autre thread que celui-ci – ce sera même toujours le cas. De cette façon, nous éviterons le plantage qui survenait en fermant intempestivement le programme.
    Naturellement, quelques modifications sont apportées à la classe d’interface GuiFract. Voici son nouvel aspect, durant le calcul de l’image par cinq threads :

    /img-articles/lm/88/art-12/fig-2.jpg

     Fig. 2 : Calcul par cinq threads

    Le curseur en haut permet de choisir le nombre de threads concurrents, de 1 à 8. On retrouve le bouton pour lancer les calculs, l’afficheur en haut à droite étant simplement le temps écoulé depuis le début du calcul (en seconde).
    Pour commencer, on ajoute à GuiFract une méthode permettant d’interrompre les calculs éventuellement en cours :

       73 void GuiFract::abortCalc()
       74 {
       75    for(int i = 0; i < _threads.count(); ++i)
       76       _threads[i]->abortCalc();
       77    _bt_calc->setEnabled(true);
       78 }

    Simple itération sur le tableau d’instances de Fract_Thread. Si aucun calcul n’est en cours, ceci n’a pas d’effet.
    Un appel à abortCalc() est ajouté au début des méthodes critiques, à savoir calc() (qui démarre les calculs) et initThreads() (qui créé et détruit les instances de Fract_Thread). Le curseur étant connecté directement à initThreads(), on obtient comme résultat que les calculs en cours sont interrompus dès que le nombre de threads à utiliser est modifié.

    Conclusion

    Comme toujours, nous n’avons fait que survoler le sujet des threads dans le cadre de Qt. Consultez la documentation et les exemples pour plus de détails. La programmation parallèle est et demeure une technique délicate, demandant du soin et de la pratique. Toutefois les quelques outils que nous venons de voir devrait permettre de faciliter son adoption et ainsi tirer le meilleur parti des architectures multiprocesseurs ou multi-cœurs. La prochaine fois, nous aborderons le vaste sujet du paradigme de programmation Modèle/Vue/Contrôleur (ou MVC) tel qu’il est proposé par Qt.

    Références :

    • Les codes sources de cet article sont du domaine public et peuvent être obtenus à l’adresse
      http://www.kafka-fr.net/articles/qt4/sources_07.tar.bz2.

    Retrouvez cet article dans : Linux Magazine 88

    Posté par (La rédaction) | Signature : Yves Bailly | Article paru dans

    Laissez une réponse

    Vous devez avoir ouvert une session pour écrire un commentaire.