1) Introduction
Le multi-tâches ou multi-threading est une technique de programmation permettant de profiter des avantages de l'utilisation des tâches (threads) qui sont situés, pour ne pas rentrer dans les détails, au niveau de la rapidité. Nous allons voir dans ce tutorial la mise en oeuvre pratique d'un programme multi-tâches simpliste.
2) Créer une tâche
En java, une tâche est en fait une classe implémentant l'interface Runnable.
Cette classe comporte un constructeur et une méthode run, qui sera appelée lors de l'éxecution de la tâche :
public class Affiche_Tache implements Runnable
{
int id; // identifiant de la tâche
// constructeur de la tâche
public Affiche_Tache (int ident)
{
this.id = ident;
}
public void run()
{
// instructions
}
}
3) Lancer une tâche
Le lancement d'une tâche va se dérouler ainsi :
- Création d'un objet
es de la classe
ExecutorService,
- Affectation à
es d'un champ de tâches retourné par la méthode statique
newFixedThreadPool de la classe
Executors, prenant en paramètre le nombre de tâches souhaité,
- Instanciation de la classe Affiche_Tache avec comme paramètre l'identifiant,
- Lancement de cette tâche grace à la méthode execute de la classe ExecutorService,
- Une fois que les instructions de la tâche ont été réalisées, on appele la méthode shutdown.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Application
{
public static void main(String[] args)
{
ExecutorService es;
es = Executors.newFixedThreadPool(2);
Affiche_Tache th = new Affiche_Tache(1);
es.execute(th);
es.shutdown();
}
}
4) Synchronisation
Imaginez maintenant que l'on désire lancer 2 tâches. Leurs instructions respectives vont donc s'executer en même temps. Un problème peut alors subvenir si ces instructions modifient une variable globale au programme par exemple. C'est ici que nous devons faire appel à des méthodes de synchronisations.
Les sémaphores
Java met à disposition une classe
Semaphore. Son constructeur prend en compte 2 paramètres : un
entier désignant la valeur courante de la semaphore qui définit le nombre de ressources disponible initialement et un
booleen indiquant si le mode de gestion doit être en
FIFO.
Dans ce tuto, nous utiliserons les méthodes
acquire et
release.
La méthode acquire vérifie si la ressource est disponible, si oui on laisse la tâche s'executer sinon la tâche est bloquée qu'elle soit libérée par la méthode release qui lui donne la ressource.
Exemple d'utilisation
Ici nous allons synchroniser 2 tâches pour obtenir un affichage cohérent :
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
public class Application
{
public static void main(String[] args)
{
ExecutorService es = Executors.newFixedThreadPool(2);
Semaphore sem1 = new Semaphore(1,true);
Semaphore sem2 = new Semaphore(0,true);
System.out.println("début tâche principale");
// 1ère tâche
AfficheTache th = new AfficheTache(1,sem1,sem2);
es.execute(th);
// 2ème thread
AfficheTache th = new AfficheTache(2,sem2,sem1);
es.execute(th);
System.out.println("fin tâche principale");
es.shutdown();
}
}
Sem1 est initialisé à 1 car lorsque nous allons appeler la méthode acquire dessus dans la première tache, nous avons besoin d'une ressource disponible initialement pour executer les instructions.
Sem2 est initialisé à 0 car dans la 2ème tâche, lorsque nous appelerons acquire, la tâche se bloquera car 0 ressources seront disponibles.
Voici maintenant notre tâche qui a pour but de générer un simple affichage de la tâche en cours.
import java.util.concurrent.Semaphore;
public class AfficheTache implements Runnable
{
int id;
Semaphore prive;
Semaphore voisin;
public AfficheTache (int ident, Semaphore prive, Semaphore voisin)
{
this.id = ident;
this.prive = prive;
this.voisin = voisin;
}
public void run()
{
try { prive.acquire(); }
catch (InterruptedException ex) { System.out.println(ex); }
System.out.println("début tâche T"+id);
for(int i=1;i<=20;i++)
System.out.println("Je suis la tache "+id);
System.out.println("fin tâche "+id);
voisin.release();
}
}
Lorsque la première tâche est lancée, l'utilisation de la méthode
acquire sur
prive permet de regarder si la ressource est disponible, ce qui est positif ici car nous avons initialisé sem1 à 1.
Lorsque la deuxième tâche est lancée, la méthode acquire nous dira que la ressource n'est pas disponible car sem2 a été initialisé à 0, la 2ème tâche va donc se bloquer.
Une fois que l'affichage de la 1ère tâche s'est bien déroulé, nous pouvons libérer la 2ème tâche grâce à
voisin.release(); en lui donnant la ressource, ce qui aura pour effet de passer au 2ème affichage.
Cependant, à l'execution de cet exemple vous vous apercevrez surement que le message
"fin tâche principale" n'apparait pas à la fin de l'exécution des 2 tâches. Pourquoi ? Car il n'a pas été synchronisé et donc il peut s'afficher à n'importe quel moment.
Cela va nous permettre de voir un 2ème méchanisme de synchronisation qui est la barrière.
La barrière
Imaginez pour ce méchanisme une barrière qui reste fermée tant que les 2 tâches n'ont pas été réalisées.
Pour cela nous allons utiliser un objet de la classe
CountDownLatch qui est donc assimilable à une barrière initialement fermée.
A la construction de cet objet, un compteur de blocage est initialisé avec la valeur entière passée en paramètre.
Les tâches qui veulent se bloquer devant cette barrière exécutent la méthode
await. Pour décrémenter la valeur du compteur, il faut appeler la méthode
countDown. Lorsque la valeur atteint 0, c'est que toutes les tâches ont terminé leur travail et donc la barrière s'ouvre.
Modification de l'exemple
Voici le nouveau code prenant en compte une barrière :
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.CountDownLatch;
public class Application
{
public static void main(String[] args)
{
ExecutorService es = Executors.newFixedThreadPool(2);
Semaphore sem1 = new Semaphore(1,true);
Semaphore sem2 = new Semaphore(0,true);
CountDownLatch bar = new CountDownLatch(2);
System.out.println("début tâche principale");
// 1ère tâche
AfficheTache th = new AfficheTache(1,sem1,sem2,bar);
es.execute(th);
// 2ème thread
AfficheTache th = new AfficheTache(2,sem2,sem1,bar);
es.execute(th);
try { bar.await(); }
catch (InterruptedException ex) { System.out.println(ex); }
System.out.println("fin tâche principale");
es.shutdown();
}
}
Ici nous avons 2 tâches donc si nous voulons que la barrière s'ouvre lorsqu'elles sont terminées, il nous faut passer 2 en paramètre lors de la création de la barrière.
import java.util.concurrent.Semaphore;
import java.util.concurrent.CountDownLatch;
public class AfficheTache implements Runnable
{
int id;
Semaphore prive;
Semaphore voisin;
CountDownLatch bar;
public AfficheTache (int ident, Semaphore prive, Semaphore voisin, CountDownLatch bar)
{
this.id = ident;
this.prive = prive;
this.voisin = voisin;
this.bar = bar;
}
public void run()
{
try { prive.acquire(); }
catch (InterruptedException ex) { System.out.println(ex); }
System.out.println("début tâche T"+id);
for(int i=1;i<=20;i++)
System.out.println("Je suis la tache "+id);
System.out.println("fin tâche "+id);
bar.countDown();
voisin.release();
}
}
A présent, à l'execution de cet exmple, le message
"fin tâche principale" s'affichera bien à la suite de l'execution de nos 2 tâches.
MUTEX
Comme vous le savez, lorsque l'on fait du multi-tâches, il y a partage des données. Par exemple, si nous avons une tâche qui modifie puis affiche une variable, nous devons protéger cette variable pendant sa modification.
Ce concept est réalisable grâce aux sémaphores mais l'utilisation d'un MUTEX (MUTual EXclusion) ou verrou peut s'avérer plus pratique.
En java, un mutex est un objet de la classe ReentrantLock dont le constructeur prend un seul paramètre : un booléen dont la valeur true permet une gestion FIFO des tâches en attente sur ce verrou.
Voici le schéma d'utilisation :
import java.util.concurrent.locks.ReentrantLock;
public class AfficheTache implements Runnable
{
ReentrantLock mutex;
int id;
AfficheTache(int id, ReentrantLock mutex)
{
this.mutex = mutex;
this.id = id;
}
public void run()
{
mutex.lock();
try
{
// Bloc d'instructions à protéger
}
catch(InterruptedException ex) { System.out.println(ex); }
finally { mutex.unlock(); }
}
}