Vous êtes sur la page 1sur 52

Dan Garlasu, dgarlasu@yahoo.

com

Si vous cherchez à devenir ingénieur logiciel senior, vous devez probablement


être conscient de l'importance des concepts de fils multiple d’exécution
(multithreading) et de simultanéité. Avec l'essor rapide des machines multicœurs,
les ingénieurs capables de gérer habilement leur complexité sont aujourd'hui les
candidats les plus recherchés par la plupart des entreprises technologiques.
Ces concepts peuvent sembler plus intimidants qu'ils ne le sont en réalité. Nous
voulons dissiper les craintes autour du multithreading et vous présenter les
bases. Nous vous présenterons les pratiques de multithreading et de
concurrence en Java

1
Ordre du jour
1. Introduction
2. Propriétés des processus concurrents
 Sûreté/Sécurité (Safety)
 Vivacité (Livleness)
3. Programmation concurrente en Java
 Notions de base sur les fils d’exécution (threads) en Java
 Le cycle de vie d’un fil d’exécution
 Priorité des fils d’exécution
 Synchronisation des fils d’exécution

Course content available at: coronet.iicm.tugraz.at/sa/scripts/lesson11.doc

2
Concurrence
 Un programme séquentiel a un seul fil (thread) de contrôle. Son
exécution s'appelle un processus.
 Le processus est une abstraction d'un programme en cours
d'exécution.

 Un programme concurrent a plusieurs fils de contrôle. Ils peuvent


être exécutés en tant que processus parallèles mais ca dépend
des ressources disponibles.
 Cette leçon présente les grands principes de la programmation
simultanée.

Concurrence
La concurrence (simultanéité) est la capacité de votre programme à traiter (ne pas faire)
plusieurs choses à la fois et est obtenue grâce au multitraitement (multithreading). Ne
confondez pas la concurrence avec le parallélisme qui consiste à faire plusieurs choses à la
fois.

La simultanéité concerne plusieurs tâches qui démarrent, s'exécutent et se terminent dans


des périodes de temps qui se chevauchent, sans ordre spécifique. Le parallélisme concerne
plusieurs tâches ou sous-tâches de la même tâche qui s'exécutent littéralement en même
temps sur un matériel avec plusieurs ressources informatiques comme un processeur
multicœur.

3
Un programme concurrent peut être exécuté par:
 Multiprogrammation – les processus partagent un ou plusieurs
processeurs
 Multitraitement – chaque processus s'exécute sur son propre
processeur mais avec une mémoire partagée
 Traitement distribué – chaque processus s'exécute sur son propre
processeur connecté par un réseau aux autres
Dans tous ces cas, les principes de base de la programmation simultanée
(concurrente) sont les mêmes

4
 Le système d'exploitation fournit généralement une abstraction
logique pour le matériel sous-jacent, et les programmes concurrents
sont basés sur de telles abstractions.
 Ainsi, au niveau de la programmation, les programmeurs ne
distinguent pas les environnements matériels à processeur unique
ou multiple, ils écrivent des programmes en utilisant des
abstractions logiques telles que :
◦ fils,
◦ processus,
◦ ressources, etc...

Programmes, processus et fils d'exécution


Les systèmes d'exploitation actuels peuvent exécuter plusieurs programmes en même temps. Par
exemple, vous lisez ce cours dans votre navigateur (un programme) mais vous pouvez également
écouter de la musique sur votre lecteur multimédia (un autre programme).
Les processus sont ce qui exécutent réellement le programme. Chaque processus est capable
d'exécuter des sous-tâches simultanées appelées fils d’exécution.
Les fils d’exécution sont des sous-tâches de processus et s'ils sont correctement synchronisés, ils
peuvent donner l'illusion que votre application s’exécute tout en même temps.

Sans les fils d’exécution, vous devriez écrire un programme par tâche, l’exécuter en tant que
processus et synchroniser via le système d'exploitation. Exemple: le cadre de traitement de données
non structuré Map/Reduce de Hadoop.

5
 Meilleures performances dans un environnement matériel multitraitement.
 Meilleures performances pour les applications distribuées et avides en E/S
◦ par exemple: pendant qu’un processus attend une réponse d'un réseau ou d'un
périphérique d'entrée/sortie, d'autres fils d’exécution peuvent continuer avec le processeur
 Les programmes simultanés modélisent les interactions utilisateur-ordinateur
et les applications de contrôle de processus, plus naturellement que les
applications séquentielles ordinaires
◦ par exemple: un fil spécial attend une action de l'utilisateur et lance d'autres fils en
fonction de l'action et du contexte actuel
 Certaines applications sont concurrentes par nature et ne peuvent tout
simplement pas être modélisées de manière séquentielle (applications
multimédia).
 Les programmes simultanés sont capables de coordonner les services
distribués

Le multitraitement (multithreading) est une technique qui permet l'exécution concurrente (simultanée)
de deux ou plusieurs parties d'un programme pour une utilisation maximale d'un processeur. Comme
exemple très basique, le multithreading vous permet d'écrire du code dans un programme et
d'écouter de la musique dans un autre. Les programmes sont constitués de processus et de fils
d’exécution. Vous pouvez y penser comme ceci :
• Un programme est un fichier exécutable comme chrome.exe
• Un processus est une instance d'exécution d'un programme. Lorsque vous double-cliquez sur l'icône
Google Chrome sur votre ordinateur, vous lancez un processus qui exécutera le programme Google
Chrome.
• Le fil est la plus petite unité exécutable d'un processus. Un processus peut avoir plusieurs fils
d’exécution avec un fil principal. Dans l'exemple, un seul fil pourrait afficher l'onglet actuel dans lequel
vous vous trouvez, et un fil différent pourrait être un autre onglet.

6
 Sureté: les processus simultanés peuvent corrompre les
données partagées
 Vivacité: les processus peuvent «s’affamer» s'ils ne sont pas
correctement coordonnés
 Non-déterminisme: le même programme exécuté deux fois
peut donner des résultats différents
 Surcoût d'exécution (run-time overhead): construction de
fils d’exécution, commutation de contexte

Sureté (Sécurité) des fils


La sécurité des fils est un concept qui signifie que différents fils d’exécution peuvent accéder aux
mêmes ressources sans exposer un comportement erroné ou produire des résultats
imprévisibles(non-déterministes) comme une condition de compétition pour une ressource ou un
blocage. La sécurité des fils peut être obtenue en utilisant diverses techniques de synchronisation.

La commutation de contexte est la technique dans laquelle le temps du processeur central est
partagé entre tous les processus en cours d'exécution et est essentielle pour le multitâche.

7
 Processus
◦ Un processus est un programme en cours d'exécution. Le système
d'exploitation alloue toutes les ressources nécessaires au processus.
 Fils
◦ Un fil est une unité de répartition au sein d'un processus. Cela signifie
qu'un processus peut avoir un certain nombre de fils en cours d'exécution
dans le cadre de ce processus particulier

8
 La principale différence entre les fils d’exécution et les processus peut être définie comme suit:
◦ alors qu’un fil a accès à l'espace d'adressage mémoire et aux ressources de son processus, un processus ne peut
pas accéder aux variables allouées à d'autres processus.
 Cette propriété fondamentale nous amène à un certain nombre de conclusions:
1. la communication entre les fils créés au sein d'un même processus est simple car les fils partagent toutes les
variables. Ainsi, une valeur produite par un fil est immédiatement disponible pour tous les autres fils.
2. les fils prennent moins de temps pour démarrer, s'arrêter ou permuter que les processus, car ils utilisent l'espace
d'adressage déjà alloué pour le processus en cours.
 Différents systèmes d'exploitation traitent les processus et les fils différemment:
◦ MS-DOS prend en charge un seul processus utilisateur et un seul fil d’exécution,
◦ UNIX prend en charge plusieurs processus utilisateur avec un seul fil par processus,
◦ Solaris OS, Linux, Windows NT/2000/XP prennent en charge plusieurs processus et plusieurs fils
◦ Dans iOS, chaque processus est composé d'un ou plusieurs fils, chacun représentant une voie d'exécution unique
à travers le code de l'application.
◦ Android : modèle à fil unique
 Si un système d'exploitation permet au moins plusieurs processus, on peut dire que le système
d'exploitation prend en charge la programmation simultanée

9
 Les programmes simultanés sont régis par deux principes clés.
Ce sont les principes de "sureté" et de "vivacité".
1. Le principe de «sureté» stipule que «rien de mal ne devrait arriver».
2. Le principe de «vivacité» stipule «finalement, quelque chose de bien aura se
produire»
 En général, la sureté signifie que:
 un seul fil peut accéder aux données à la fois, et
 garantit que les données restent dans un état cohérent (consistent) pendant et
après l'opération

10
 Supposons que les fonctions "A" et "B" ci-dessous s'exécutent
simultanément. Quelle est la valeur résultante de "x" ?
var x = 0;
function A()
{x = x + 1;}
function B()
{x = x + 2;}
 x = 3 si les opérations x = x + 1 et x = x + 2 sont atomiques, c'est-à-
dire qu'elles ne peuvent pas être interrompues.
 x = 1,2 ou 3 si les opérations x = x + 1 et x = x + 2 peuvent
s'interrompre l’une par l’autre.
 Si nous lisons/modifions/écrivons un fichier et permettons aux
opérations de s'interrompre, le fichier peut être facilement corrompu

11
 La sureté est assurée par la mise en place de:
◦ "l'exclusion mutuelle" et
◦ "synchronisation des conditions"
lorsque vous travaillez sur des données partagées.
 "L'exclusion mutuelle" signifie qu'un seul fil d’exécution peut accéder
aux données à la fois et garantit que les données restent dans un
état cohérent pendant et après l'opération (mise à jour atomique).
 "Synchronisation des conditions" signifie que les opérations peuvent
être retardées si les ressources partagées sont dans le mauvais état
◦ par exemple, lire à partir d'un tampon vide

Verrouillage
L’exclusion mutuelle s’acheve par verrouillage. Les verrous sont une fonctionnalité très importante qui
rend possible le multithreading. Les verrous sont une technique de synchronisation utilisée pour
limiter l'accès à une ressource dans un environnement où il existe de nombreux fils d'exécution. Un
bon exemple de verrou est un mutex.

Mutex
Mutex, comme son nom l'indique, implique une exclusion mutuelle. Un mutex est utilisé pour protéger
des données partagées telles qu'une liste liée, un tableau ou tout type primitif simple. Un mutex
permet à un seul thread d'accéder à une ressource.

12
 L'exclusion mutuelle résout de nombreux problèmes de sureté, mais
donne lieu à d'autres problèmes, notamment le blocage et la famine.
 Le problème de blocage survient lorsqu’un fil détient un verrou sur
un objet et bloque la tentative d'obtenir un verrou sur un autre objet,
car le deuxième objet est déjà verrouillé par un fil différent, qui est
bloqué par le verrou que le fil d'origine détient actuellement.

XXX

Les deux fils d’exécution s'installent pour attendre que l'autre libère le verrou
nécessaire, mais aucun fil ne libérera jamais son propre verrou car ils sont
tous les deux bloqués en attendant que l'autre verrou soit libéré en premier.
En d'autres termes, cela peut sembler peu probable, mais en fait, le blocage
est l'un des bogues de programmation simultanée les plus courants.
Le problème est que le blocage s'étend sur plus de deux fils et peut
impliquer des interdépendances complexes.

13
 Le blocage est une forme extrême de famine.
 La famine se produit lorsqu’un fil ne peut pas continuer car il ne peut pas
accéder à une ressource dont il a besoin.

 Les problèmes de blocage et de famine nous amènent au prochain grand


sujet de la programmation concurrente - la vivacité.
 Les programmes simultanés sont également décrits comme ayant une
propriété de "vivacité" s'il y a:
◦ Pas de blocage: certains processus peuvent toujours accéder à une ressource partagée
◦ Pas de famine: tous les processus peuvent éventuellement accéder aux ressources partagées
XXX

La propriété de vivacité indique que finalement quelque chose de bien se produit. Les
programmes bloqués ne répondent pas à cette exigence.
La vivacité est graduelle. Les programmes peuvent être « presque » morts ou « pas
très » actives. Chaque fois que vous utilisez une méthode synchronisée, vous forcez
l'accès séquentiel à un objet. Si vous avez beaucoup de fils appelant beaucoup de
méthodes synchronisées sur le même objet, votre programme ralentira beaucoup.

Comment éviter la famine ?


La meilleure façon d'éviter la famine est d'utiliser un verrou tel que ReentrantLock ou
un mutex. Cela introduit un verrou "équitable" qui favorise l'accès au fil d’exécution qui
attend depuis le plus longtemps. Si vous souhaitez que plusieurs fils s'exécutent à la
fois tout en évitant la famine, vous pouvez utiliser un sémaphore.

14
 Un langage de programmation doit fournir des mécanismes pour
exprimer la concurrence:
◦ Création de processus - comment spécifiez-vous des processus
concurrents ?
◦ Communication: comment les processus échangent-ils des informations?
◦ Synchronisation: comment les processus maintiennent-ils la cohérence?
 La plupart des langages concurrents offrent une variante des
mécanismes de création de processus:
◦ Co-routines: pour spécifier qu’il y a des processus concurrents
◦ Fork and Join: pour permettre aux processus échanger des informations
◦ Cobegin/coend: assurer que les processus maintiennent la cohérence

15
 les co-routines ne sont que pseudo-concurrentes et nécessitent
des transferts de contrôle explicites :

 Les co-routines peuvent être utilisées pour implémenter la plupart


des mécanismes concurrents de niveau supérieur

16
 Fork peut être utilisé pour créer n'importe quel nombre de processus :

 Join attend qu'un autre processus se termine.


 Fork et Join (bifurcation et la jointure) ne sont pas structurées, elles nécessitent
donc soin et discipline

17
 Les blocs Cobegin/coend sont mieux structurés, mais ils ne
peuvent créer qu'un nombre fixe de processus

 L'appelant continue lorsque tous les coblocs sont terminés

18
 Il existe différentes techniques de synchronisation dont la puissance
expressive est quasi équivalente et qui peuvent être utilisées pour
s'implémenter mutuellement.
 Chaque approche met l'accent sur un style de programmation
différent.
◦ Approche orientée sur la procédure
1. Busy-Waiting (primitif mais efficace). Traite de manière atomique les variables
partagées et les teste
2. Sémaphores,
3. Moniteurs,
4. Expressions de voie (Path expression)
◦ Approche orientée sur le message (passage de message)
◦ Approche orientée sur opération (appel de procédure à distance; RPC – Remote Procedure Call)

19
 La synchronisation des conditions est simple à mettre en œuvre:
◦ pour signaler une condition, un processus définit une variable partagée
◦ pour attendre qu'une condition soit remplie, un processus teste à plusieurs
reprises la variable

var iAmReady = false; function B()


function A() {
{ ...
... if (!iAmReady) {
// signal (!) // wait 300 mseconds
iAmReady = true; setTimeout("B()",300);return;}
... ...
} }

20
 L'exclusion mutuelle est plus difficile à réaliser correctement et
efficacement.
 Les sémaphores ont été introduits par Dijkstra (1965) en tant que primitives
de niveau supérieur pour la synchronisation des processus.
 Un sémaphore est une variable "s" non négative à valeur entière avec deux
opérations :
◦ P(s) : délai jusqu'à s>0 puis exécute atomiquement s := s-1
◦ V(s) : exécute atomiquement s:= s+1
 Dijkstra a proposé d'avoir les deux opérations down(P) et up(V).
L'opération down sur un sémaphore vérifie si la valeur est supérieure à 0.
Si c'est le cas, elle décrémente la valeur (utilise un réveil stocké) et
continue. Si la valeur est 0, le processus est mis en repos sans achever le
down pour le moment (fonctionnement atomique).

21
 De nombreux problèmes peuvent être résolus en utilisant des
sémaphores binaires, qui prennent les valeurs 0 ou 1:
var i_have_message = new Semaphore(0);
var message = new String();
function Sender(mX)
{
...
message = mX;
// semaphor down (!)
V(i_have_message);
...
}
function receiver()
{
...
P(i_have_message);
// message is available
...
}

22
 Un moniteur est un processus unique qui function editDocument(dX)
{
encapsule des ressources et fournit des ...
opérations de verrouillage/déverrouillage lock(dX);
(Lock/Unlock) qui les manipulent. // read, edit and save
// document "dX";
◦ lock(r): délai jusqu'à ce que "r" soit marqué ...
comme "déverrouillé", puis marqué unLock(dX);
atomiquement comme "verrouillé". ...
◦ unLock(r): marque atomiquement la ressource }
comme déverrouillé (unlock). function editDocument(dX)
{
 Les appels de moniteur imbriqués doivent ...
être spécialement gérés pour éviter les lock(dX);
blocages // read, edit and save
// document "dX";
 Introduit en 1974 par Hoare et Brinch ...
Hansen (1975) unLock(dX);
...
}

23
 Les expressions de voie expriment la séquence d'opérations autorisée sous la
forme d'une sorte d'expression régulière:
buffer : ((put && get) || (write && read))*
◦ Bien qu'elles expriment avec élégance des solutions à de nombreux problèmes, les
expressions de voie sont trop limitées pour la programmation concurrente générale.
 Le transfère de messages combine la communication et la synchronisation:
◦ l'expéditeur spécifie le message et une destination: soit un processus, un port, un
ensemble de processus, ...
◦ le récepteur spécifie des variables de message et une source: la source peut ou non être
explicitement identifiée
 Cette méthode de communication interprocessus utilise les deux primitives,
envoi et reçoit qui, comme les sémaphores, et contrairement aux moniteurs,
sont des appels de système, plutôt que des constructions de langage.

24
function sender(mX)
{
...
Le transfère de messages peut être: m = mX;
send(m,"receiver");
• asynchrone: les opérations d'envoi ne
...
bloquent jamais }
• mis en mémoire tampon: l'expéditeur peut
bloquer si la mémoire tampon est pleine
• synchrone: émetteur et récepteurs prêts function receiver()
{
var Messages = new Buffer(); . . .
while(Messages.length > 0);
{
// take and process a message
// message = Messages[0];
...
}
...

25
 La méthode de transfère function sender(mX)
{
de messages synchrones ...
est également connue m = mX;
sous le nom de "Rendez- // point of Rendezvous
// wait if the receiver is not ready
vous" send(m,"receiver");
...
}
function receiver()
{
var Message = new Buffer(); . . .
// point of Rendezvous
// wait if the sender is not ready
x = Message;
...
}

L'autre extrême d'avoir des boîtes postales est d'éliminer toute mise en mémoire
tampon. Lorsque cette approche est suivie, si l'envoi est effectué avant la
réception, le processus d'envoi est bloqué jusqu'à ce que la réception se
produise, moment auquel le message peut être copié directement de l'expéditeur
au destinataire, sans mise en mémoire tampon intermédiaire. De même, si la
réception est effectuée en premier, le récepteur est bloqué jusqu'à ce qu'un envoi
se produise. Cette stratégie s'appelle le rendez-vous. Il est plus facile à
implémenter qu'un schéma de messages tamponné mais est moins flexible car
l'expéditeur et le destinataire sont obligés de fonctionner en parallèle.

26
 Le langage de programmation Java prend en charge le modèle de
processus multifils en tant que partie intégrante du langage de
programmation, c'est-à-dire qu'il existe des opérations spéciales
pour créer, démarrer, exécuter et arrêter plusieurs fils dans le cadre
d'une application Java.
 Dans ce cours, nous utilisons le langage de programmation Java
pour illustrer les grands principes de la programmation concurrente
 Le mécanisme de base pour le support des fils d’exécution en
langage Java se fait avec les classes suivantes qui appartiennent à
la bibliothèque standard:
 Classes Timer et TimerTask
 Classe Thread (fil)
 Runnable interface (Interface exécutable)

27
 La classe Timer propose deux manières de planifier une tâche :
◦ Exécuter la tâche une fois après un délai
◦ Effectuer la tâche à plusieurs reprises après un délai initial
public TimerTaskExample()
{
timer = new Timer();
timer.schedule(new myTask(),0,1000);
// after delay "0" msec
// create an instance of myTask
// invoke method "run" defined for myTask
// repeat the process every second
}
class myTask extends TimerTask
{
... Les sous-classes de la classe
public void run() { ... } TimerTask remplacent la
} méthode d'exécution abstraite
pour implémenter une tâche
particulière

28
 La classe Timer n'est qu'une sous-classe d'une classe Thread plus
générale.
 Fondamentalement, la classe Thread fournit l'abstraction d’un fil
d'exécution dans un programme Java.
 Ainsi, si les programmeurs veulent démarrer leurs propres fils
d'exécution, ils implémentent des sous-classes de la classe Thread
et remplacent sa méthode run pour implémenter la séquence
d'exécution pour ce fil particulier.
 Cependant, les classes Timer et TimerTask fournissent une
implémentation utile d'un mécanisme de planification de tâches
simple, et elles doivent être utilisées chaque fois que leur
fonctionnalité est suffisante pour l'application.

29
 Runnable est une classe d'interface du paquet Java standard. Cette
classe n'a qu'une seule méthode publique - la méthode run.
 Ainsi, une classe implémentant cette interface fournit la définition de la
méthode run pour un fil d’exécution Java:
public class myThread implements Runnable
{ Cependant, ce fil doit être
private String name_; démarré ailleurs dans le code et
public myThread(String name) pointé vers la méthode run de la
{ classe implémentant l'interface
name_ = name; Runnable
}
...
public void run() { . . .}
}

myThreadOne = new myThread("myThreadOne");
myThread myThreadTwo = new myThread("myThreadTwo");

30
 Un soi-disant état d'exécution, est l'une des propriétés de base du fil
 un fil
◦ peut être en cours d'exécution,
◦ il peut être arrêté,
◦ prêt, etc...

31
 Il existe des méthodes spéciales qui provoquent les transitions d'état
d'exécution. Nous pouvons identifier les états suivants et les méthodes
correspondantes :
◦ "Créé", après avoir créé une nouvelle instance de la classe Thread
◦ "Runnable", après avoir soit :
1. appelé la méthode start de l'instance Thread nouvellement créée, ou
2. repris une instance de fil non exécutable ("Not Runnable").
◦ "Not Runnable":
1. après avoir appelé la méthode "sleep" de l'instance du fil "runnable",
2. l'instance du fil "runnable" appelle la méthode "wait", ou
3. les blocs d'instance de fils exécutables sur une opération d’E/S.
◦ "Dead", après la terminaison de l'instance du fil d'exécution de la méthode run.

32
 En pratique, afin de faciliter les transitions d'états d’un fil, la classe Thread
devrait fournir une interface publique qui peut être utilisée pour manipuler
des instances de la classe
public class myThread implements Runnable
{
private String name_;
private Thread thread_ = null;
public myThread(String name)
{
name_ = name;
}
...
public void run() { . . .}
// Inteface to start the thread
public void start(){
if(thread_ == null){
thread_ = new Thread(this);
// thread is created
thread_.start();}}
// thread is running
//
// Inteface to stop the thread
public void stop(){thread_ = null;}
}

33
 La méthode de lancement Start
◦ crée les ressources de système nécessaires à l'exécution du fil,
◦ planifie l'exécution du fil, et
◦ appelle la méthode run du fil
. . . // Inteface to start the thread
public void start()
{
if(thread_ == null)
{
thread_ = new
Thread(this);
// thread is created
thread_.start();
}
// thread is running
//
}

34
 Une fois la méthode Start évaluée, le fil est "en cours d'exécution"
 Comme le montre la figure, un fil qui a été démarré est en fait à l'état
exécutable Runnable.
 Pour les ordinateurs ayant un seul processeur, il est impossible
d'exécuter tous les fils "en cours d'exécution" en même temps.
 Le système d'exécution Java doit implémenter un schéma de
planification qui partage le processeur entre tous les fils "en cours
d'exécution". Ainsi, à tout moment, un fil "en cours d'exécution" peut en
fait attendre son tour pour l’access au processeur.

35
 Un fil devient "non exécutable" (Not Runnable) lorsque l'un des
événements suivants se produit :
◦ sa méthode "sleep" est invoquée;
◦ le fil appelle la méthode "wait" pour attendre qu'une condition spécifique
soit satisfaite;
◦ le fil est bloqué sur les E/S.
 La liste suivante décrit la voie d'évacuation pour chaque entrée
dans l'état non exécutable "Not Runnable":
◦ Si un fil a été mis en repos, le nombre de millisecondes spécifié doit
s'écouler;
◦ Si un fil attend une condition, un autre objet doit avertir le fil en attente d'un
changement de condition en appelant notify ou notifyAll.
◦ Si un fil est bloqué sur les E/S, les E/S doivent se terminer.

Par exemple, si un fil a été mis en repos, le nombre de millisecondes spécifié doit
s'écouler avant que le fil redevienne executable "Runnable".

36
 Il existe deux méthodes pour arrêter un fil d’exécution:
1. une méthode run peut se terminer naturellement.
 Par exemple, la méthode run peut incorporer une boucle while finie - le fil itérera un nombre
prédéfini de fois, puis la méthode run se terminera

...
public void run()
{
int i = 0;
while (i < 10)
{
...
i++;
}
}
..
2. un fil peut définir une méthode publique spéciale à utiliser pour "arrêter" le fil
public void stop()
{
thread_ = null;
}

37
 Conceptuellement, les fils s'exécutent simultanément. En pratique,
ce n'est pas tout à fait correct pour les configurations d'ordinateurs
ayant un seul processeur, de sorte que les fils s'exécutent en fait un
à la fois de manière à fournir une illusion de simultanéité.
 L'exécution de plusieurs fils sur un seul processeur, dans un certain
ordre, est appelée planification (scheduling). Java prend en charge
un algorithme de planification déterministe très simple, appelé
planification à priorité fixe. Cet algorithme planifie les fils en fonction
de leur priorité par rapport aux d’autres fils exécutables.
 Lorsqu’un fil Java est créé, il hérite la priorité du fil qui l'a créé.

38
 La priorité d’un fil peut être modifiée à tout moment à l'aide de la méthode
setPriority.
 Les priorités des fils sont des nombres entiers.
 L'entier supérieur signifie la priorité la plus élevée.
 À tout moment, lorsque plusieurs fils sont prêts à être exécutés, le système
d'exécution choisit le fil exécutable avec la priorité d'exécution la plus élevée.
 Un fil de priorité inférieure s'exécute uniquement lorsque tous les fils de
priorité supérieure s'arrêtent ou deviennent inexécutables pour une raison
quelconque.
 Ainsi, un fil choisi, s'exécute jusqu'à ce que l'un des fils de priorité supérieure
devient exécutable, ou qu'il soit arrêté. Ensuite, le deuxième fil a une chance
de s'exécuter, et ainsi de suite, jusqu'à ce que l'interpréteur quitte.

39
 La priorité du fil peut être définie dynamiquement, si elle est
prédéfinie par la définition de classe correspondante
public class myThread implements Runnable
{
private String name_;
private Thread thread_ = null;
public myThread(String name, int priority)
{
name_ = name;
priority_ = priority;
}
...
public void run() { . . .}
// Inteface to start the thread
public void start(){
if(thread_ == null){
thread_ = new Thread(this);
// thread is created
thread_.setPriority(priority_);
thread_.start();}}
// thread is running with priority ("priority_")
//
// Inteface to stop the thread
public void stop(){thread_ = null;}
}

40
 Notez que le constructeur de myThread prend maintenant un
argument entier pour définir la priorité de son fil. Nous définissons la
priorité du fil dans la méthode start:
myThreadOne = new myThread("myThreadOne",1);
myThread myThreadTwo = new myThread("myThreadTwo",2);

41
 Supposons que la méthode run soit définie comme suit:
public void run()
{
int i = 0;
String r = "";
while ((thread_ == Thread.currentThread()) && (i < 10))
{
r = r + i + " " + name_;
i++;
}
System.out.println(r);
}
 La boucle while de la méthode run est une boucle serrée. Une fois que le planificateur a
choisi un fil avec ce corps de fil pour l'exécution, le fil n'abandonne jamais volontairement le
contrôle du CPU - le fil continue de s'exécuter jusqu'à ce que la boucle while se termine
naturellement ou jusqu'à ce que le fil soit préempté par un autre fil de priorité plus élevée.
Les fils démontrant un tel comportement sont appelés des fils égoïstes!

Dans certaines situations, avoir des fils égoïstes ne pose aucun problème car un
fil de priorité plus élevée devance le fil égoïste. Cependant, dans d'autres
situations, les fils avec des méthodes d'exécution avides en CPU peuvent
prendre le contrôle du CPU et faire attendre longtemps d'autres fils avant d'avoir
une chance de s'exécuter.

42
 Certains systèmes, tels que Windows NT/2000/XP, combattent le
comportement égoïste des fils d’exécution par la stratégie de
découpage temporel (time slicing). Le découpage temporel intervient
lorsqu'il existe plusieurs fils exécutables de priorité égale et que ces
sont les fils de priorité la plus élevée en compétition pour le CPU:
myThread myThreadOne = new myThread("myThreadOne",2);
myThread myThreadTwo = new myThread("myThreadTwo",2);

 Désormais, les fils "myThreadOne" et "myThreadTwo" seront soumis


à un algorithme de découpage temporel, si cet algorithme est pris en
charge par l'implémentation actuelle de la machine virtuelle Java

43
 Dans un cas général, les fils concurrents partagent des données et
analysent les activités d'autres fils.
 Un tel ensemble de situations de programmation est connu sous le
nom de scénarios producteur/consommateur où le producteur génère
un flux de données qui est ensuite consommé par un consommateur.
 Dans la programmation orientée sur l’objet, partager des données
équivaut à partager un objet public class Producer
extends Thread {
private IS storage_;
public Producer(IS storage)
{
storage_ = storage;
}
public void run()
{
for (int i = 0; i < 10; i++)
{
storage_.put(i);
System.out.println
("Producer put: " + i);

44
}
}
}
public class Consumer
extends Thread {
private IS storage_;
public Consumer(IS storage)
{ public class IS
storage_ = storage; {
} private int value_;
public void run() public void put(int value){value_ = value;}
{ public int get(){return value_;}
int value; }
for (int i = 0; i < 10; i++)
{
value = storage_.get();
System.out.println
("Consumer got: " + value);
}
}
}

45
 Le programme principal démarrant tous les fils et créant une instance partagée de IS, peut
ressembler à ceci:
IS storage = new IS();
Producer put: 0 Producer producer = new Producer(storage);
Producer put: 1 Consumer consumer = new Consumer(storage);
Consumer got: 1 producer.start();
consumer.start();

 Si nous exécutons l'exemple ci-dessus, nous pourrions remarquer les problèmes suivants :
◦ Un problème survient lorsque le Producteur est plus rapide que le Consommateur et génère deux
valeurs consécutives avant que le Consommateur ait une chance de consommer la premiere. Ainsi,
le Consommateur sauterait une valeur;
◦ Un autre problème qui peut survenir est lorsque le Consommateur est plus rapide que le Producteur
et consomme deux fois la même valeur. Dans cette situation, le consommateur imprimerait la même
valeur deux fois et pourrait produire une sortie qui ressemblait à ceci: Producer put: 0
Consumer got: 0
Consumer got: 0
◦ Dans tous les cas, le résultat est faux.
◦ Le consommateur doit obtenir chaque valeur produite par le producteur, exactement une fois

Des problèmes comme celui-ci sont appelés conditions de course (race


conditions). Ils proviennent de plusieurs fils s'exécutant de manière asynchrone
essayant d'accéder à un seul objet en même temps et obtenant le mauvais
résultat.

Comment éviter les conditions de course ?


Les conditions de course (compétition) se produisent dans la section critique de
votre code. Ceux-ci peuvent être évités avec une bonne synchronisation des fils
dans les sections critiques en utilisant des techniques telles que les verrous, les
variables atomiques et le transfère de messages.

46
 Les activités du Producteur et du Consommateur doivent être
synchronisées de deux manières :
◦ deux fils ne doivent pas accéder simultanément un objet partagé. Un fil Java peut
empêcher un tel accès simultané en verrouillant un objet. Lorsqu'un objet est
verrouillé par un fil et qu'un autre fil invoque une méthode synchronisée sur le
même objet, le deuxième fil se bloque jusqu'à ce que l'objet soit déverrouillé.
◦ deux fils doivent coordonner leurs opérations Put/Get. Par exemple, le producteur
indique que la valeur est prête et le consommateur indique que la valeur a été
récupérée.
 La classe Thread fournit une collection de méthodes - wait, notify
et notifyAll - pour construire de tels mécanismes de coordination

47
 Les segments de code d'un programme qui accèdent au même objet à partir
de fils distincts et simultanés sont appelés sections critiques.
 Une section critique est un bloc ou une méthode identifiée par le mot clé
synchronized. Java associe un verrou à chaque objet qui a du code
synchronisé
public class IS
{
private int value_;
public synchronized void put(int value){value_ = value;}
public synchronized int get(){return value_;}
}
 Notez que les déclarations de méthode pour put et get contiennent le mot
clé synchronized.
◦ Par conséquent, le système associe un verrou unique à chaque instance IntegerStorage
(y compris celle partagée par le Producteur et le Consommateur).

48
 Maintenant, nous voulons que le Consommateur attende que le
Producteur mette une valeur dans la methode IS et le Producteur
doit informer le Consommateur lorsque cela s'est produit.
 De même, le Producteur doit attendre que le Consommateur prenne
une valeur (et notifie le Producteur) avant de la remplacer par une
nouvelle valeur

public class IntegerStorage


{
private int value_;

49
private boolean available_ = false;
public synchronized void put(int value){
while(available_)
{
try {wait();}
// wait for Consumer to get value
catch (InterruptedException exc) {exc.printStackTrace();}
}
value_ = value;
available_ = true;
// notify Consumer that value has been set
notifyAll();
}
public synchronized int get()
{
while(!available_){try {wait();}
// wait for Producer to put value
catch (InterruptedException exc) {exc.printStackTrace();}
}
available_ = false;
// notify Producer that value has been retrieved
notifyAll();
return value_;
}
}

50
Les développeurs devraient utiliser le multithreading pour plusieurs
raisons :
1. Débit plus élevé;
2. Des applications réactives qui donnent l'illusion du multitâche;
3. Utilisation efficace des ressources. La création de fils est légère
par rapport à la génération d'un tout nouveau processus. C’est
efficace, par exemple pour les serveurs Web qui utilisent des
fils d’exécution. Au lieu de créer un nouveau processus lors du
traitement des requêtes Web, on peut utiliser les fils d’exécution
qui consomment beaucoup moins de ressources.

Notez que vous ne pouvez pas continuellement ajouter des fils d’exécution et vous
attendre à ce que votre application s'exécute plus rapidement. Plus de fils signifie plus
de problèmes, et vous devez concevoir avec soin et réflexion comment ils
fonctionneront ensemble. Il se peut même que, dans certains cas, vous souhaitiez
éviter complètement le multithreading, en particulier lorsque votre application effectue
de nombreuses opérations séquentielles.
Une compréhension du fonctionnement du threading et une connaissance des
principes de programmation simultanée montreront la maturité et la profondeur
technique d'un développeur. C'est également un facteur de différenciation important
pour décrocher un poste plus élevé dans une entreprise.

51
 Java Multithreading and Concurrency: What to know to crack a senior
engineering interview

 A Tutorial on Modern Multithreading and Concurrency in C++

 Top 5 Concurrency Interview Questions for Software Engineers

 The Educative Team, Multithreading and concurrency fundamentals, Apr 7, 2023

52

Vous aimerez peut-être aussi