Catégorie : Programmation     Tags :      

    Retrouvez cet article dans : Linux Magazine 84

    Il s’agit d’un article décrivant la réalisation d’un économiseur d’écran en utilisant les threads pour la réalisation du traitement de l’image affichée. Il présente la façon de protéger des données par des verrous et la façon de « discuter » avec le programme principal.

    1. Introduction

    Non non, il ne s’agit pas d’une erreur et d’une recopie de l’article de mars 2006, il s’agit de la présentation programmée de la suite de l’article si vous lisez bien la conclusion de celui-ci. Le principe de fonctionnement est, cette fois-ci, de faire le traitement de l’image dans un thread et d’avertir ensuite le programme principal que l’affichage peut avoir lieu. Un thread ? Un mot bien étrange qui désigne en fait un processus léger, c’est-à-dire un programme qui fonctionne en parallèle et dont la particularité est de partager la même zone mémoire que l’application.
    La compilation des programmes se fait avec la ligne de commande suivante : g++ mon_prog.cpp -o mon_prog `wx-config --libs --cppflags`. Le ` est obtenu par la combinaison des touches [ALT GR]+[7] (pour vérifier que la bibliothèque est correctement installée, il suffit de taper wx-config --version). Les programmes sont conformes à la version 2.6.x (voire 2.4.2).

    2. Exemple d’utilisation d’un thread

    2.1 Introduction : la classe wxThread

    La classe de base d’un thread est wxThread, il s’agit d’une classe abstraite : il est donc nécessaire d’utiliser une classe dérivée de celle-ci.
    Il y a deux méthodes importantes dans la classe : Entry() et OnEx it(). Comme on peut le deviner, il s’agit des fonctions d’entrée et de sortie du thread.
    Cela permet de séparer l’exécution du thread de sa phase de sortie. Dans notre premier exemple, seulement Entry() sera utilisée.

    /img-articles/lm/84/fig-1.jpg

    Fig. 1 : Plusieurs threads ensemble

    Le but est de faire apparaître les problèmes d’un partage d’une simple variable entière entre plusieurs threads la modifiant. Le premier thread incrémente cette variable et le deuxième décrémente celle-ci et ainsi de suite sur les autres threads.
    On fera un simple affichage dans une zone de texte à chaque changement et on verra à l’exécution les problèmes sur le système GNU Linux. Le thread a besoin de connaître la fenêtre, la variable à modifier ainsi que la valeur de l’incrément.
    On a alors le code des cadres code 1, code 2 et code 3 qui donne le résultat de la figure 1 (disons qui devrait donner ce résultat : l’explication arrive plus loin). Mais expliquons avant tout le programme.

    #include <wx/wx.h>
    #include <wx/thread.h>
    
    // déclaration de l’application
    class MyApp : public wxApp
    {
      public:
       virtual bool OnInit(); // le main
    };
    
    // la nouvelle fenêtre
    class MyFrame : public wxFrame
    {
      public:
       // constructeur
       MyFrame(wxWindow *parent,wxWindowID id);
       // pour afficher dans affichage
       // la variable m_variable
       void Affiche();
       // obtenir la variable
       int& GetVariable();
      private:
       // le texte pour l’affichage
       wxTextCtrl *affichage;
       int m_variable;
    };
    
    // création de l’application
    IMPLEMENT_APP(MyApp)
    
    // création de la fenêtre
    MyFrame::MyFrame(wxWindow *parent,wxWindowID id) :
             wxFrame(parent,id,wxEmptyString),
             affichage(new wxTextCtrl(this,wxID_ANY)),m_variable(0)
    {
      // création d’un sizer
      wxBoxSizer *box=new wxBoxSizer(wxVERTICAL);
      // on place la zone de texte dans ce
      // sizer à 10 pixels des bords
      box->Add(affichage,1,wxLEFT|wxRIGHT|
    	wxTOP|wxBOTTOM|wxEXPAND,10);
      // on indique que le sizer fixe la
      // taille de la fenêtre
      box->SetSizeHints( this );
      // on place le sizer dans la fenêtre
      SetSizer(box);
    }
    
    int& MyFrame::GetVariable()
    {
      return m_variable;
    }
    
    // affichage de la variable dans la
    // zone de texte
    void MyFrame::Affiche()
    {
      // on efface
      affichage->Clear();
      // on affiche
      affichage->WriteText(wxString::Format(wxT(«%d «),
    	m_variable));
    }

    Code 1 : Déclaration de l’application et de la fenêtre

    Dans le code 1, on réalise une nouvelle fenêtre contenant la variable à afficher ainsi que la zone de texte d’affichage. Une méthode Affiche() permet cela. La méthode GetVariable() permet d’obtenir une référence sur la variable afin de permettre les modifications.
    Dans le code 3, on réalise une nouvelle classe MyThread qui est ici un thread détaché sachant se terminer tout seul, vous le créez et vous « l’oubliez ». Il est constitué d’une boucle qui teste s’il doit se terminer ou non (en utilisant TestDestroy()) et, dans cette boucle, on incrémente la variable de la fenêtre et on demande à la fenêtre de l’afficher. En fin de boucle, on redonne le temps restant sur la tâche au système pour qu’il donne la main à une autre tâche.

    bool MyApp::OnInit()
    {
      // une fenêtre
      MyFrame *frame = new MyFrame(NULL,wxID_ANY);
      // un thread
      MyThread *thread;
      // disons 6 threads
      for(int i=0;i<6;i++) {
        // création
        thread=new MyThread(*frame,frame->GetVariable(),
    	i%2==0?1:-1);
        // si créé exécuter
        if (thread->Create()==wxTHREAD_NO_ERROR)
    	thread->Run();
      }
      // affichage de la fenêtre
      frame->Show(true);
      // fenêtre principale
      SetTopWindow(frame);
      // tout se passe bien passage
      // sur la boucle d’événements
      return true;
    }

    Code 2 : Le programme principal

    Dans le code 2, on crée une nouvelle fenêtre, puis 6 threads affectés des incréments 1 et -1. Lorsque l’on démarre le programme, il s’arrête en indiquant différentes erreurs dont le message « Xlib : unexpected async reply ». Il faut éventuellement relancer plusieurs fois le programme pour voir apparaître ce message (mais le programme ne fonctionne pas même quand celui-ci ne s’affiche pas).
    Il indique en fait que l’on a un autre thread que le thread principal qui accède aux éléments graphiques. Sur la plupart des systèmes, il ne peut y avoir qu’un seul accès à la partie graphique. Pour contrer ceci, il existe un verrou de mutuelle exclusion pour la partie graphique : il suffit donc de mettre wxMutexGuiEnter(); avant m_frame.Affiche(); et wxMutexGuiLeave(); ensuite. Cela permet d’obtenir la figure 1. Mais le plus simple est d’envoyer au programme principal un événement de changement de valeur (ce sera fait dans l’économiseur) ou dans le cas présent d’utiliser un timer pour l’affichage. On a alors le code du cadre code 4.

     

    class MyThread : public wxThread
    {
      public:
       MyThread(MyFrame& frame,int& variable,int increment);
       // l’entrée du thread, le main en quelque sorte
       void* Entry();
      private:
       MyFrame &m_frame; // référence à la fenêtre
       int &m_variable;  // idem pour la variable
       int m_increment;  // incrément
    };
    
    MyThread::MyThread(MyFrame& frame,int&
      variable,int increment) :
      wxThread(),m_frame(frame),
      m_variable(variable),
      m_increment(increment)
    {
    }
    
    void* MyThread::Entry()
    {
      // tant que le thread peut continuer
      while(!TestDestroy())
      {
        // incrémentation
        m_variable+=m_increment;
        m_frame.Affiche(); // affichage
        // on laisse le temps qui reste à un autre thread
        Yield();
      }
      // tout s’est bien passé
      return NULL;
    }

    Code 3 : Déclaration de la classe MyThread

    Dans le code 4, on ajoute une table d’événements pour récupérer l’événement lié au timer. On affecte ce dernier à la fenêtre dans le programme principal. On constate donc dans le résultat que le nombre affiché peut varier sur de grandes amplitudes autant négatives que positives (même si tous les threads ont le même temps d’exécution). Exemple : le thread A lit la variable C et y ajoute 1, entre-temps le thread B lit aussi cette variable, mais avant la modification de A et ajoute -1. Le thread A écrit la nouvelle valeur en mémoire, puis le thread B dans la foulée. Tout se passe alors comme si A n’avait jamais incrémenté la variable. Si ce phénomène se produit à chaque fois de la même façon, alors la variable C diminue de 1 à chaque tour. Le seul moyen d’éviter cela est d’informer le thread B qu’un autre thread accède déjà à la variable et qu’il doit attendre son tour. Il faut indiquer que la lecture de A est une ressource partagée et utiliser un verrou (utilisation d’un mutex ou d’un sémaphore par exemple).

     

    class MyFrame : public wxFrame
    {
      public:
       ...
       // traitement de l’événement, uniquement
       // possible dans le thread principal
       void OnTimer(wxTimerEvent& event);
       ...
       DECLARE_EVENT_TABLE()
    };
    
    BEGIN_EVENT_TABLE(MyFrame,wxFrame)
      EVT_TIMER(wxID_ANY,MyFrame::OnTimer)
    END_EVENT_TABLE()
    
    // traitement à réaliser périodiquement
    void MyFrame::OnTimer(wxTimerEvent& event)
    {
      Affiche();
    }
    
    bool MyApp::OnInit()
    {
      ...
      // timer associé à la fenêtre
      wxTimer *timer=new wxTimer(frame);
      // toutes les 50ms
      timer->Start(50);
      ...
    }

    Code 4 : Utilisation d’un timer pour le ré-affichage

    2.2 Ajout d’un mutex : wxMutex

    Pour protéger la ressource, on va verrouiller son accès. Pour faire simple, il faut verrouiller une zone et ne pas oublier de déverrouiller. Pour que ce soit automatique (car il est fréquent d’oublier de libérer la ressource), on utilise un élément wxMutexLocker qui verrouille le mutex associé à la variable (de type wxMutex) et le déverrouille lors de sa destruction. L’exécution du code en fixant 10 threads donne une oscillation des valeurs entre 0 et 1 sans obtenir de résultats étranges. Il faut naturellement voir que, dans notre cas, nous voulons modifier la variable. On a alors le code du cadre code 5.

    class MyFrame : public wxFrame
    {
      public:
       ...
       void Ajoute(int a);
      private:
       // le mutex pour verrouiller la variable
       wxMutex m_mutex;
       ...
    };
    
    class MyThread : public wxThread
    {
      public:
       MyThread(MyFrame& frame,int increment);
       ... // et disparition de int &m_variable
    };
    
    MyThread::MyThread(MyFrame& frame,
      int increment) : wxThread(),
      m_frame(frame),m_increment(increment)
    {
    }
    
    void* MyThread::Entry()
    {
      while(!TestDestroy())
      {
        // on ajoute l’incrément
        m_frame.Ajoute(m_increment);
        // on s’endort pour 10ms
        Sleep(10);
      }
      return NULL;
    }
    
    void MyFrame::Ajoute(int a)
    {
      // on verrouille la zone
      wxMutexLocker mutlock(m_mutex);
      m_variable+=a;
    } // ici le mutex est de nouveau libre
    
    bool MyApp::OnInit()
    {
     ...
     thread=new MyThread(*frame,i%2==0?1:-1);
     ...
    }

    Code 5 : Verrouillage de la variable

    3. Notre économiseur

    3.1 Introduction

    Il s’agit ici de réaliser un économiseur d’écran, c’est-à-dire un programme réalisant une animation plein écran qui s’arrête et redonne la main au système lorsque l’utilisateur bouge la souris ou appuie sur les touches du clavier. Nous voulons, dans notre cas, faire onduler l’écran affiché. Pour ce faire, on va créer une fenêtre contenant un élément de type wxPanel, afin de pouvoir récupérer les événements liés au clavier. Mais avant de créer celui-ci, une mémorisation de l’écran du système doit être faite. Cette mémoire sera utilisée par le thread pour la création de l’image affichée. Elle sera partagée avec l’application. Un événement de rafraîchissement d’écran sera envoyé par le thread à l’application lorsqu’un calcul d’image sera terminé. La méthode reste similaire au premier économiseur, le but étant ici de ne pas trop s’éloigner de la première solution et de permettre à ceux l’ayant vu de comprendre plus facilement l’utilisation des threads.

    3.2 Problèmes

    Il existe plusieurs problèmes liés à l’utilisation de threads :

    • Problème d’accès aux données.
    • Problème de synchronisation : imaginez qu’un thread A attend la réponse d’un thread B avant de libérer une ressource. Et ce même thread B qui ne donne une réponse qu’une fois la ressource récupérée. Chacun attendant comme deux voitures à un carrefour que l’autre avance, il y a un dead lock, c’est-à-dire un blocage du programme dû à des utilisations de ressources « croisées », dans cet exemple. Pour ce cas simple, on trouve rapidement le problème, mais sur des applications possédant plusieurs dizaines de threads, cela n’est pas aussi facile à détecter.
    • Problème d’accès aux ressources graphiques.
    • Problème de réentrance du code. Certaines bibliothèques ne sont pas thread-safe.

    L’accès aux éléments graphiques est celui qui pose le plus de problèmes aux personnes désirant utiliser les threads (même si ce n’est pas là où se situe la plus grande difficulté). On est toujours tenté de demander une boîte de dialogue, de vouloir afficher un message... mais pour être complètement portable, il n’y a qu’un seul thread qui peut accéder aux éléments graphiques, et c’est le thread principal. Il existe alors trois solutions :

    • Changer votre code pour éviter les threads. Mais pas de chance pour nous, car c’est précisément le sujet de cet article. Mais, si vous ne faites pas un programme spécialement pour illustrer les threads, alors une solution est de donner la main à la boucle de traitement des événements de façon périodique. Pour cela, utilisez dans votre calcul la méthode Yield de l’application ou ::wxYield. Cela permet de traiter les événements courants sans être obligé de bloquer tout le programme. Vous pouvez donc récupérer un événement bouton de l’utilisateur qui demande d’arrêter le traitement.
    • Bloquer l’accès graphique avec un mutex (classe wxMutex) lié au graphisme, puis libérer l’accès une fois l’utilisation de la partie graphique terminée (wxMutexGuiEnter et wxMutexGuiLeave).
    • Envoyer un événement pour signaler à l’application qu’elle doit accéder aux données graphiques.

    La dernière solution est, dans certains cas, la seule utilisable, mais elle demande aussi une réflexion et un codage plus importants. Naturellement, si vous demandez une valeur grâce à une boîte de dialogue, elle doit être non modale pour permettre à plusieurs boîtes d’être actives en même temps, s’il y a plusieurs threads.

    3.3 Premier programme

    3.3.1 Introduction

    Ce premier programme présente une utilisation des threads pour un « traitement d’image ». Celui-ci consiste uniquement à décaler sous une forme sinusoïdale les lignes de l’image de façon à faire onduler cette dernière. On présente de façon rapide les éléments nécessaires à la compréhension du traitement des événements et ensuite une méthode pour communiquer entre le thread et le programme principal (en fait, dans notre cas, le dialogue ne va que du sens thread vers l’application). Le traitement se fait de façon classique par l’utilisation d’éléments dérivés de wxDC en faisant apparaître les limites de la méthode. La présentation des éléments se fait dans l’ordre : fenêtre, panneau, thread et application.

    3.3.2 La fenêtre : wxFrame

    La fenêtre contient un panneau qui gère les événements de réaffichage (événement wxPaintEvent), de la souris (événement wxMouseEvent), du clavier (wxKeyEvent) et d’un événement de type menu. Le panneau demande la fermeture de la fenêtre s’il y a un mouvement de souris ou une frappe clavier (on attend 4 mouvements de souris pour finir, car, lors de la création de la fenêtre, des événements du type « entrée dans la fenêtre » et mouvement de souris peuvent être générés).
    Lors de la réception d’un événement de type menu sélectionné, on réaffiche la fenêtre, afin d’éviter de créer un événement personnalisé. La fenêtre contient 2 images de type wxBitmap : l’image à afficher et la copie de l’écran système du départ. Il y a aussi une condition (wxCondition) et un mutex (wxMutex) pour se synchroniser sur la fin du thread à la fermeture de la fenêtre. Les cadres code 9 et code 10 présentent respectivement la déclaration de la fenêtre et les tables d’événements. Mais, comme l’affichage se fait sur un panneau, il est nécessaire d’obliger le panneau à se réafficher. Pour cela, on utilise la méthode Refresh. Il y a alors un événement de type wxPaintEvent qui est récupéré par le panneau.

    class MyThread; // declaration d’un “thread”
    
    class MyFrame : public wxFrame
    {
     public:
      // constructeur
      MyFrame();
      // destructeur
      ~MyFrame();
      // Pour rafraîchir la fenêtre
      void OnRefresh(wxCommandEvent& event);
      // Pour la fermeture
      void OnClose(wxCloseEvent& event);
      // Obtenir l’image affichée
      wxBitmap *GetImage();
      // Obtenir la copie de l’écran de départ
      wxBitmap *GetCopie();
      // utilisé pour une synchronisation
      wxCondition* GetCondition() { return &m_condition;}
     private:
      // image stockée de l’écran de l’ordinateur
      wxBitmap *copie;
      // image affichée
      wxBitmap *affiche;
      // largeur et hauteur de l’écran
      int largeur,hauteur;
      // panneau stocké
      MyPanel *m_panel;
      // thread associé à la fenêtre
      MyThread *m_thread;
      // mutex lié à la condition de synchronisation
      wxMutex m_mutex;
     // la condition
      wxCondition m_condition;
      // table d’événements
     DECLARE_EVENT_TABLE()
    };

    Code 9 : Déclaration de la fenêtre

    Le constructeur de la fenêtre dont le code se trouve dans le cadre code 12 permet la création d’un panneau, la récupération de la taille de l’écran et copie celui-ci dans une image de type wxBitmap. Et, chose importante, on verrouille le mutex (wxMutex), on crée un thread et, si la création se passe bien, on l’exécute. Dans le destructeur (cadre code 12bis), on n’oublie pas de déverrouiller le mutex et de détruire les images stockées. Dans le cas d’un oubli de déverrouillage, il y aura une erreur lors de la destruction du mutex (un affichage sur la console).

    MyFrame::~MyFrame()
    {
      std::cout<<»~MyFrame»<<std::endl;
      m_mutex.Unlock();
      // on libère les images stockées
      delete copie;
      delete affiche;
    }

    Code 12bis : Destructeur de la fenêtre

     

     MyFrame::MyFrame() : wxFrame((wxWindow*)NULL,
    	wxID_ANY,wxEmptyString,wxPoint(200,200),
    	wxDefaultSize,wxSTAY_ON_TOP|wxNO_BORDER),
    	m_condition(m_mutex)
    {
     // on vérrouille
     m_mutex.Lock();
     // écran de l’ordinateur
     wxScreenDC dc;
     // pour stocker l’écran
     wxMemoryDC memDC;
     int largeur,hauteur;
     // création du panneau
     m_panel=new MyPanel(this);
     // on stocke une fois pour toute la taille
     dc.GetSize(&largeur,&hauteur);
     // on doit quand même fixer la taille
     // de la fenêtre au départ
     SetSize(0,0,largeur,hauteur);
     // image de la fenêtre
     copie=new wxBitmap(largeur,hauteur);
     // on reserve de la place
     affiche=new wxBitmap(largeur,hauteur);
     // association bitmap<->mémoire
     memDC.SelectObject(*copie);
     // on stocke l’écran du système
     memDC.Blit(0,0,largeur,hauteur,&dc,0,0);
     // association bitmap<->mémoire
     memDC.SelectObject(*affiche);
     // on stocke l’écran du système
     memDC.Blit(0,0,largeur,hauteur,&dc,0,0);
     // création d’un nouveau thread associé à la fenêtre
     m_thread=new MyThread(this);
     // création et exécution
     if (m_thread->Create() == wxTHREAD_NO_ERROR )
    	 m_thread->Run();
    }

    Code 12 : Constructeur de la fenêtre

    La suite du cadre code 13 présente les méthodes pour obtenir les images et le code de la fermeture du thread. Il existe deux cas de figure (naturellement, lors de la création du thread, vous devez en choisir un des deux) pour caractériser le thread et la méthode de terminaison diffère donc. Les deux cas sont :

     

    • Thread détaché : il se termine tout seul donc, dans ce cas, on se synchronise avec la condition.

     

    • Thread avec « jonction » : normalement on doit attendre la fin de celui-ci pour commencer une nouvelle tâche. Il faut donc : attendre, puis détruire le thread, car il ne sait pas se « libérer » lui-même.

    Pour éviter qu’un événement de fermeture arrive pendant l’attente, une variable statique permet de s’assurer qu’une seule destruction des threads aura lieu.

     

    // l’image affichée
    wxBitmap * MyFrame::GetImage() {
      return affiche;
    }
    
    // la copie
    wxBitmap * MyFrame::GetCopie()
    {
      return copie;
    }
    
    void MyFrame::OnClose(wxCloseEvent& event)
    {
      static int n=0;
      n++;
      // pour ne détruire qu’une seule fois le thread
      if (n==1) {
        // si le thread est “détaché»
        if (m_thread->IsDetached()) {
           // demande d’arrêter
           m_thread->Delete();
           // on attend la fin du thread
           m_condition.Wait();
        }
        else {
           // demande d’arrêter
           m_thread->Delete();
           // on attend la fin du thread
           m_thread->Wait();
           // on le détruit car il ne se
           // détruit pas lui même
           delete m_thread;
        }
      }
      // traitement normal de fermeture
      event.Skip();
    }
    
    void MyFrame::OnRefresh(wxCommandEvent& event)
    {
      // on rafraîchit le panneau
      m_panel->Refresh();
    }

    Code 13 : Obtention des images, code de fermeture et
    de demande de ré-affichage de la fenêtre

    3.3.3 Le panneau : wxPanel

    Le panneau existe uniquement pour pouvoir récupérer les événements du clavier, car, sur différentes versions de wxWidgets (par exemple la version wxGTK), il n’est pas possible d’avoir un « focus » sur une fenêtre, donc il faut passer par un élément de type wxPanel. La déclaration du nouveau panneau se fait dans le cadre code 8 et la table d’événements associée se trouve dans le cadre code 10.

    #include <wx/wx.h>
    // pour le décalage
    #include <math.h>
    // juste pour voir comment s’exécutent les parties
    #include <iostream>
    #include <wx/thread.h>
    
    class MyPanel : public wxPanel
    {
     public:
      MyPanel(wxWindow* parent);
      ~MyPanel(); // destructeur
      // fonction pour le ré-affichage
      void OnPaint(wxPaintEvent& event);
      // vide
      void OnEraseBackground(wxEraseEvent& event) {};
      // récupération des événements clavier
      void OnKeyDown(wxKeyEvent& event);
      // récupération des événements souris
      void OnMouse(wxMouseEvent& event);
     private:
      // déclaration d’une table d’événements
      DECLARE_EVENT_TABLE()
    };

    Code 8 : Déclaration du panneau

    Il est bien sûr possible de laisser l’événement se propager vers le parent et traiter alors celui-ci dans la fenêtre. Pour cela, on peut trouver le parent et propager l’événement ou indiquer dans le traitement que l’événement peut se propager d’un niveau. Ceci est illustré dans le cadre code 8bis. Il faut alors ajouter une table de réception des événements clavier dans la fenêtre MyFrame.

    void MyPanel::OnKeyDown(wxKeyEvent& event)
    {
      // 1ère solution
      GetParent()->ProcessEvent(event);
      // ou
      // 2ème solution
      event.ResumePropagation(1);
      event.Skip();
    }
    
    void MyFrame::OnKeyDown(wxKeyEvent& event)
    {
      // une seule touche ici
      wxGetApp().Finir();
    }

    Code 8 bis : Propagation de l’événement

    Le traitement des différents événements est assez simple en soi, le code est illustré dans le cadre code 9. Il faut avoir le focus dès la création, sinon les événements clavier ne pourront avoir lieu. Pour le réaffichage de l’image, on passe par un wxMemoryDC pour faire une association avec l’image et on écrit directement dans un élément wxPaintDC.

    // création de l’application
    IMPLEMENT_APP(MyApp)
    
    // table d’événements
    BEGIN_EVENT_TABLE(MyPanel,wxPanel)
    // on ré-affiche l’image
     EVT_PAINT(MyPanel::OnPaint)
    // gestion de tous les événements de souris
     EVT_MOUSE_EVENTS(MyPanel::OnMouse)
    // on ré-affiche le fond de l’image
     EVT_ERASE_BACKGROUND(MyPanel::OnEraseBackground)
    // gestion des événements clavier
     EVT_KEY_DOWN(MyPanel::OnKeyDown)
    END_EVENT_TABLE()
    
    BEGIN_EVENT_TABLE(MyFrame,wxFrame)
     // récupération d’un événement envoyé par le thread
     EVT_MENU(wxID_ANY,MyFrame::OnRefresh)
     // événement de fermeture de la fenêtre
     EVT_CLOSE(MyFrame::OnClose)
    END_EVENT_TABLE()

    Code 10 : Déclaration des tables d’événements

    3.3.4 Le thread : wxThread

    Le thread prend comme unique paramètre, lors de sa construction, la fenêtre à laquelle il est associé. Il est caractérisé par 2 fonctions importantes : Entry et OnExit.

    MyPanel::MyPanel(wxWindow *parent) :
    	wxPanel(parent,wxID_ANY,wxDefaultPosition,
    	wxDefaultSize,wxWANTS_CHARS)
    {
     // on prend le focus dès le début
     SetFocus();
    }
    
    MyPanel::~MyPanel()
    {
      std::cout << «~MyPanel» << std::endl;
    }
    
    void MyPanel::OnPaint(wxPaintEvent& event)
    {
      wxPaintDC dc(this);
      wxMemoryDC memDC;
      // la fenêtre
      static MyFrame *frame=
    	dynamic_cast<MyFrame*>(GetParent());
      // association image<-->memDC
      memDC.SelectObject(*(frame->GetImage()));
      // affichage
      dc.Blit(0,0,dc.GetSize().GetWidth(),
    	dc.GetSize().GetHeight(),&memDC,0,0);
    }
    
    void MyPanel::OnMouse(wxMouseEvent& event)
    {
     int static n=0;
     n++;
     // au bout de 4 mouvements on arrête tout
     if (n>4) wxGetApp().Finir();
    }
    
    void MyPanel::OnKeyDown(wxKeyEvent& event)
    {
      // une seule touche ici
      wxGetApp().Finir();
    }

    Code 9 : Constructeur, destructeur et traitement des événements du panneau

    Il s’agit de la fonction d’entrée du thread et de sa fonction de sortie. Dans cet exemple, un destructeur a été ajouté pour voir où se termine le thread par rapport à la fin de l’application (cf. 3.4.5). De plus, il est fortement conseillé de ne pas quitter l’application « trop rapidement », mais plutôt de bien s’assurer de la destruction des threads avant de quitter, car sinon une destruction des threads a lieu automatiquement par l’application entraînant alors une perte éventuelle de mémoire. Dans notre exemple, deux cas sont présentés dans le même code (à vous ensuite de choisir votre solution) :

     

    • Le premier cas concerne un thread dit « détaché », c’est-à-dire sachant se détruire lui-même. Vous le lancez et s’il doit se terminer, il le fera automatiquement. Mais il est souhaitable de prévoir une porte de sortie au cas où l’utilisateur voudrait quitter rapidement l’application. On doit se synchroniser en utilisant une condition (cf. ci-après).
    • Le premier cas concerne un thread lié qui ne se détruit pas lui-même, mais, par contre, il sait se faire attendre, donc il est plus facile de se synchroniser dessus sans utiliser le mécanisme précédent. Ensuite une petite destruction par delete s’impose.

    Le problème est de ne pas laisser l’application se fermer avant le thread et surtout de ne pas permettre une réentrance de la fonction de traitement de fermeture de la fenêtre. On ne doit détruire le thread qu’une seule fois et, de plus, il n’est pas possible de savoir si un thread a été détruit ou pas. Le cadre code 6 présente la déclaration du thread et le cadre code 11 présente le constructeur, destructeur et la méthode de sortie du thread. On peut constater que la méthode de sortie envoie grâce à un élément de type wxCondition une information qui indique à la fenêtre que sa fermeture peut se poursuivre.
    En effet, on ne peut pas savoir si la fermeture du thread sera plus rapide que celle de la fenêtre ou inversement, cela dépend de l’instant de demande de fermeture par l’application. Or, si la fenêtre venait à se terminer avant le thread, cela entraînerait une fermeture de l’application qui détruirait alors les threads non terminés. Pour éviter cela, on « synchronise » la fermeture du thread et de la fenêtre.

     

     // le thread
    class MyThread : public wxThread
    {
     public:
      // constructeur avec passage de la fenêtre
      MyThread(MyFrame *);
      // fonction d’entrée du thread
      ExitCode Entry();
      // fonction de sortie
      void OnExit();
      // destructeur ==> en fin de programme
      ~MyThread();
     private:
      // la fenêtre «parente» du thread
      MyFrame *m_frame;
    };

    Code 6 : Déclaration du thread

    Le traitement de l’image se fait dans le cadre code 12. On commence par créer un élément de type wxMemoryDC pour travailler sur l’image. On crée un événement de type wxCommandEvent pour la sélection d’un menu. Puis, on travaille sur un texte que l’on positionne au centre de l’image avec une police de taille 60 et on indique que le fond du texte est transparent. On précalcule la table des sinus qui sera utilisée en stockant les résultats sur des nombres signés sur 16 bits (en supposant que toutes les plateformes supportent les entiers sur 16 bits). Pour être certain d’avoir des entiers sur 16 bits, il faudrait utiliser (mea-culpa) wxInt16. Mais il est vrai que les « mauvaises » habitudes ont la vie dure.

    // Au choix wxTHREAD_JOINABLE ou détaché par défaut
    MyThread::MyThread(MyFrame *frame) :
    	 wxThread(/*wxTHREAD_JOINABLE*/),
    	 m_frame(frame)
    {
    }
    
    MyThread::~MyThread()
    {
      // on quitte
      std::cout << «~MyThread» << std::endl;
    }
    void MyThread::OnExit()
    {
     std::cout<<»Thread : OnExit»<<std::endl;
     // on indique la fin du thread à la fenêtre
     m_frame->GetCondition()->Signal();
     std::cout<<»Thread : OnExit Signal envoyé”<<std::endl;
    }

    Code 11 : Constructeur, destructeur et sortie du thread

    Ensuite, on rentre dans une boucle qui teste dans un premier temps si le thread a le droit de continuer. Si tel est le cas, on a une boucle de la hauteur de lignes de l’écran qui décale chacune des lignes de façon variable et sinusoïdale en fonction du « temps » et du numéro de la ligne. Puis, une fois ceci réalisé, un événement est envoyé à la fenêtre. Cet événement est similaire à celui d’un menu sélectionné, sauf que dans notre cas il n’y a pas besoin de numéro identificateur, puisqu’il n’y a pas de menu dans notre application. L’instruction Yield permet de donner le temps qui reste (durée gérée par l’ordonnanceur de tâches) à un autre thread. Le point important ici est qu’il y a une obligation de verrouiller l’accès aux ressources graphiques pour pouvoir travailler sur l’image. Pour cela, il existe déjà un verrou prévu. Ceci n’est pas conseillé, mais, dans notre cas, il n’y a pas d’autres solutions pour traiter le problème (à part la seconde solution).

     

    wxThread::ExitCode MyThread::Entry()
    {
     // pour pouvoir travailler sur les images
     wxMemoryDC memDC,dc;
     // événement de type sélection de menu
     wxCommandEvent event(wxEVT_COMMAND_MENU_SELECTED,wxID_ANY);
     // association memDC<==>Bitmap de l’écran
     memDC.SelectObject(*m_frame->GetCopie());
     // même chose avec l’image affichée
     dc.SelectObject(*m_frame->GetImage());
     // compte le nombre de tour
     static int tour=0;
     // variables utilisée pour gérer
     // l’amplitude des ondulations
     static int max=2;
     static int delta_max=2;
     // table des sinus pour éviter de tout recalculer
     int tab_sin[360];
     // largeur et hauteur de l‘image
     int hauteur,largeur;
     dc.GetSize(&largeur,&hauteur);
     for(int i=0;i<360;i++) tab_sin[i]=
    	int(32768*sin(i*2*M_PI/360));
     wxString texte=wxT(“Un economiseur d’ecran”);
     int haut_texte,larg_texte;
     // police de caractères
     wxFont font;
     // récupération de la police utilisée
     font=dc.GetFont();
     // fixer la taille de celle-ci
     font.SetPointSize(60);
     // mettre la même police pour notre fenêtre
     dc.SetFont(font);
     // récupération de la taille réelle d’affichage
     dc.GetTextExtent(texte,&larg_texte,&haut_texte);
     // mettre affichage transparent
     dc.SetBackgroundMode(wxTRANSPARENT);
     // tant que le thread a le droit de fonctionner
     while(!TestDestroy())
     {
       tour++;
       // réglage de la vitesse de croissance des ondulations
       if (tour%2==0) max+=delta_max;
       // décroissance
       if (max>=380) delta_max=-4;
       // croissance
       if (max<2) delta_max=4;
       // on verrouille l’accès aux éléments du graphisme
       wxMutexGuiEnter();
       for(int i=0;i<hauteur;i++)
       {
         // calcul du décalage
         int delta=max+(max*tab_sin[((i+tour))%360])/32768;
         // recopie 1ere partie de ligne
         dc.Blit(delta,i,largeur-delta,1,&memDC,0,i);
         // idem
         dc.Blit(0,i,delta,1,&memDC,largeur-delta,i);
       }
       // affichage centré
       dc.DrawText(texte,(largeur-larg_texte)/2,
    	(hauteur-haut_texte)/2);
       // on libère la ressource
       wxMutexGuiLeave();
       // on envoie un événement à la fenêtre
       m_frame->AddPendingEvent(event);
       // on redonne le temps restant au système
       Yield();
     }
     std::cout<<»MyThread : Entry On quitte»<<std::endl;
     // on quitte
     return NULL;
    }

    Code 12 : Fonction d’entrée et de traitement du thread

    3.3.5 L’application : wxApp

    L’application (dont la déclaration se trouve dans le cadre code 7) est un élément dérivé de wxApp et contient plusieurs méthodes dont OnInit pour l’initialisation de l’application et OnExit pour finir l’application. Les différents éléments graphiques peuvent être détruits dans OnExit, mais pas dans le destructeur.
    C’est la raison pour laquelle le destructeur n’apparaît pas forcément tout le temps. Dans notre cas, on conserve un pointeur sur la fenêtre pour pouvoir ensuite forcer celle-ci à être fermée. Il aurait aussi été possible de chercher les éléments enfants de l’application et de fermer les éléments de type MyFrame, mais cela aurait compliqué le code.

     

     // l’application
    class MyApp : public wxApp
    {
     public:
      // constructeur
      MyApp();
      // fonction «main»
      bool OnInit();
      // on quitte
      ~MyApp() { std::cout << «~MyApp : ~destructeur»
    	 << std::endl; }
      // on quitte
      int OnExit();
      // on termine l’application
      void Finir() { frame->Close(true); }
     public://private:
      // unique fenêtre du système
      MyFrame *frame;
    };

    Code 7 : Déclaration de l’application

    Le cadre code 14 présente l’application qui consiste simplement à créer une fenêtre, l’afficher et ensuite à poursuivre automatiquement dans la boucle d’événements (chose que l’utilisateur n’a pas à refaire, sauf si un comportement particulier était nécessaire). Le fait de mettre un message pour la console dans le destructeur permet d’indiquer dans notre exemple l’ordre d’exécution des différentes méthodes.

     

     // création de l’application
    MyApp::MyApp() : wxApp()
    {
    }
    
    // début de l’application
    // ===> main en C ou winMain de windows
    bool MyApp::OnInit()
    {
     // création de la fenêtre
     frame=new MyFrame;
     // affichage de la fenêtre
     frame->Show(true);
     // passage sur wxApp::OnRun
     // qui traite les événements
     return true;
    }
    
    int MyApp::OnExit()
    {
      std::cout << «MyApp : OnExit» << std::endl;
      // fonction virtuelle
      return wxApp::OnExit();

    }

    Code 14 : Constructeur, destructeur et fonction de fermeture de l’application

    3.4 Deuxième programme

    3.4.1 Introduction

    Le premier programme doit faire le traitement de l’image en utilisant un élément de type wxMemoryDC. Or, ceci oblige à bloquer les ressources graphiques, alors que l’on sent bien que l’on pourrait faire autrement. Il faudrait pour cela avoir accès aux données internes de l’image et pouvoir faire les décalages sans passer par wxMemoryDC. Cependant, il est pour le moment impossible d’accéder aux données stockées (sauf à regarder le code source de la bibliothèque en essayant de rester portable). Il faut pour notre seconde méthode passer par un élément de type wxImage, qui permet certes d’avoir accès aux données, mais qui n’offre pas pour le moment de possibilités graphiques développées. On devra donc transformer pour l’affichage notre image wxImage en image wxBitmap. Dans notre exemple, cela prendra beaucoup de temps sur l’affichage rendant celui-ci lent (sur un Athlon 1,2Ghz en tout cas). Mais l’accès aux données sous forme de valeurs RVB (Rouge Vert Bleu) permet surtout l’accès à des traitements par d’autres bibliothèques. Et cela offre une possibilité d’accès sans blocage des ressources graphiques à plusieurs threads. Comme bien souvent pour un affichage temps réel, on utilisera plutôt une accélération matérielle avec des bibliothèques type OpenGL. Donc, il ne faut pas s’attendre sur cette seconde partie à des résultats exceptionnels (pour cette version de wxWidgets au moins). Les modifications à apporter sont légères, il faudra faire nos décalages de mémoire nous-mêmes et au moment des affichages faire des conversions entre wxImage et wxBitmap.

    3.4.2 La fenêtre

    Toutes les méthodes traitant wxBitmap utilisent maintenant wxImage.

     

     class MyFrame : public wxFrame
    {
     public:
      // ... idem
      wxImage GetImage();
      wxImage GetCopie();
      ... // idem
     private:
      // image stockée de l’écran de l’ordinateur
      wxImage copie;
      // image affichée
      wxImage affiche;
      // ... idem
    };
    
    wxImage MyFrame::GetImage()
    {
      return affiche;
    }
    
    wxImage MyFrame::GetCopie()
    {
      return copie;
    }
    
    MyFrame::MyFrame() :  ... //idem
    {
     // ... idem
     // maintenant ça change un peu
     SetSize(0,0,largeur,hauteur);
     // image de la fenêtre
     wxBitmap bitmap_copie(largeur,hauteur);
     // on réserve de la place
     wxBitmap bitmap_affiche(largeur,hauteur);
     // association bitmap<->mémoire
     memDC.SelectObject(bitmap_copie);
     // on stocke l’écran du système
     memDC.Blit(0,0,largeur,hauteur,&dc,0,0);
     // association bitmap<->mémoire
     memDC.SelectObject(bitmap_affiche);
     // on stocke l’écran du système
     memDC.Blit(0,0,largeur,hauteur,&dc,0,0);
     copie=bitmap_copie.ConvertToImage();
     affiche=bitmap_affiche.ConvertToImage();
     // ensuite ça ne change plus
     m_thread=new MyThread(this);
     // ... idem
    }

    Code 15 : La fenêtre

    3.4.3 Le panneau

    Ici la seule chose qui change, c’est l’affichage. Il faut faire une conversion pour afficher et placer ensuite la partie d’affichage du texte (ou utiliser une autre bibliothèque que wxWidgets pour afficher du texte). Il faut simplement pouvoir traiter des images 24 bits.

     

    void MyPanel::OnPaint(wxPaintEvent& event)
    {
      wxPaintDC dc(this);
      static MyFrame *frame=
    	dynamic_cast<MyFrame*>(GetParent());
      // affichage de l’image avec conversion
      dc.DrawBitmap(wxBitmap(frame->GetImage()),0,0);
      // ici on démarre la gestion du texte affiché
      int hauteur,largeur;
      // CHANGEMENT
      dc.GetSize(&largeur,&hauteur);
      wxString texte=wxT(«Un economiseur d’ecran»);
      // ... idem
      // affichage centré
      dc.DrawText(texte,(largeur-larg_texte)/2,
    	(hauteur-haut_texte)/2);
    }

    Code 16 : Traitement de l’événement de ré-affichage du panneau

    3.4.4 Le threads

    Dans le cadre code 17, le thread commence par obtenir un pointeur de type char* sur les images et ensuite utilise la fonction memcpy pour faire des recopies de blocs en mémoire. Les tests effectués montrent un gain au niveau de la vitesse d’exécution du nouveau thread de l’ordre de 10% à 20%, mais l’affichage du fait de la conversion annule de loin ce gain. Certes, cette version semble plus logique, puisqu’il paraît naturel qu’un traitement d’une image puisse se faire sans bloquer toute la partie graphique. Mais le fait que les capacités actuelles de la classe wxImage soient limitées et ne permettent pas un affichage direct sur un contrôle bride les performances. La documentation laisse bien entrevoir des perspectives futures, mais il faudra attendre encore un peu.

     

    wxThread::ExitCode MyThread::Entry()
    {
     wxImage image_source,image_affiche;
     unsigned char *source,*affiche;
     wxCommandEvent event(
    	wxEVT_COMMAND_MENU_SELECTED,wxID_ANY);
     image_source=m_frame->GetCopie();
     image_affiche=m_frame->GetImage();
     source=image_source.GetData();
     affiche=image_affiche.GetData();
     // ... idem
     hauteur=image_source.GetHeight();
     largeur=image_source.GetWidth();
     // ... idem
     for(int i=0;i<hauteur;i++)
     {
       delta=max+(max*tab_sin[((i+tour))%360])
    	/32768;
       memcpy(affiche+(i*largeur+delta)*3,
    	source+(i*largeur)*3,(largeur-delta)*3);
       memcpy(affiche+(i*largeur)*3,
    	source+(i*largeur+largeur-delta)*3,delta*3);
     }
     // ... idem
     } // fin de while
     std::cout<<»MyThread : Entry On quitte»<<std::endl;
     return NULL;
    }

    Code 17 : Le thread

    3.3.5 L’ordre des exécutions

    Dans le code, il a été placé différents affichages sur la console. Voici ce que vous pouvez voir dans le cadre La console. Dans cet exemple, le thread est détruit après l’application, mais cela n’est pas forcément toujours le cas,.Si vous placez un wxSleep(1) dans OnExit pour arrêter 1s l’application, l’affichage du thread se fera avant la fin de l’application. Il faut impérativement que le thread puisse arriver dans sa méthode OnExit avant que l’application se termine, donc le synchronisme ne doit pas se faire à la fin de la méthode Entry.

     

    MyPanel : OnKeyDown
    MyThread : Entry On quitte
    Thread : OnExit
    ~MyFrame
    ~MyPanel
    MyApp : OnExit
    ~MyApp : ~destructeur
    Thread : OnExit Signal envoyé
    ~MyThread

    La console

    4. Conclusion

    Cette quatrième partie présente une autre méthode pour réaliser l’économiseur précédent en donnant un fonctionnement d’un thread. Pour les heureux propriétaires de processeurs récents multi-cœurs ou de cartes biprocesseurs, l’utilisation de thread peut amener des gains en performance, mais avec le risque d’une augmentation de la complexité de l’application et surtout de la difficulté de débuggage de celle-ci. Il restera à voir une utilisation de l’OpenGL pour réaliser un économiseur. L’utilisation d’une autre bibliothèque de traitement d’image (telle la SDL déjà présentée dans le magazine) peut se faire sur la seconde version de l’économiseur, car on a directement accès aux images sous la forme de données 24 bits. Mais le problème de la vitesse d’affichage reste naturellement présent. Donc, il n’est pas du tout évident de pouvoir améliorer cet économiseur en réalisant ceci. Mais, naturellement, peut être existe-il une meilleure méthode pour cette seconde version ?

    Posté par (La rédaction) | Signature : Olivier Corrio | Article paru dans Creative Commons License

    Laissez une réponse

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


    • Il y a actuellement

    • 666 articles/billets en ligne.