Threads et programmation concurrente - Programmation II Hiver 2007 Dr Y.Sami Threads et programmation concurrente Objectifs: - Comprendre le concept de thread. - Savoir comment concevoir et écrire des programmes avec plusieurs threads. - Être capable d utiliser d la classe Thread et l interface Runnable. - Comprendre le cycle de vie d un d thread. - Savoir comment synchroniser des threads. - Apprécier l utilisation l de l héritage l dans les applications utilisant les threads. 1 2 Plan du cours Introduction Introduction Qu est est-ce qu un un thread? Les états et le cycle de vie d un d thread. L utilisation des threads pour améliorer le temps de réponse r d une d interface. Étude de cas: Les threads qui coopèrent. Faire plus d une d chose à la fois en alternant entre ces choses : prendre une cuillère de céréales, une gorgée e de café,, un bout de tartine... et ainsi de suite jusqu à ce que le petit déjeuner d se termine. Il y a plusieurs applications oùo le programme a besoin de faire plus d une d chose à la fois ou concurremment. Dans Java, la programmation concurrente est permise grâce aux threads. 3 4 Qu est est-ce qu un un thread? Un thread (thread d exécution ou thread de contrôle) est une séquence d instructions d exécutables à l intérieur d un d programme. Une façon de visualiser un thread est d éd établir la liste d instructions d un d programme telle qu elle s exs exécute par la CPU. Supposons qu on divise le programme en deux ou plusieurs threads.. Chaque thread aura sa propre séquence s d instructions. d À l intérieur d un d thread,, les instructions s exs exécutent séquentiellement, l une l après s l autre. l Cependant, en alternant entre l exl exécution des instructions de plusieurs threads,, l ordinateur l exécute plusieurs threads concurremment. La CPU exécute une instruction à la fois et plusieurs threads concurremment. La machine virtuelle Java (JVM) est un exemple de programme à plusieurs threads.. Les threads de JVM réalisent r les tâches nécessaires à l exécution d un d programme Java. L un L des threads est la thread de ramasse miettes (Garbage( Collector Thread). L exécution concurrente de plusieurs threads: La majorité des ordinateurs personnels sont séquentiels (une seule CPU). Il existe des ordinateurs parallèles (plusieurs CPU). Dans les machines séquentielles, la répartition du temps de CPU entre plusieurs programmes est rendu possible grâce à un algorithme d ordonnancement qui est sous le contrôle du système d exploitation et de la machine virtuelle Java. Le choix de l algorithme d ordonnancement dépend de la plateforme (Windows, Unix,...). Une technique d ordonnancement commune est connue sous le nom de temps partagé (time slicing). Un quantum de temps est alloué à chaque thread. Dans un ordonnancement basé sur les priorités un thread n est préempté que par un thread de plus haute priorité que lui. 5 6 1
Exemple: la classe NumberThread public class NumberThread extends Thread { int num; public NumberThread(int n) { num = n; for (int k=0; k < 10; k++) { System.out.print(num); public class Numbers { public static void main(string args[]) { Thread number1, number2, number3, number4, number5; // 5 threads number1 = new Thread(new NumberThread(1)); number1.start(); // Create and // // start each thread number2 = new Thread(new NumberThread(2)); number2.start(); number3 = new Thread(new NumberThread(3)); number3.start(); number4 = new Thread(new NumberThread(4)); number4.start(); number5 = new Thread(new NumberThread(5)); number5.start(); // main() // Numbers Exécuter ce programme avec 10 itérations dans NumberThread ensuite avec 100 itérations (dans NumberThread). L ordre et le timing d exécution des threads est imprévisible. 7 Une façon de créer des threads dans Java est de définir une sous classe Thread et de réécrire la méthode run(). 8 Une autre façon de créer un thread est de créer une instance de Thread et de lui passer l objet Runnable qui est tout objet qui implémente l interface Runnable (c est à dire la méthode run()). public class NumberPrinter implements Runnable { int num; public NumberPrinter(int n) { num = n; for (int k=0; k < 10; k++) { System.out.print(num); public class Numbers { public static void main(string args[]) { Thread number1, number2, number3, number4, number5; // 5 threads number1 = new Thread(new NumberPrinter(1)); number1.start(); // Create and // start each thread number2 = new Thread(new NumberPrinter(2)); number2.start(); number3 = new Thread(new NumberPrinter(3)); number3.start(); number4 = new Thread(new NumberPrinter(4)); number4.start(); number5 = new Thread(new NumberPrinter(5)); number5.start(); // main() // Numbers L utilisation de l interface Runnable pour la création des threads nous permet de convertir une classe existante en un thread. C est mieux que de redéfinir une classe existante comme sous-classe de thread. 9 10 Le contrôle d un thread: Les différentes méthodes de la classe Thread (start(), stop()) peuvent être utilisées pour contôler l exécution d un thread. La priorité d un thread: La méthode setpriority(int) permet d assigner à un thread une priorité entre Thread.MIN.PRIORITY et Thread.MAX.PRIORITY. L utilisation de la méthode setpriority() permet d avoir un certain contrôle sur les threads. Le thread de plus haute priorité permet de préempter les threads de plus basse priorité. Une façon de coordonner le comportement de deux threads est de donner à un thread une priorité plus haute qu un autre. Un thread de plus haute priorité qui ne rend jamais la CPU peut mettre en état de famine (starvation) les autres processus. Forcer les threads à dormir: Les deux méthodes yield() et sleep() permettent de libérer la CPU, mais la méthode sleep() empêche le thread d être réordonnancé pendant un certain nombre de millisecondes. La méthode sleep permet d endormir un thread pendant un certain nombre de millisecondes. La méthode sleep() lève une exception InterruptedException, elle doit donc être dans un bloc Try/catch. Public void run() { for (int k=0; k<10; k++) { Thread.sleep((long)(Math.random()*1000)); catch (InterruptedException e) { System.out.println(e.getMessage()); System.out.print(num); À moins d assigner des priorités aux threads ou de les synchroniser, ils fonctionnent de manière complètement asynchrone (l ordre d exécution des threads et leur timing est complètement non prévisible). 11 12 2
Les états et le cycle de vie d un d thread L utilisation des threads pour améliorer le temps de réponse r d une interface Définition du problème: Le but est de mesurer la rapidité à laquelle l utilisateur l répond r à certains stimulus. Le programme utilise deux boutons. Lorsque le bouton Draw est cliqué, le programme commence à dessiner des points noirs à des locations aléatoires atoires dans une zone rectangulaire de l él écran. Après s un intervalle de temps aléatoire, atoire, des boutons rouges commencent à se dessiner. Ceci constitue la présentation du stimulus. Aussitôt que le stimulus est présent senté,, l utilisateur l est sensé appuyer sur le bouton Clear pour effacer le contenu de la zone rectangulaire. Pour fournir la mesure du temps de réaction r de l utilisateur, l le programme retourne le nombre de points rouges qui ont été dessinés s avant que l utilisateur l appuie sur le bouton Clear. 13 14 Conception à base d un seul thread. Le programme a besoin de deux classes: - La classe RandomDotApplet: gère l interface répond aux actions de l utilisateur en appelant les méthodes de la classe Dotty. - La classe Dotty: Cette classe contient les méthodes draw() et clear(). 15 import java.awt.event.*; public class RandomDotApplet extends JApplet implements ActionListener { public final int NDOTS = 10000; private Dotty dotty; // The drawing class private JPanel controls = new JPanel(); private JPanel canvas = new JPanel(); private JButton draw = new JButton("Draw"); private JButton clear = new JButton("Clear"); public void init() { getcontentpane().setlayout(new BorderLayout()); draw.addactionlistener(this); clear.addactionlistener(this); controls.add(draw); controls.add(clear); canvas.setborder(borderfactory.createtitledborder("drawing Canvas")); getcontentpane().add("north", controls); getcontentpane().add("center", canvas); getcontentpane().setsize(400, 400); // init() public void actionperformed(actionevent e) { if (e.getsource() == draw) { dotty = new Dotty(canvas, NDOTS); dotty.draw(); else { dotty.clear(); // actionperformed() // RandomDotApplet 16 public class Dotty { private static final int HREF = 20, VREF = 20, LEN = 200; // Coordinates private JPanel canvas; private int ndots; // Number of dots to draw private int ndrawn; // Number of dots drawn private int firstred = 0; // Number of the first red dot public Dotty(JPanel canv, int dots) { canvas = canv; ndots = dots; public void draw() { for (ndrawn = 0; ndrawn < ndots; ndrawn++) { int x = HREF + (int)(math.random() * LEN); int y = VREF + (int)(math.random() * LEN); g.filloval(x, y, 3, 3); // Draw a dot Le problème avec la version avec un seul thread est qu aussi longtemps que la if ((Math.random() < 0.001) && (firstred == 0)) { méthode draw() s exécute, le programme ne peut pas répondre au bouton Clear de g.setcolor(color.red); // Change color to red firstred = ndrawn; l applet. //for // draw() public void clear() { // Clear screen and report result g.setcolor(canvas.getbackground()); g.fillrect(href, VREF, LEN + 3, LEN + 3); System.out.println("Number of dots drawn since first red = " + (ndrawn-firstred)); // clear() // Dotty 17 18 3
Conception à base de plusieurs threads: le thread Dotty Une façon de palier au problème est d introduire un deuxième thread (en plus de l applet) pour faire le dessin. Le thread qui s occupe du dessin sera périodiquement interrompu de manière à permettre à l applet de réagir aux évènements. Le fait d éclater le programme en plusieurs threads n est pas suffisant pour améliorer le temps de réponse. La coordination des threads est nécessaire. Une façon de faire est de laisser Dotty dormir (en utilisant sleep()) après chaque point dessiné. Ainsi si un certain évènement attend d être pris en compte par l applet, il aura la chance de s exécuter. 19 public class Dotty implements Runnable { private static final int HREF = 20, VREF = 20, LEN = 200; // Coordinates private JPanel canvas; private int ndots; // Number of dots to draw private int ndrawn; // Number of dots drawn private int firstred = 0; // Number of the first red dot private boolean iscleared = false; // The panel has been cleared draw(); public Dotty(JPanel canv, int dots) { canvas = canv; ndots = dots; public void draw() { for (ndrawn = 0;!isCleared && ndrawn < ndots; ndrawn++) { int x = HREF + (int)(math.random() * LEN); int y = VREF + (int)(math.random() * LEN); g.filloval(x, y, 3, 3); // Draw a dot if (Math.random() < 0.001 && firstred == 0) { g.setcolor(color.red); // Change color to red firstred = ndrawn; Thread.sleep(1) ; // Sleep for an instant catch (InterruptedException e) { System.out.println(e.getMessage()); //for // draw() public void clear() { iscleared = true; g.setcolor( canvas.getbackground() ); g.fillrect(href,vref,len+3,len+3); g.setcolor(color.black); g.drawstring("dots since first red = " + (ndrawn-firstred),href,vref+len); // System.out.println("Number of dots drawn since first red = " + (ndrawn-firstred) ); // clear() // Dotty 20 import java.awt.event.*; public class RandomDotApplet extends JApplet implements ActionListener { public final int NDOTS = 10000; private Dotty dotty; // The drawing class private Thread dottythread; private JPanel controls = new JPanel(); private JPanel canvas = new JPanel(); private JButton draw = new JButton("Draw"); private JButton clear = new JButton("Clear"); public void init() { getcontentpane().setlayout(new BorderLayout()); draw.addactionlistener(this); clear.addactionlistener(this); controls.add(draw); controls.add(clear); canvas.setborder(borderfactory.createtitledborder("drawing Canvas")); getcontentpane().add("north", controls); getcontentpane().add("center", canvas); getcontentpane().setsize(400, 400); // init() public void actionperformed(actionevent e) { if (e.getsource() == draw) { dotty = new Dotty( canvas, NDOTS ); dottythread = new Thread(dotty); dottythread.start(); else { dotty.clear(); // actionperformed() // RandomDotApplet 21 Étude de cas: Les threads qui coopèrent. Pour certaines applications, il est nécessaire de synchroniser et de coordonner le comportement de plusieurs threads afin qu ils coopèrent à la réalisation r d une tâche. Plusieurs de ces applications sont basées sur le modèle du producteur consommateur. Le thread producteur crée un certain résultat r et le thread consommateur utilise ce résultat. r 22 Définition du problème: Nous voulons simuler une boulangerie où chaque client qui se présente doit prendre un numéro. Ces numéros sont utilisés pour gérer la file. Le distributeur du pain va ensuite annoncé un numéro avant de servir un client. Le client qui se présente doit détenir le même numéro. Les numéros sont annoncés selon l ordre croissant, ce qui permet aux clients d être servis selon leur ordre d arrivée. La simulation utilise quatre classes: -Bakery: responsable de la création des threads et de l amorcement de la simulation. -TakeANumber: représente le gadget qui garde trace du prochain client à servir. -Clerck: Va utiliser TakeANumber pour déterminer le prochain client et va le servir. -Customer: représente les clients qui vont utilser TakeANumber pour prendre leur place dans le file. 23 24 4
La classe TakeANumber: Cette classe doit garder trace de deux choses: -Quel est le numéro du client qui sera prochainement servi. -Quel est le numéro qui sera attribué au prochain client qui se présente. class TakeANumber { private int next = 0; // Next place in line private int serving = 0; // Next customer to serve public synchronized int nextnumber() { next = next + 1; return next; // nextnumber() public int nextcustomer() { ++serving; return serving; // nextcustomer() // TakeANumber public class Customer extends Thread { private static int number = 10000; // Initial ID number private int id; public Customer( TakeANumber gadget ) { id = ++number; sleep( (int)(math.random() * 1000 ) ); System.out.println("Customer " + id + " takes ticket "+ takeanumber.nextnumber()); catch (InterruptedException e) { System.out.println("Exception " + e.getmessage()); // run() // Customer La méthode nextnumber() est déclarée synchronized. Ceci assure qu un seul thread à la fois exécute cette méthode. Ceci assure l exclusion mutuelle de l accès de plusieurs clients à un même objet: TakeANumber. Ceci évite d attribuer un même numéro à deux 25 clients différents. Les variables static sont associées aux classes elles même et non à ses instances. Elles sont souvent utilisées pour associer un identificateur unique à toute instance d une classe. 26 public class Clerk extends Thread { public Clerk(TakeANumber gadget) { while (true) { sleep( (int)(math.random() * 50)); System.out.println("Clerk serving ticket " + takeanumber.nextcustomer()); catch (InterruptedException e) { System.out.println("Exception " + e.getmessage() ); //while //run() // Clerk La méthode sleep est nécessaire pour permettre aux threads Customer de s exécuter. public class Bakery { public static void main(string args[]) { System.out.println( "Starting clerk and customer threads" ); TakeANumber numbergadget = new TakeANumber(); Clerk clerk = new Clerk(numberGadget); clerk.start(); for (int k = 0; k < 5; k++) { Customer customer = new Customer(numberGadget); customer.start(); // main() // Bakery Nous risquons avec cette solution de servir des clients inexistants. Pour résoudre ce problème nous rajoutons une méthode customerwaiting() à TakeANumber. Cette méthode doit retourner true si next>serving. 27 28 class TakeANumber { private int next = 0; // Next place in line private int serving = 0; // Next customer to serve public synchronized int nextnumber() { next = next + 1; return next; // nextnumber() public int nextcustomer() { ++serving; return serving; // nextcustomer() public boolean customerwaiting() { return next > serving; // TakeANumber public class Clerk extends Thread { public Clerk(TakeANumber gadget) { while (true) { sleep((int)(math.random() * 50)); if (takeanumber.customerwaiting()) System.out.println("Clerk serving ticket " + takeanumber.nextcustomer()); catch (InterruptedException e) { System.out.println("Exception " + e.getmessage() ); // while // run() // Clerk 29 30 5
Le problème avec la solution précédente est qu un Customer peut être interrompu entre le moment où il prend un numéro de TakeANumber et le moment où il l affiche. Une section critique est une section d un thread qui ne doit pas être interrompue lors de l exécution. Pour résoudre le problème précédent, il faut traiter toutes les actions de TakeANumber comme sections critiques. Les deux méthodes de TakeANumber doivent être déclarées synchronized. public class TakeANumber { private int next = 0; // Next place in line private int serving = 0; // Next customer to serve public synchronized int nextnumber(int custid) { next = next + 1; System.out.println( "Customer " + custid + " takes ticket " + next ); return next; public synchronized int nextcustomer() { ++serving; System.out.println(" Clerk serving ticket " + serving ); return serving; public synchronized boolean customerwaiting() { return next > serving; // TakeANumber 31 32 public class Clerk extends Thread { public Clerk(TakeANumber gadget) { // for (int k = 0; k < 10; k++) { while (true) { sleep( (int)(math.random() * 1000)); if (takeanumber.customerwaiting()) takeanumber.nextcustomer(); catch (InterruptedException e) { System.out.println("Exception: " + e.getmessage()); // for // run() // Clerk public class Customer extends Thread { private static int number = 10000; // Initial ID number private int id; public Customer( TakeANumber gadget ) { id = ++number; sleep((int)(math.random() * 2000)); takeanumber.nextnumber(id); catch (InterruptedException e) { System.out.println("Exception: " + e.getmessage() ); // run() // Customer 33 34 Le problème avec la solution précédente est que le thread Clerk est busy waiting, il vérifie constamment s il y a un Customer pour le servir. Il consomme inutilement le temps de CPU. Une solution serait de le forcer à attendre (en utilisant wait()) jusqu à ce que un Customer arrive. Dés qu un Customer est prêt, il doit réveiller Clerck en utilisant la méthode notify(). Les modifications à apporter sont: -la méthode nextcustomer en utilisant wait(). -La méthode nextnumber en utilisant notify(). -La suppression de customerwaiting() dans la méthode run() de Clerk Référence: Ralph Morelli, Object oriented problem solving Java, Java, Java, Prentice Hall 2002. 35 6