Threads

Exécution de plusieurs tâches simultanément

Ce site ne sera plus alimenté de contenu après août 2014. Tous les nouveaux articles seront redigés pour www.waitingforcode.com

Contrairement au PHP, Java supporte le multi-threading (possibilité d'effectuer plusieurs actions simultanément). Grâce à cela il est beaucoup plus performant dans les actions composées.

Pour mieux comprendre l'utilité du multi-threading, prenons l'exemple de construction d'une maison. Supposons que l'on a embauché une entreprise de bâtiment pour la réalisation du domicile. Pour une meilleure clarté, la société s'appelera Java. Elle représente le Thread principal (celui qui déclenche et coordonne le processus de construction). L'entreprise en question possède 4 équipes (pour simplifier) : peintres, maçons, ouvriers et couvreurs. Il s'agit aussi des Threads qui seront synchronisés par le Thread principal. La spécificité de la société repose sur le fait que toutes les 4 équipes peuvent travailler presqu'en même temps. Pendant que les couvreurs préparent le toit, les peintres partent acheter des pots de peinture, les maçons mélangent le ciment et les ouvriers aménagent le terrain. Bien sûr, le toit ne peut pas être posé avant les murs qui à leur tour ne peuvent pas être construits sans ciment. Cependant, les opérations sont synchronisées et effectuées toutes en même temps. La construction de la maison a pris 4 mois.

Pour la même situation on emploie la société PHP. Malheuresement, ses équipes ne savent pas travailler en même temps. Cela veut dire que les ouvriers préparent d'abord le terrain. Une fois cette opération terminée, les maçons commencent à mélanger le ciment. Ensuite les ouvrieurs peuvent poser les murs. Les peintres vont alors acheter les pots de peinture. Après cette action ils vont peindre les murs. A la fin, les couvreurs vont préparer le toit et le poser sur la construction. La construction de la maison a pris 16 mois. La différence est flagrante. Elle l'est aussi en programmation si l'on compare les mêmes opérations réalisées par Java et PHP. Dans la plupart des cas, les performances de Java écrasseront celles de PHP.

Dans cet article on abordera plusieurs aspects liés à multi-threading. On commencera par définir un Thread. Ensuite on passera à des exemples plus précis qui contiendront les méthodes de gestion des traitements. On parlera également de relation happens-before, synchronisation des traitements et les problèmes de cohérence.

Qu'est-ce que c'est un Thread en Java ?

Un Thread n'est pas un processus. Un ou plusieurs Threads peuvent composer un processus. En plus, sa création est moins gourmande en ressources que celles d'un processus.

Autrement dit, on peut considérer qu'un processus est comme une pile de traitements. Chaque traitement correspond à un Thread qui peut s'exécuter en parallèle à d'autres actions.

Pour éviter toute confusion dans la suite de l'article, il est nécessaire de préciser que les Threads ne sont pas lancés simultanément. Ils sont exécutés les uns après les autres, selon la priorité attaché à chaque traitement. Et ce n'est que leur exécution qui peut être simultanée.

Comment lancer plusieurs Threads en Java ?

Pour illustrer la construction d'un processus multi-threading, on utilisera l'exemple évoqué au tout début. Voici son implémentation :

afficher le code

Comme on peut le constater, le Thread principal initialise 4 Threads locaux qui correspondent à des métiers évoqués dans le deuxième paragraphe de l'article. Chacun parmi eux hérite de la classe abstraite MainWorker qui, quant à elle, implémente l'interface Runnable. C'est l'un des deux façons de construire un Thread qui s'exécute grâce à l'appel de la méthode run().

La deuxième façon d'initialiser un Thread correspond à l'héritage de la classe Thread. Dans notre exemple on aurait alors :

abstract class MainWorker extends Thread {
    protected House houseInst = null;
    
    public MainWorker(House h) {  
        houseInst = h;
    }
}

La première approche est plus généraliste et permet d'hériter d'une autre classe que Thread. Elle est donc plus facile à manipuler dans le cas d'une application plus complexe que celle décrite ci-dessus. D'ailleurs, elle nous pose déjà pas mal de problèmes. Car, comment les maçons peuvent savoir à quel moment ils peuvent commencer à poser les murs ? Comment les couvreurs peuvent savoir quand commencer à poser le toit ? On suppose que les peintres ont moins de problèmes car ils peuvent peindre après la préparation de construction par ouvriers. Bref, comment ordonner le travail de Threads ? Java possède plusieurs méthodes qui permettent cela. Regardons cela sur notre exemple :

afficher le code

Le résultat sur l'écran :

Workers start working for house construction in London
Bricklayers start working for house construction in London

House construction was done. Now we can put the roof and paint the wall.

Deadline passed. The work will start for 4 seconds.

Roofers start working for house construction in London
Painters start working for house construction in London

Deux nouvelles méthodes ont apparu. La première, join() indique au Thread current qu'il doit attendre l'exécution d'un autre traitement avant de continuer. En occurence, le Thread principal fait appel à des couvreurs et peintres après avoir appelé des ouvriers et des maçons.

En ce qui concerne la fonction sleep(), elle endormit le Thread principal. Dans notre exemple il se réveille au bout de 4 secondes pour reprendre l'exécution des traitements. Ce sont juste des premières mesures qui permettent de synchroniser la réalisation des tâches. Malgré cela, elles amènent déjà la problémtique qui se cache derrière la relation de happens-before.

Relation happens-before en Java

Elle répond à la problématique principale du multi-threading qui est le partage des données. Imaginons que plusieurs tâches manipulent la même variable. L'objectif de l'application est alors la cohérence. Chaque traitement doit pouvoir accéder à la version la plus récente de cette variable. Il s'agit donc de synchroniser l'exécution des Threads.

On a déjà vu une méthode de synchronisation, la fonction join(). Voici comment se présente le code qui illustre cela plus explicitement :

afficher le code

On verra sur l'écran :

Workers start working for house construction in London
Step 1 has been started
Bricklayers start working for house construction in London
Step 2 has been started

House construction was done. Now we can put the roof and paint the wall.

Deadline passed. The work will start for 4 seconds.

Roofers start working for house construction in London
Step 3 has been started
Painters start working for house construction in London
Step 4 has been started

La variable partagée est dans ce cas int step de la classe House. Grâce à la synchronisation avec join(), on peut avoir des valeurs cohérentes. Pour voir la différence, regardons le code sans cette méthode et ses résultats :

t3.start();
t1.start();
System.out.println("\nHouse construction was done. Now we can put the roof and paint the wall. \n");
System.out.println("Deadline passed. The work will start for 4 seconds. \n");
t2.start();
t4.start();
// all thread.join() were been removed

Et le résultat :

House construction was done. Now we can put the roof and paint the wall.

Bricklayers start working for house construction in London
Workers start working for house construction in London
Step 2 has been started
Deadline passed. The work will start for 4 seconds.

Step 1 has been started
Roofers start working for house construction in London
Painters start working for house construction in London
Step 2 has been started
Step 3 has been started

On voit clairement que JVM, dans le but de rendre l'exécution du code plus performante, a reordonné les traitements. En occurence, la machine Java a commencé par la tâche correspondant à l'instance Bricklayer et non pas Worker. Cela a provoqué la valeur incorrecte pour les étapes suivantes (2 au lieu du 3, 3 au lieu du 4).

On appele les problèmes de ce type memory consistency errors (erreurs de cohérence de mémoire). Ils correspondent à des situations où le JVM récupère des valeurs incorrectes des variables partagées (par exemple en les récupérant dans le cache local). Pour les éviter, on peut synchroniser l'exécution de tous les blocks ainsi que la lecture des variables. Cette synchronisation peut être effectuée avec l'un des mots clés : synchronized ou volatile.

Bartosz KONIECZNY Concurrence

Une question ? Une remarque ?

*

*

Moi

Développeur d'applications Internet et journaliste passionné par l'adjectif français. Un aigle polonais orienté vers la progression, volant très haut et écoutant du zouk après les matches du foot français.

Vous appréciez mon travail ?

Pour contribuer au développement de ce site, ou pour remercier pour des articles rédigés, vous pouvez faire un don.

Un conseil JavaScript

Comment supprimer un élément style sous jQuery ?

Ce framework JavaScript permet de manipuler facilement les attributs d'un élément. On peut par exemple spécifier la position de l'image de fond pour l'attribut "style" :

$('#newBackground').css('background-position' , '0 -28px');
Comment faire pour supprimer cet attribut, par exemple dans une fonction callback ? Rien de compliqué, car il suffit tout simplement de passer une valeur vide :
$('#newBackground').css('background-position' , '');