IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Penser en Java 2nde édition - Sommaire |  Préface |  Avant-propos | Chapitre : 1  2  3  4  5  6  7  8  9  10  11  12  13  14  15 |  Annexe : A B C D  | Tables des matières - Thinking in Java

  Chapitre 14 - Les Threads multiples

pages : 1 2 3 4 5 6 7 8 9 

14) Les Threads multiples

Les objets permettent une division d'un programme en sections indépendantes. Souvent, nous avons aussi besoin de découper un programme en unités d'exécution indépendantes.

Chacune de ces unités d'exécution est appelé un thread, et vous programmez comme si chaque thread s'exécutait par elle même et avait son propre CPU. Un mécanisme sous-jacent s'occupe de diviser le temp CPU pour vous, mais en général vous n'avez pas besoin d'y penser, c'est ce qui fait de la programmation multi-threads une tâche beaucoup plus facile.

Un processus est un programme s'exécutant dans son propre espace d'adressage. Un système d'exploitation multitâche est capable d'exécuter plusieurs processus (programme) en même temps, en faisant comme si chacun d'eux s'exécutait de façon indépendante, en accordant périodiquement des cycles CPU à chaque processus. Un thread est un flot de contrôle séquentiel à l'intérieur d'un processus. Un processus peut contenir plusieurs threads s'exécutant en concurrence.

Il existe plusieurs utilisations possibles du multithreading, mais en général, vous aurez une partie de votre programme attachée à un événement ou une ressource particulière, et vous ne voulez pas que le reste de votre programme dépende de ça. Vous créez donc un thread associé à cet événement ou ressource et laissez celui-ci s'exécuter indépendamment du programme principal. Un bon exemple est celui d'un bouton « quitter » - vous ne voulez pas être obliger de vérifier le bouton quitter dans chaque partie de code que vous écrivez dans votre programme, mais vous voulez que le bouton quitter réponde comme si vous le vérifiez régulièrement. En fait, une des plus immédiatement indiscutables raisons d'être du multithreading est de produire des interfaces utilisateurs dynamiques. [responsive user interface]

Interfaces utilisateurs dynamiques [Responsive user interfaces]

Comme point de départ, considérons un programme qui contient des opérations nécessitant beaucoup de temps CPU finissant ainsi par ignorer les entrées de l'utilisateur et répondrait mal. Celle-ci, une combinaison applet/application, affichera simplement le résultat d'un compteur actif [running counter] :


//: c14:Counter1.java
// Une interface utilisateur sans répondant.
// <applet code=Counter1 width=300 height=100>
// </applet>
import javax.swing.*;
import java.awt.event.*;
import java.awt.*;
import com.bruceeckel.swing.*;

public class Counter1 extends JApplet {
  private int count = 0;
  private JButton
    start = new JButton("Start"),
    onOff = new JButton("Toggle");
  private JTextField t = new JTextField(10);
  private boolean runFlag = true;
  public void init() {
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    cp.add(t);
    start.addActionListener(new StartL());
    cp.add(start);
    onOff.addActionListener(new OnOffL());
    cp.add(onOff);
  }
  public void go() {
    while (true) {
      try {
        Thread.sleep(100);
      } catch(InterruptedException e) {
        System.err.println("Interrupted");
      }
      if (runFlag)
        t.setText(Integer.toString(count++));
    }
  }
  class StartL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      go();
    }
  }
  class OnOffL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      runFlag = !runFlag;
    }
  }
  public static void main(String[] args) {
    Console.run(new Counter1(), 300, 100);
  }
} ///:~

À ce niveau, le code Swing et de l'applet devrait être raisonnablement familier depuis le chapitre 13. La méthode go() est celle dans laquelle le programme reste occupé: il met le valeur courante de count dans le JTextField t, avant d'incrémenter count.

La boucle infinie à l'intérieur de go() appelle sleep(). sleep() doit être associé avec un objet Thread, ce qui montre que toute application a un thread qui lui est associé. (En effet, Java est basé sur des threads et il y en a toujours qui tourne avec votre application.) Donc, que vous utilisiez explicitement les threads ou non, vous pouvez accéder au thread courant utilisé par votre programme avec Thread et la méthode static sleep()

Notez que sleep() peut déclencher une InterruptedException, alors que le déclenchement d'une telle exception est considéré comme un moyen hostile d'arrêter un thread et devrait être découragé. (Encore une fois les exceptions sont destinées aux conditions exceptionnelles et non pour le controle du flot normal.) L'interruption d'un thread en sommeil sera incluse dans une future fonctionnalité du langage.

Quand le bouton start est pressé, go() est appelé. En examinant go(), vous pouvez naïvement pensé (comme je le fais) que cela autorisera le multithreading parce que elle se met en sommeil [it goes to sleep]. C'est le cas, tant que la méthode n'est pas en sommeil, tout ce passe comme si le CPU pouvait être occupé à gérer les autres boutons. [=That is, while the method is asleep, it seems like the CPU could be busy monitoring other button presses. ] Mais il s'avère que le vrai problème est que go() ne s'achève jamais, puisqu'il s'agit d'une boucle infinie, ce qui signifie que actionPerformed() ne s'achève jamais. Comme vous êtes bloqué dans actionPerformed() par la première touche appuyée, le programme ne peut pas capturer les autres événements. (Pour sortir vous devez tué le processus; le moyen le plus facile de le faire est de presser les touches Control-C dans la console, si vous avez lancé le programme depuis la console. Si vous l'avez démarrer dans un navigateur, vous devez fermer la fenêtre du navigateur.)

Le problème de base ici est que go() doit continuer de réaliser ses opérations, et dans le même temps elle doit retourner à la fonction appelante de façon à ce que actionPerformed() puisse se terminer et que l'interface utilisateur puisse continuer à répondre à l'utilisateur. Mais une méthode conventionnelle comme go() ne peut pas continuer et dans le même temps rendre le contrôle au reste du programme. Cela à l'air d'une chose impossible à accomplir, comme si le CPU devait être à deux endroits à la fois, mais c'est précisément l'illusion que les threads permettent.

Le modèle des threads (et son support de programmation Java) est une commodité de programmation pour simplifier le jonglage entre plusieurs opérations simultanées dans un même programme. Avec les threads, le temp CPU sera éclaté et distribué entre les différentes threads. Chaque thread a l'impression d'avoir constamment le CPU pour elle toute seule, mais le temps CPU est distribué entre les différentes threads. L'exception à cela est si votre programme tourne sur plusieurs CPUs. Mais ce qui est intéressant dans le threading c'est de permettre une abstraction par rapport à cette couche, votre code n'a pas besoin de savoir si il tournera sur un ou plusieurs CPUs. Ainsi, les threads sont un moyen de créer de façon transparente des programmes portables.

Le threading réduit l'efficacité informatique, mais la net amélioration dans la conception des programmes, la gestion de ressources [resource balancing] , et le confort d'utilisation est souvent valable. Bien sûr, si vous avez plus d'un CPU, alors le système d'exploitation pourra décider quel CPU attribué à quel jeux de threads ou attribué une seule thread et la totalité du programme pourra s'exécuter beaucoup plus vite. Le multi-tâche et le multithreading tendent à être l'approche la plus raisonnable pour utiliser les systèmes multi-processeurs.

Héritage de Thread

Le moyen le plus simple de créer une thread est d'hériter de la classe Thread, qui a toutes les connexions nécessaires pour créer et faire tourner les threads. La plus importante méthode de Thread est run(), qui doit être redéfinie pour que le thread fasse ce que vous lui demander. Ainsi, run(), contient le code qui sera exécuté « simultanément » avec les autres threads du programme.

L'exemple suivant créée un certain nombre de threads dont il garde la trace en attribuant à chaque thread un numéro unique, généré à l'aide d'une variable static. La méthode run() du Thread est redéfinie pour décrémenter le compteur à chaque fois qu'elle passe dans sa boucle et se termine quand le compteur est à zéro (au moment où run() retourne, le thread se termine).


//: c14:SimpleThread.java
//  Un example très simple de Threading.

public class SimpleThread extends Thread {
  private int countDown = 5;
  private static int threadCount = 0;
  private int threadNumber = ++threadCount;
  public SimpleThread() {
    System.out.println("Making " + threadNumber);
  }
  public void run() {
    while(true) {
      System.out.println("Thread " +
        threadNumber + "(" + countDown + "#004488">")");
      if(--countDown == 0) return;
    }
  }
  public static void main(String[] args) {
    for(int i = 0; i < 5; i++)
      new SimpleThread().start();
    System.out.println("All Threads Started");
  }
} ///:~

Une méthode run() a toujours virtuellement une sorte de boucle qui continue jusqu'à ce que le thread ne soit plus nécessaire, ainsi vous pouvez établir la condition sur laquelle arrêter cette boucle (ou, comme dans le cas précédent, simplement retourner de run()). Souvent, run() est transformé en une boucle infinie, ce qui signifie que à moins qu'un facteur extérieur ne cause la terminaison de run(), elle continuera pour toujours.

Dans main() vous pouvez voir un certain nombre de threads créé et lancée. La méthode start() de la classe Thread procède à une initialisation spéciale du thread et appelle ensuite run(). Les étapes sont donc: le constructeur est appelé pour construire l'objet, puis start() configure le thread et appelle run(). Si vous n'appelez pas start() (ce que vous pouvez faire dans le constructeur, si c'est approprié) le thread ne sera jamais démarré.

La sortie d'une exécution de ce programme (qui peut être différente d'un exécution à l'autre) est:


Making 1
Making 2
Making 3
Making 4
Making 5
Thread 1(5)
Thread 1(4)
Thread 1(3)
Thread 1(2)
Thread 2(5)
Thread 2(4)
Thread 2(3)
Thread 2(2)
Thread 2(1)
Thread 1(1)
All Threads Started
Thread 3(5)
Thread 4(5)
Thread 4(4)
Thread 4(3)
Thread 4(2)
Thread 4(1)
Thread 5(5)
Thread 5(4)
Thread 5(3)
Thread 5(2)
Thread 5(1)
Thread 3(4)
Thread 3(3)
Thread 3(2)
Thread 3(1)

Vous aurez remarqué que nulle part dans cet exemple sleep() n'est appelé, mais malgré cela la sortie indique que chaque thread a eu une portion de temps CPU dans lequel s'exécuter. Cela montre que sleep(), bien que relié à l'existence d'un thread pour pouvoir s'exécuter, n'est pas impliqué dans l'activation et la désactivation du threading. C'est simplement une autre méthode [NDT: de la classe Thread].

Vous pouvez aussi voir que les threads ne sont pas exécutés dans l'ordre où ils sont créés. En fait, l'ordre dans lequel le CPU s'occupe d'un ensemble de threads donné est indéterminé, à moins que vous ne rentriez dans l'ajustement des priorités en utilisant la méthode setPriority() de Thread.

Quand main() créée les objets Thread elle le capture aucune des références sur ces objets. Un objet ordinaire serait la proie rêvée pour le garbage collector, mais pas un Thread. Chaque Thread « s'enregistre » lui-même, il y a alors quelque part une référence sur celui-ci donc le garbage collector ne peut pas le détruire.

Threading pour une une interface réactive

Il est maintenant possible de résoudre le problème de Counter1.java avec un thread. Le truc est de placer une sous-tâche  —  est la boucle de la méthode go() —  dans la méthode run() du thread. Quand l'utilisateur presse le bouton start, le thread est démarré, mais quand la creation du thread est terminé, donc que le thread tourne, le principal travail du programme (chercher et répondre aux événements de l'interface utilisateur) peut continuer. Voici la solution:


//: c14:Counter2.java
// Une interface utilisateur réactive grace aux threads.
// <applet code=Counter2 width=300 height=100>
// </applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import com.bruceeckel.swing.*;

public class Counter2 extends JApplet {
  private class SeparateSubTask color="#0000ff">extends Thread {
    private int count = 0;
    private boolean runFlag = color="#0000ff">true;
    SeparateSubTask() { start(); }
    void invertFlag() { runFlag = !runFlag; }
    public void run() {
      while (true) {
       try {
        sleep(100);
      } catch(InterruptedException e) {
        System.err.println("Interrupted");
      }
       if(runFlag)
         t.setText(Integer.toString(count++));
      }
    }
  }
  private SeparateSubTask sp = null;
  private JTextField t = new JTextField(10);
  private JButton
    start = new JButton("Start"),
    onOff = new JButton("Toggle");
  class StartL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      if(sp == null)
        sp = new SeparateSubTask();
    }
  }
  class OnOffL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      if(sp != null)
        sp.invertFlag();
    }
  }
  public void init() {
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    cp.add(t);
    start.addActionListener(new StartL());
    cp.add(start);
    onOff.addActionListener(new OnOffL());
    cp.add(onOff);
  }
  public static void main(String[] args) {
    Console.run(new Counter2 (), 300, 100);
  }
} ///:~

Counter2 est un programme simple, son seul travail est de préparer et maintenir l'interface utilisateur. Mais maintenant, quand l'utilisateur presse le bouton start, le code gérant l'événement n'appelle plus de méthode. Au lieu de ça un thread de la classe SeparateSubTask est créer et la boucle d'événements de Counter2 continue.

La classe SeparateSubTask est une simple extension de Thread avec un constructeur qui lance le thread en appelant start(), et un run() qui contient essentiellement le code de « go() » de Counter1.java.

La classe SeparateSubTask étant une classe interne, elle peut accéder directement au JTextField t dans Counter2; comme vous pouvez le voir dans run(). SeparateSubTask peut accéder au champ t sans permission spéciale malgré que ce champ soit déclaré private dans la classe externe  —  il est toujours bon de rendre les champs « aussi privé que possible » de façon à ce qu'ils ne soient pas accidentellement changés à l'extérieur de votre classe.

Quand vous pressez le bouton onOff, runFlag est inversé dans l'objet SeparateSubTask. Ce thread (quand il regarde le flag) peut se démarrer ou s'arrêter tout seul. Presser le bouton onOff produit un temps de réponse apparent. Bien sûr, la réponse n'est pas vraiment instantanée contrairement à ce qui se passe sur un système fonctionnant par interruptions. Le compteur ne s'arrête que lorsque le thread a le CPU et s'aperçoit que le flag a changé.

Vous pouvez voir que la classe interne SeparateSubTaskest private, ce qui signifie que l'on peut donner à ces champs et méthodes l'accès par défaut (excepté pour run() qui doit être public puisque elle est public dans la classe de base). La classe interne private ne peut être accéder par personne sauf Counter2, les deux classes sont ainsi fortement couplés. Chaque fois que vous constatez que deux classes apparaissent comme fortement couplées, vous remarquerez le gain en codage et en maintenance que les classes internes peuvent apportées.

Ce livre a été écrit par Bruce Eckel ( télécharger la version anglaise : Thinking in java )
Ce chapitre a été traduit par Cédric Babault ( groupe de traduction )
télécharger la version francaise (PDF) | Commandez le livre en version anglaise (amazon) | télécharger la version anglaise
pages : 1 2 3 4 5 6 7 8 9 
Penser en Java 2nde édition - Sommaire |  Préface |  Avant-propos | Chapitre : 1  2  3  4  5  6  7  8  9  10  11  12  13  14  15 |  Annexe : A B C D  | Tables des matières - Thinking in Java