Académique Documents
Professionnel Documents
Culture Documents
Par ramius
www.siteduzero.com
Sommaire
1/17
Sommaire
Sommaire ........................................................................................................................................... 1 Informations sur le tutoriel ................................................................................................................... 0 Le framework Executor ....................................................................................................................... 2
Informations sur le tutoriel ................................................................................................................................................. 2 Un petit rappel ................................................................................................................................................................... 2
L'utilisation des threads ............................................................................................................................................................................................... 2
www.siteduzero.com
Le framework Executor
2/17
Le framework Executor
Bonjour tous, Tout d'abord, sachez que la programmation concurrente est un sujet trs vaste et souvent complexe, rempli de piges et de comportements tous plus tranges les uns que les autres . Dans son big-tuto, Cysboy a introduit les threads et a illustr leur complexit. Je ne vais pas aborder ici les moyens d'assurer la thread-safety (le fait qu'un objet se comporte correctement quand plusieurs threads y accdent), cela fera peut-tre l'objet d'un futur tutoriel. Pour en revenir aux threads, JAV 5 a introduit un nouveau moyen d'excuter les tches en A parallles : le framework Executor.
Je ne vais pas vous l'expliquer en intgralit, mais vous devriez tre mme d'en comprendre les grandes lignes.
Je pars du principe que vous avez dj une certaine pratique de Java et que vous avez dj entendu le mot thread. Si les collections, les gnriques et la programmation Java en gnral vous sont compltement trangers, commencez par le tutoriel de Cysboy.
Sans plus attendre amis zros, voyons ce que nous rserve le framework Executor. Sommaire du tutoriel :
Un petit rappel Les bases Les pools de threads Callable<V> et Future<V> Un peu de structure dans nos programmes Q.C.M.
www.siteduzero.com
Le framework Executor
3/17
Je ne doute pas une seule seconde que les threads n'ont absolument aucun secret pour vous mais juste pour vous rafraichir la mmoire, gnralement on cr un thread de cette faon : On implmente l'interface java.lang.Runnable Code : Java public class MonRunnable implements Runnable { @Override public void run(){ System.out.println("Travail effectuer"); } }
et on l'enveloppe dans un thread : Code : Java public class Main { public static void main(String[] args){ //On crer notre thread avec notre tache executer Thread t = new Thread(new MonRunnable()); //On lance le thread t.start();
Avant Java 5, c'tait aux dveloppeurs dimplmenter les classes ncessaire la politique qu'ils souhaitaient mettre en place (Files d'excutions, pool de threads...).
www.siteduzero.com
Le framework Executor
Ceci tant dit, explorons Executor.
4/17
L'abstraction de base que vous allez utiliser est l'interface Executor. Code : Java public interface Executor { } void execute(Runnable command);
J'aimerais ici faire un petit apart sur les interfaces. Comme l'a trs bien expliqu Cysboy dans son tutoriel les interfaces permettent de crer des super-types et donc de permettre un polymorphisme. De ces deux choses dcoulent le principal intrt des interfaces, le dcouplage. Je m'explique : Une interface A dcrit un certain nombres d'oprations Code : Java public interface A { void methodeA(); void methodeB();
On veut, qelle que soit la classe qui implmente A, faire un traitement particulier. V ous pouvez sans problme crire une mthode qui prend en paramtre une interface, car vous savez quelles mthodes seront implmentes dans les classes qui implmentent votre interface. Si la classe qui implmente l'interface A change (pour une autre classe d'implmentation plus adapte par exemple), la mthode execute (voir exemple ci-aprs) n'aura pas besoin d'tre change. C'est la raison pour laquelle on utilise des interfaces. Code : Java public class Execution { public void execute(A a) { a.methodeA(); a.methodeB();
Crer un framework bas sur les interfaces permet donc une grande souplesse dans l'excution des tches. V ous pourrez excuter n'importe quelle tche qui implmentent Runnable. Comme dit prcdemment, les executors excutent tout comme les threads, des Runnables. Nous allons reprendre l'exemple prcdent en utilisant Executor cette fois : Au lieu de crer un thread avec new Thread(...), nous allons utiliser Executor qui fournit une mthode pour produire le mme effet : la mthode newSingleThreadExecutor().
www.siteduzero.com
Le framework Executor
Code : Java public class Main { public static void main(String[] args) { Executor executor = Executors.newSingleThreadExecutor(); executor.execute(new MonRunnable());
5/17
} }
Il y a 3 interfaces connaitre : Executor : Pour l'excution des "Runnables" ExecutorService : Pour l'excution des tches "Runnables" et "Callables"(les Callables seront expliqus par la suite) ScheduledExecutorService : Pour l'excution des tches priodiques (qui se rpte dans le temps) et diffre (la tche doit commencer dans 60 secondes par exemple)
Cependant vous le savez, on n'instancie pas une interface. Pour crer les bons objets nous allons nous en remettre une seule classe : Executors. Notez bien Executor ; C'est une classe qui contient toutes les mthodes statiques ncessaire la cration d'objets du framework Executor. Dans l'exemple prcdent j'ai utilis cette classe : Code : Java public class Main {
www.siteduzero.com
Le framework Executor
public static void main(String[] args) { Executor executor = Executors.newSingleThreadExecutor(); executor.execute(new MonRunnable());
6/17
La mthode newSingleThreadExecutor(); renvoie un executor mono-thread. Je vous rappelle que vous ne manipulez pas les threads directement. Executor contient plusieurs fabriques qui ne renvoient que des executors mono-thread : Executors.newSingleThreadExecutor() : Executor mono-thread classique Executors.newSingleThreadScheduledExecutor() : Executor mono-thread pour les tches priodiques Nous verrons les autres mthodes par la suite.
Exemple
Prcedemment je vous ai montr le newSingleThreadExecutor() donc je vais vous montrer le newSingleThreadScheduledExecutor(). Pour information pour faire des tches priodiques avec des threads on utilisait les "TimerTask" et la classe utilitaire "Timer". Cependant Timer ne s'appuyait que sur le temps absolu (de votre horloge systme) qui risquait d'tre change, alors que Executor ne sappuie que sur le temps relatif.
Ceci tant dit, sans plus attendre voici un exemple de tche excut toutes les secondes. Code : Java public class Main { public static void main(String[] args){ ScheduledExecutorService execute = Executors.newSingleThreadScheduledExecutor(); //Execute MonRunnable toutes les secondes execute.scheduleAtFixedRate(new MonRunnable(), 0, 1, TimeUnit.SECONDS); } }
Lnumration TimeUnit apporte une plus grande clart au code. Cela vite d'avoir crire le temps en milli-secondes, ce qui est illisible arriv un certain point. Simple non ?
www.siteduzero.com
Le framework Executor
Merci de poser la question Comme vous avez sans doute pu le voir si vous dveloppez dans un IDE (h oui dans le bloc note il n'y a pas d'autocompltion), la classe Executors possde des mthodes aux noms un peu trange : newFixedThreadPool(), newScheduledThreadPool(), newCachedThreadPool()...
7/17
Si vous ne savez pas ce qu'est un pool d'objets, et bien pour simplifier disons que c'est une collection d'objets crs et initialiss puis mis en mmoire. On les utilise quand le cot de cration et d'initialisation d'un objet est assez important. Le fait de mettre certains objets en mmoire peut augmenter les performances dans certain cas mais je ne vais pas rentrer dans les dtails de la performance en java. Les tailles des pools de threads ne sont pratiquement jamais codes en dur. La taille des pools dpend en effet des ressources. Passer de 3 100 processeurs n'est pas ngligeable et le programme doit prendre en compte ces changements. Il existe une mthode pour rcuprer dynamiquement le nombre de processeurs disponibles: Code : Java int proc = Runtime.getRuntime().availableProcessors();
Bref, revenons notre Executors. Le but ici est de ne pas utiliser le mme thread pour toutes les tches mais d'utiliser un thread diffrent pour chacune. Donc il n'y a plus besoin d'attendre qu'une tche soit finie pour excuter la suivante. Allons-y pas pas, nous allons commencer par modifier la classe MonRunnable pour observer dans quel thread on se trouve. La nouvelle classe Runnable : Code : Java public class MonRunnable implements Runnable { @Override public void run() { try { //On simule un traitement long en mettant en pause le Thread pendant 4 secondes Thread.sleep(4000); //On affiche le nom du thread o on se trouve System.out.println("Execution dans le thread " + Thread.currentThread().getName()); } catch (InterruptedException e) { e.printStackTrace(); } } }
Nous allons tout d'abord l'excuter en mono-thread, c'est dire un thread pour toutes nos tches. Code : Java public class Main { public static void main(String[] args) { //La liste qui va stocker les taches effectuer
www.siteduzero.com
Le framework Executor
List<Runnable> runnables = new ArrayList<Runnable>(); //On crer 4 taches (instance de MonRunnable) runnables.add(new MonRunnable()); runnables.add(new MonRunnable()); runnables.add(new MonRunnable()); runnables.add(new MonRunnable()); //Notre executor mono-thread ExecutorService execute = Executors.newSingleThreadExecutor(); //La mthode qui se charge de l'excution des tches executeRunnables(execute, runnables); } public static void executeRunnables(final ExecutorService service, List<Runnable> runnables){ for(Runnable r : runnables){ service.execute(r); } //On ferme l'executor une fois les taches finies //En effet shutdown va attendre la fin d'excution des tches service.shutdown();
8/17
} }
Rsultat l'excution:
Modifions le code pour utiliser un pool de threads : chaque thread est lanc en parallle et excute la tche qui lui est ddie. (j'ai comment le code pour que vous compreniez). Code : Java public class Main { public static void main(String[] args){ List<Runnable> runnables = new ArrayList<Runnable>(); runnables.add(new runnables.add(new runnables.add(new runnables.add(new MonRunnable()); MonRunnable()); MonRunnable()); MonRunnable());
//Cette fois on crer un pool de 10 threads maximum ExecutorService execute = Executors.newFixedThreadPool(10); } executeRunnables(execute, runnables);
public static void executeRunnables(final ExecutorService service, List<Runnable> runnables){ //On excute chaque "Runnable" de la liste "runnables" for(Runnable r : runnables){
www.siteduzero.com
Le framework Executor
service.execute(r); } service.shutdown();
9/17
} }
Rsultat l'excution :
Executor facilite grandement l'excution de tches en parallles. Si tous les threads du pool sont occups les tches sont places dans une file d'attente jusqu' ce qu'un thread soit libre.
www.siteduzero.com
Le framework Executor
}); } catch (IOException e) { e.printStackTrace(); }
10/17
} }
Le traitement des requtes dans un pool permet de contrler le nombre de threads crs. La cration illimite de threads est toujours dangereuse.
Callable<V> et Future<V>
Maintenant que vous savez comment excuter des tches j'aimerais attirer votre attention sur l'interface java.lang.Runnable. Pour rappel voici l'interface Runnable : Code : Java public interface Runnable { } public void run();
Cette interface souffre de deux limitations: La mthode run() ne peut renvoyer aucune valeur(void) Vous ne pouvez lancer aucune exception
Pour pallier ces deux problmes, une nouvelle interface a t dveloppe: (java.util.concurrent.Callable) Code : Java public interface Callable<V> { } public V call() throws Exception;
Callable<V>
V ous pouvez ainsi renvoyer le rsultat d'un calcul qui s'est droul dans la fonction call() sans avoir crer une variable globale,
www.siteduzero.com
Le framework Executor
ou passer en paramtre un objet pour stocker le rsultat. L'exemple suivant est effectivement inutile, mais illustre bien l'implmentation de Callable<V>. Code : Java public class MonCallable implements Callable<Integer> { @Override public Integer call() throws Exception { try { Thread.sleep(4000); //Traitement.... System.out.println("Dans le Callable"); } catch(InterruptedException e){ throw new Exception("Thread interrompu ; cause " + e.getMessage()); } return 3;//On peut retourner une valeur } }
11/17
De la mme manire que les "Runnables", vous pouvez utiliser les Executors pour soumettre des Callables. Cependant, nous n'utiliserons pas la mthode EXECUTE mais SUBMIT. Code : Java public class Main { public static void main(String[] args) { ExecutorService execute = Executors.newSingleThreadExecutor(); execute.submit(new MonCallable()); System.out.println("Apres submit"); } } execute.shutdown();
Je vous rappelle que les oprations sont asynchrones, donc l'excution "Apres submit" va s'afficher avant "Dans le Callable"
Je veux rcuprer le rsultat du Callable un instant prcis je fais comment ? J'utilise la mthode join() des threads, c'est a ?
Heureusement que vous tes la pour posez les questions Et bien non en fait, la methode submit(Callable<V> callable); renvoie un objet
Future<V>.
Le framework Executor
Un Future<V> reprsente le cycle de vie d'une tche. Future<V> est bien sr une interface et submit renvoie un objet qui implmente cette interface; En loccurrence FutureTask. Rien que pour vous zros, voici l'interface Future<V> : Code : Java public interface Future<V> { boolean cancel(boolean mayInterruptIfRunning); boolean isCancelled(); boolean isDone(); V get() throws InterruptedException, ExecutionException, CancellationException; V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, CancellationException, TimeoutException; }
12/17
Revenons ma votre question. Future<V> possde une mthode bien pratique get() qui permet de rcuprer le rsultat envelopp dans l'objet FutureTask. On s'en sert de la faon suivante : Code : Java public class Main { public static void main(String[] args) { ExecutorService execute = Executors.newSingleThreadExecutor(); //On rcupre un objet Future<V> Future<Integer> future = execute.submit(new MonCallable()); try { //future.get() est bloquant, l'excution attend le resultat //et on l'affiche dans la console. System.out.println("Resultat du Callable " + future.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } System.out.println("Apres submit"); } execute.shutdown();
Rsultat de l'excution :
Callable est une abstraction bien suprieure Runnable. Outre le fait de pouvoir renvoyer des rsultats qui ne nous sert parfois rien, le fait de pouvoir lever des exceptions n'est pas prendre la lgre.
www.siteduzero.com
Le framework Executor
13/17
ce qui nous vitent de chercher des solutions des problmes complexes. Comprenez moi bien, je ne vous empche pas d'implmenter vos propres solutions et je vous encourage comprendre et rflchir de nouvelles possibilits, mais pour le bien de votre application utilisez plutt celles prsentes dans le JDK. Pour les applications concurrentes les solutions ne manquent pas comme le service de terminaison.
Le service de terminaison
Un service de terminaison rpond la problmatique suivante : Comment rcuprer les rsultats d'un grand nombre de tches correctement et quand ils sont disponibles ? En ralit il y a plusieurs faon, plus ou moins bonnes, de le faire mais Executor nous en fournit une trs efficace : CompletionService. CompletionService utilise les files, c'est dire le modle producteursconsommateurs. C'est un modle assez simple, les producteurs crent les tches et les soumettent une file, les consommateurs retirent la premire tche et l'excute librant ainsi une nouvelle place dans la file pour une tche future. CompletionService permet de rcuprer un rsultat ds qu'il est disponible, on obtient alors une meilleure ractivit de l'application. CompletionService est une interface et sa classe d'implmentation, prsente dans le JDK et que nous allons utiliser ici, est ExecutorCompletionService. L'approche est la suivante: Nous allons rcuprer une liste de tches excuter et nous allons traiter les rsultats quand ceux-ci sont disponibles. Dans un premier temps, il faut crer les tches qui vont tre excutes. Pour simuler un calcul long, nous allons utiliser Thread.sleep(), la tche prendra en paramtres un entier qui correspondra au nombre de secondes pendant lesquelles le thread sera "endormi". Code : Java public class Task implements Callable<Integer> { private final int sleepTime; public Task(int n) { sleepTime = n; } @Override public Integer call() throws Exception { Thread.sleep(1000 * sleepTime); return sleepTime;
} }
V ous pouvez imaginer que dans la fonction call() se trouve en ralit un calcul trs important qui va prendre plus ou moins de temps en fonction de l'entier pass en paramtre. A prsent que notre classe reprsentant une tche est dclare, nous allons crer plusieurs de ces tches puis les excuter. Code : Java public class TestTask { public static void main(String[] args) { //Notre liste qui va contenir toutes les tches List<Callable<Integer>> taches = new ArrayList<Callable<Integer>>(); //On cr 4 tches avec un paramtre pour le //Thread.sleep() diffrent. Callable<Integer> tache1 = new Task(1); Callable<Integer> tache2 = new Task(5); Callable<Integer> tache3 = new Task(10);
www.siteduzero.com
Le framework Executor
Callable<Integer> tache4 = new Task(2); //On ajoute ces taches la liste taches.add(tache1); taches.add(tache2); taches.add(tache3); taches.add(tache4); pouvoir //On instancie un executor contenant 10 threads pour
14/17
Jusque l rien de nouveau. Si cela vous parait flou, revenez en arrire dans le tutoriel avant de continuer. Nous avons donc nos tches et notre executor qui va traiter ces tches. C'est ici que nous allons introduire notre service de terminaison : ExecutorCompletionService. Le constructeur de cette classe est trs simple : il prend en paramtre un Executor. Code : Java //Le type entre les chevrons dpend du type de vos Callables. //Si vous avez cr des Callable<String> alors vous instancierez un //CompletionService<String>. CompletionService<Integer> completionService = new ExecutorCompletionService<Integer>(executor);
Nous allons dclarer une mthode rsoudre qui va se charger d'excuter les tches travers notre service de terminaison. Pour l'instant, votre mthode devrait ressembler a: Code : Java public static void main(String[] args) { List<Callable<Integer>> taches = new ArrayList<Callable<Integer>>(); Callable<Integer> Callable<Integer> Callable<Integer> Callable<Integer> tache1 tache2 tache3 tache4 = = = = new new new new Task(1); Task(5); Task(10); Task(2);
public static void resoudre(final ExecutorService executor, List<Callable<Integer>> taches) { //Le service de terminaison CompletionService<Integer> completionService = new ExecutorCompletionService<Integer>(executor); } }
www.siteduzero.com
Le framework Executor
15/17
Le service de terminaison contient plusieurs mthodes, mais nous n'allons en utiliser que deux. La premire SUBMIT, qui comme vous l'aurez srement devin, prend en paramtre un Callable<V> et va lancer votre tche. La seconde est TAKE. La mthode take() va en fait vous permettre d'attendre qu'un rsultat soit prt pour l'utiliser. En d'autres mots, quand une tche s'achve, son rsultat est mis dans une file d'attente. La mthode take() va rcuprer le premier rsultat disponible dans cette file, puis va le supprimer. Ainsi un nouvel appel take() vous renverra le rsultat suivant. Si vous vous souvenez bien, la mthode submit d'un executor renvoie un Future<V>. Et bien celle de CompletionService fait de mme. Commenons par le submit : Code : Java //une liste de Future pour rcuprer les rsultats List<Future<Integer>> futures = new ArrayList<Future<Integer>>(); Integer res = null; try { //On soumet toutes les tches l'executor for(Callable<Integer> t : taches){ futures.add(completionService.submit(t)); } } catch(Exception e){}
De cette faon toutes les tches sont lances dans l'executor via notre service de terminaison. Maintenant se pose le problme de la rcupration des rsultats quand ceux-ci sont disponibles. La premire approche consiste faire une boucle sur la liste de Future<V> et en utilisant la mthode get() rcuprer le rsultat. Cela fonctionne, mais vous ne rcuprez pas le rsultat au moment o il est disponible. V otre appel get() va attendre le rsultat du Future<V> en cours mais d'autres tches pourront tre termines pendant ce temps. Le service de terminaison rgle cette question. Comme dit prcdemment, l'aide de take() vous rcuprerez le premier rsultat disponible. Code : Java for (int i = 0; i < taches.size(); ++i) { try { //On rcupre le premier rsultat disponible //sous la forme d'un Future avec take(). Puis l'appel // get() nous donne le rsultat du Callable. res = completionService.take().get(); if (res != null) { //On affiche le rsultat de la tche System.out.println(res);
} } catch(ExecutionException ignore) {}
Je vous remet le code global pour ceux qui auraient un peu de mal Code : Java public class TestTask {
www.siteduzero.com
Le framework Executor
public static void main(String[] args) { List<Callable<Integer>> taches = new ArrayList<Callable<Integer>>(); Callable<Integer> Callable<Integer> Callable<Integer> Callable<Integer> tache1 tache2 tache3 tache4 = = = = new new new new Task(1); Task(5); Task(10); Task(2);
16/17
public static void resoudre(final ExecutorService executor, List<Callable<Integer>> taches){ //Le service de terminaison CompletionService<Integer> completionService = new ExecutorCompletionService<Integer>(executor); //une liste de Future pour rcuprer les rsultats List<Future<Integer>> futures = new ArrayList<Future<Integer>>(); Integer res = null; try { //On soumet toutes les tches l'executor for(Callable<Integer> t : taches){ futures.add(completionService.submit(t)); } for (int i = 0; i < taches.size(); ++i) { try { disponible take(). Puis l'appel Callable. //On rcupre le premier rsultat //sous la forme d'un Future avec // get() nous donne le rsultat du res = if (res != null) { //On affiche le resultat de la } System.out.println(res);
completionService.take().get();
tche
} catch(ExecutionException ignore) {}
www.siteduzero.com
Le framework Executor
V ous devriez voir affichez les tches ds quelles sont termines. (C'est dire l'affichage "1" "2" "5" "10")
17/17
Ce genre de mthode est trs efficace et faire la mme chose avec des threads ncessite une certaine maitrise. Mais les outils existent, alors autant s'en servir. Le framework Executor permet de s'affranchir avantageusement des threads et des Timers. Il nous permet aussi de disposer de pools de threads efficaces, ce genre d'implmentation n'tant pas vraiment la porte des dbutants. L'organisation des tches dans un service de terminaison permet de manipuler avec une grande facilit les rsultats de tches excutes en parallles. Mais t'a pas parler des verrous et des classes thread-safe ?
Je sais, et pour une bonne raison. En effet en parlant de la thread-safety dans ce cours j'aurais t trs long et vous seriez pass cot du framework Executor. Un conseil donc pour finir :
www.siteduzero.com