Blog ENI : Toute la veille numérique !
🐠 -25€ dès 75€ 
+ 7 jours d'accès à la Bibliothèque Numérique ENI. Cliquez ici
Accès illimité 24h/24 à tous nos livres & vidéos ! 
Découvrez la Bibliothèque Numérique ENI. Cliquez ici
  1. Livres et vidéos
  2. Apprendre la Programmation Orientée Objet avec le langage Java
  3. Le multithreading
Extrait - Apprendre la Programmation Orientée Objet avec le langage Java (avec exercices pratiques et corrigés) (3e édition)
Extraits du livre
Apprendre la Programmation Orientée Objet avec le langage Java (avec exercices pratiques et corrigés) (3e édition) Revenir à la page d'achat du livre

Le multithreading

Introduction

La programmation multithread est un domaine passionnant mais qui peut devenir rapidement très complexe à mettre au point. Plusieurs exécutions parallèles au cœur de votre application devront partager des informations, s’attendre, échanger... Le succès d’une architecture de ce type repose avant tout sur une analyse solide. Ce chapitre n’a pas vocation de vous exposer toutes les possibilités de programmation multithread et leurs implémentations en Java mais de vous en présenter l’essentiel avec la philosophie POO.

Comprendre le multithreading

Un processus peut effectuer des traitements longs qui vont "bloquer" l’application durant leurs exécutions. Pour éviter cela, le développeur peut créer une sorte de chemin d’exécution parallèle qui va prendre en charge ce traitement et ainsi délester l’exécuteur principal. Dans ce cas le système d’exploitation Windows de Microsoft partage très rapidement le temps machine entre les différents flux d’exécution (typiquement 20 ms par tranche de temps) donnant l’illusion d’une exécution simultanée. On parle de système d’exploitation préemptif. Le contenu d’une file d’exécution peut enchaîner tous les traitements qu’elle souhaite sans se soucier du temps que cela prendra globalement.

Le système d’exploitation viendra "l’interrompre" périodiquement pour donner du temps à la file d’exécution suivante et ainsi de suite jusqu’à revenir sur elle pour qu’elle reprenne son traitement là où il avait été interrompu.

Reprenons pour exemple cette carte d’acquisition d’entrées/sorties équipée d’une interface de programmation très sommaire. Le constructeur nous livre un jeu de fonctions permettant, entre autres...

Multithreading et Java

La partie encapsulation de processus est proposée au travers de la classe java.lang.Process. Comme la création d’un processus est étroitement liée au système d’exploitation dans lequel fonctionne la machine virtuelle, il faut passer par une classe très spécialisée qui s’appelle Runtime. Chaque application Java dispose de façon native d’une instance unique sur un objet de type Runtime et c’est grâce à elle que vous pourrez démarrer un nouveau processus.

L’extrait de code suivant permet d’exécuter le programme Windows calc.exe depuis un programme Java.

package demoprocess; 
 
import java.io.IOException; 
 
 
public class DemoProcess { 
 
    public static void main(String[] args) { 
 
        // Récupération d'une référence sur le "runtime" 
        Runtime runtime = Runtime.getRuntime(); 
 
        try { 
            // Utilisation de sa méthode exec 
            runtime.exec("calc.exe"); 
        } catch (IOException ex) { 
            // Si l'exécution se passe mal  ...

Implémentation des threads en Java

Il existe deux façons principales de programmer des threads en Java : étendre la classe Thread ou implémenter l’interface Runnable.

1. Étendre la classe Thread

En étendant la classe Thread et en plaçant le code à dérouler dans la méthode run, reprise à votre compte, votre classe devient directement "threadable". Après son instanciation, un simple appel à la méthode start permet de démarrer le thread et d’exécuter "en parallèle" le code contenu dans sa méthode run. Le thread s’arrête dès que le code de la méthode run a été entièrement déroulé ou qu’une exception non gérée a été levée. Pratique, non ?

Voici un exemple de code utilisant ce principe :

package demothread; 
 
 
// MaClasseThread étend la classe java.lang.Thread 
public class MaClasseThread extends java.lang.Thread { 
 
    //... 
    //... On imagine ici différentes méthodes et accesseurs 
    //... 
 
    // On place dans la méthode run le traitement "long" 
    // qui va être exécuté dans un thread instancié 
    // et démarré depuis le code appelant  
    // (le main dans cet exemple) 
    @Override 
   public void run(){ 
 
        // Trace de début de traitement. 
        // On utilise la méthode de type static 
        // Thread.currentThread() 
        // pour afficher le nom attribué à ce thread 
        System.out.println("Début d'un traitement de 10 sec " 
                + "dans le thread "  
                + Thread.currentThread().getName()); 
 
        // Ici on simule un travail de 10 secondes (10 x 1000 ms) 
        for(int i=0; i<10; i++) 
        { 
       ...

Synchronisation entre threads

1. Nécessité de la synchronisation

La programmation de plusieurs chemins d’exécution ne pose aucun problème particulier jusqu’à ce qu’ils partagent les mêmes informations ou les mêmes ressources... En effet, étant donné que le système d’exploitation peut interrompre les traitements à n’importe quel moment, il risque d’y avoir des objets en cours de modification dans un thread préempté qui se retrouveront dans des états instables pour le thread suivant. Pour se prémunir de ces dysfonctionnements il faut "synchroniser" les threads c’est-à-dire protéger des zones délicates de traitements.

Cela ne va pas jouer sur le système de gestion qui continuera à activer les threads les uns après les autres ; simplement lorsqu’un thread A aura besoin d’accéder à une donnée commune protégée qu’un thread B n’aura pas terminé de mettre à jour alors le thread A devra "attendre le prochain tour". Et si au prochain tour le travail du thread B n’est toujours pas terminé alors il devra attendre le suivant, etc.

Même principe s’il s’agit d’un traitement commun que le thread B devra avoir terminé avant que le thread A ne puisse l’effectuer à son tour. C’est ce scénario que l’extrait de code suivant propose. En effet le traitement permet d’afficher un comptage de zéro jusqu’à neuf effectué par dix threads. L’objectif à atteindre est l’affichage suivant :

0123456789   
0123456789    
0123456789    
0123456789   
0123456789    
0123456789   
0123456789  
0123456789   
0123456789    
0123456789  
  

Et voici une première version de code "sans protection" :

package demothreadsanssynchro; 
 
 
public class TraitementSansProtection{ 
 
    public void ExecuterLeTraitement() { 
 
        for(int i=0; i<10; i++){ 
            new Thread(new Runnable() 
            { 
 ...

Communication interthreads

1. La méthode join

Comme vu précédemment, la méthode join permet à un thread principal de "s’endormir" en attendant la fin de l’exécution d’un thread secondaire.

Exemple de code

using System; 
using System.Threading; 
 
namespace SynchroInterThreads 
{ 
  class Program 
  { 
    static void Main(string[] args) 
    { 
      Test t = new Test(); 
      t.TraitementPrincipal(); 
    } 
  } 
 
  class Test 
  { 
    public void TraitementPrincipal() 
    { 
      Console.WriteLine("Début TraitementPrincipal"); 
      ThreadStart ts 
        = new ThreadStart(TraitementSecondaire); 
      Thread t = new Thread(ts); 
      t.IsBackground = false; 
      t.Priority = ThreadPriority.Highest; 
      t.Name = "C'est mon thread :)"; 
      t.Start(); 
 
 
      t.Join(); 
 
      Console.WriteLine("Fin TraitementPrincipal"); 
    } 
 
    private void TraitementSecondaire() 
    { 
      Console.WriteLine("Début TraitementSecondaire"); 
      Thread.Sleep(1000 * 10); 
      Console.WriteLine("Fin TraitementSecondaire"); 
    } 
 
  } 
 
} 

Sortie console associée :

Début TraitementPrincipal 
Début TraitementSecondaire 
Fin TraitementSecondaire 
Fin TraitementPrincipal 
Appuyez sur une touche pour continuer... 

Sortie console associée SANS la ligne t.Join(); :

Début TraitementPrincipal 
Fin TraitementPrincipal 
Début TraitementSecondaire 
Fin TraitementSecondaire 
Appuyez sur une touche pour continuer... 

Un thread endormi ou bloqué ne consomme pas de temps machine.

L’utilisation de la méthode join est très efficace quand on veut se synchroniser sur la fin d’un traitement entier....

Exercice

1. Énoncé

En partant de l’exemple précédent (avec Thread.sleep(500); dans la boucle de consommation), vous devez introduire la notion de gestion de flux entre producteur et consommateur. Le thread de production doit "s’endormir" quand un nombre maximum de trames en attente d’être traité est atteint (dix par exemple). Le thread de consommation viendra lire ces trames puis, quand la file sera vide, le thread de production devra reprendre son travail.

Type de comportement souhaité :

debug: 
Début Traitement global 
Début ProducteurDeTrames 
Appuyez sur Enter pour arrêter... 
Début ConsommateurDeTrames 
Consommateur attend 
Trame reçue : 46 
Trame consommée : 46 
Trame reçue : 67 
Trame reçue : 23 
Trame reçue : 92 
Trame reçue : 20 
Trame reçue : 23 
Trame consommée : 67 
Trame reçue : 54 
Trame reçue : 21 
Trame reçue : 60 
Trame reçue : 66 
Trame consommée : 23 
Trame reçue : 2 
Trame reçue : 58 
 
Trame reçue : 69 
 
File saturée 
Trame consommée : 92 
Trame consommée : 20 
Trame consommée : 23 
Trame consommée : 54 
Trame consommée : 21 
Trame consommée : 60 
Trame consommée : 66 
Trame consommée : 2 
Trame consommée : 58 
Trame consommée : 69 
File dé-saturée 
Trame reçue : 97 
Trame reçue : 76 
Trame reçue : 41 
Trame reçue : 89 
Trame consommée : 97 
Trame reçue : 12 
 
Abandon demandé 
InterruptedException dans ProducteurDeTrames 
Fin ProducteurDeTrames 
InterruptedException dans ConsommateurDeTrames 
Fin ConsommateurDeTrames 
Fin Traitement global 
BUILD SUCCESSFUL (total time: 9 seconds) 

Voici quelques informations pour vous aider :

Dans le code de départ, le développeur a fait jouer un double rôle au producteur : produire et synchroniser le consommateur pour qu’il s’endorme quand il n’y a rien à lire... Une solution possible à la nouvelle problématique serait de faire de même avec le consommateur...