6
Exécution des tâches
La plupart des applications concurrentes sont organisées autour de l’exécution de tâches,
que l’on peut considérer comme des unités de travail abstraites. Diviser une application
en plusieurs tâches simplifie l’organisation du programme et facilite la découverte des
erreurs grâce aux frontières naturelles séparant les différentes transactions. Cette division
encourage également la concurrence en fournissant une structure naturelle permettant
de paralléliser le travail.
6.1 Exécution des tâches dans les threads
La première étape pour organiser un programme autour de l’exécution de tâches consiste
à identifier les frontières entre ces tâches. Dans l’idéal, les tâches sont des activités
indépendantes, c’est-à-dire des opérations qui ne dépendent ni de l’état, ni du résultat,
ni des effets de bord des autres tâches. Cette indépendance facilite la concurrence puisque
les tâches indépendantes peuvent s’exécuter en parallèle si l’on dispose des ressources
de traitement adéquates. Pour disposer de plus de souplesse dans l’ordonnancement et
la répartition de la charge entre ces tâches, chacune devrait également représenter une
petite fraction des possibilités du traitement de l’application.
Les applications serveur doivent fournir un bon débit de données et une réactivité correcte
sous une charge normale. Les fournisseurs d’applications veulent des programmes permet-
tant de supporter autant d’utilisateurs que possible afin de réduire d’autant les coûts par
utilisateur ; ces utilisateurs veulent évidemment obtenir rapidement les réponses qu’ils
demandent. En outre, les applications doivent non pas se dégrader brutalement lorsqu’elles
sont surchargées mais réagir le mieux possible. Tous ces objectifs peuvent être atteints en
choisissant de bonnes frontières entre les tâches et en utilisant une politique raisonnable
d’exécution des tâches (voir la section 6.2.2).
La plupart des applications serveur offrent un choix naturel pour les frontières entre
tâches : les différentes requêtes des clients. Les serveurs web, de courrier, de fichiers,
=Java FM.book Page 117 Mardi, 7. avril 2009 7:11 07
118 Structuration des applications concurrentes Partie II
les conteneurs EJB et les serveurs de bases de données reçoivent tous des requêtes de
clients distants via des connexions réseau. Utiliser ces différentes requêtes comme des
frontières de tâches permet généralement d’obtenir à la fois des tâches indépendantes et
de taille appropriée. Le résultat de la soumission d’un message à un serveur de courrier,
par exemple, n’est pas affecté par les autres messages qui sont traités en même temps,
et la prise en charge d’un simple message ne nécessite généralement qu’un très petit
pourcentage de la capacité totale du serveur.
6.1.1 Exécution séquentielle des tâches
Il existe un certain nombre de politiques possibles pour ordonnancer les tâches au sein
d’une application ; parmi elles, certaines exploitent mieux la concurrence que d’autres.
La plus simple consiste à exécuter les tâches séquentiellement dans un seul thread. La
classe SingleThreadWebServer du Listing 6.1 traite ses tâches – des requêtes HTTP
arrivant sur le port 80 – en séquence. Les détails du traitement de la requête ne sont pas
importants ; nous ne nous intéressons ici qu’à la concurrence des différentes politiques
d’ordonnancement.
Listing 6.1 : Serveur web séquentiel.
class SingleThreadWebServer {
public static void main(String[] args) throws IOException {
ServerSocket socket = new ServerSocket(80);
while (true) {
Socket connection = socket.accept();
handleRequest(connection);
}
}
}
SingleThreadedWebServer est simple et correcte d’un point de vue théorique, mais
serait très inefficace en production car elle ne peut gérer qu’une seule requête à la fois.
Le thread principal alterne constamment entre accepter des connexions et traiter la
requête associée : pendant que le serveur traite une requête, les nouvelles connexions
doivent attendre qu’il ait fini ce traitement et qu’il appelle à nouveau accept(). Cela
peut fonctionner si le traitement des requêtes est suffisamment rapide pour que handle
Request() se termine immédiatement, mais cette hypothèse ne reflète pas du tout la
situation des serveurs web actuels.
Traiter une requête web implique un mélange de calculs et d’opérations d’E/S. Le
serveur doit lire dans un socket pour obtenir la requête et y écrire pour envoyer la
réponse ; ces opérations peuvent être bloquantes en cas de congestion du réseau ou de
problèmes de connexion. Il peut également effectuer des E/S sur des fichiers ou lancer
des requêtes de bases de données, qui peuvent elles aussi être bloquantes. Avec un
serveur monothread, un blocage ne fait pas que retarder le traitement de la requête en
cours : il empêche également celui des requêtes en attente. Si une requête se bloque
pendant un temps très long, les utilisateurs penseront que le serveur n’est plus disponible
=Java FM.book Page 118 Mardi, 7. avril 2009 7:11 07
Chapitre 6 Exécution des tâches 119
puisqu’il ne semble plus répondre. En outre, les ressources sont mal utilisées puisque le
processeur reste inactif pendant que l’unique thread attend que ses E/S se terminent.
Pour les applications serveur, le traitement séquentiel fournit rarement un bon débit ou
une réactivité correcte. Il existe des exceptions – lorsqu’il y a très peu de tâches et qu’elles
durent longtemps, ou quand le serveur ne sert qu’un seul client qui n’envoie qu’une
seule requête à la fois – mais la plupart des applications serveur ne fonctionnent pas de
cette façon1.
6.1.2 Création explicite de threads pour les tâches
Une approche plus réactive consiste à créer un nouveau thread pour répondre à chaque
nouvelle requête, comme dans le Listing 6.2.
Listing 6.2 : Serveur web lançant un thread par requête.
class ThreadPerTaskWebServer {
public static void main(String[] args) throws IOException {
ServerSocket socket = new ServerSocket(80);
while (true) {
final Socket connection = socket.accept();
Runnable task = new Runnable() {
public void run() {
handleRequest(connection);
}
};
new Thread(task).start();
}
}
}
La structure de la classe ThreadPerTaskWebServer ressemble à celle de la version
monothread – le thread principal continue d’alterner entre la réception d’une connexion
entrante et le traitement de la requête. Mais, ici, la boucle principale crée un nouveau
thread pour chaque connexion afin de traiter la requête au lieu de le faire dans le thread
principal. Ceci a trois conséquences importantes :
mLe thread principal se décharge du traitement de la tâche, ce qui permet à la boucle
principale de poursuivre et de venir attendre plus rapidement la connexion entrante
suivante. Ceci autorise de nouvelles connexions alors que les requêtes sont en cours
de traitement, ce qui améliore les temps de réponse.
mLes tâches peuvent être traitées en parallèle, ce qui permet de traiter simultanément
plusieurs requêtes. Ceci améliore le débit lorsqu’il y a plusieurs processeurs ou si
des tâches doivent se bloquer en attente d’une opération d’E/S, de l’acquisition d’un
verrou ou de la disponibilité d’une ressource, par exemple.
1. Dans certaines situations, le traitement séquentiel offre des avantages en termes de simplicité et de
sécurité ; la plupart des interfaces graphiques traitent séquentiellement les tâches dans un seul thread.
Nous reviendrons sur le modèle séquentiel au Chapitre 9.
=Java FM.book Page 119 Mardi, 7. avril 2009 7:11 07
120 Structuration des applications concurrentes Partie II
mLe code du traitement de la tâche doit être thread-safe car il peut être invoqué de
façon concurrente par plusieurs tâches.
En cas de charge modérée, cette approche est plus efficace qu’une exécution séquentielle.
Tant que la fréquence d’arrivée des requêtes ne dépasse pas les capacités de traitement
du serveur, elle offre une meilleure réactivité et un débit supérieur.
6.1.3 Inconvénients d’une création illimitée de threads
Cependant, dans un environnement de production, l’approche "un thread par tâche" a quel-
ques inconvénients pratiques, notamment lorsqu’elle peut produire un grand nombre de
threads :
mSurcoût dû au cycle de vie des threads. La création d’un thread et sa suppression
ne sont pas gratuites. Bien que le surcoût dépende des plates-formes, la création d’un
thread prend du temps, induit une certaine latence dans le traitement de la requête et
nécessite un traitement de la part de la JVM et du système d’exploitation. Si les
requêtes sont fréquentes et légères, comme dans la plupart des applications serveur,
la création d’un thread par requête peut consommer un nombre non négligeable de
ressources.
mConsommation des ressources. Les threads actifs consomment des ressources du
système, notamment la mémoire. S’il y a plus de threads en cours d’exécution qu’il
n’y a de processeurs disponibles, certains threads resteront inactifs, ce qui peut
consommer beaucoup de mémoire et surcharger le ramasse-miettes. En outre, le fait
que de nombreux threads concourent pour l’accès aux processeurs peut également
avoir des répercussions sur les performances. Si vous avez suffisamment de threads
pour garder tous les processeurs occupés, en créer plus n’améliorera rien, voire
dégradera les performances.
mStabilité. Il y a une limite sur le nombre de threads qui peuvent être créés. Cette
limite varie en fonction des plates-formes, des paramètres d’appels de la JVM, de la
taille de pile demandée dans le constructeur de Thread et des limites imposées aux
threads par le système d’exploitation1. Lorsque vous atteignerez cette limite, vous
obtiendrez très probablement une exception OutOfMemoryError. Tenter de se rétablir
de cette erreur est très risqué ; il est bien plus simple de structurer votre programme
afin d’éviter d’atteindre cette limite.
1. Sur des machines 32 bits, un facteur limitant important est l’espace d’adressage pour les piles des
threads. Chaque thread gère deux piles d’exécution : une pour le code Java, l’autre pour le code natif.
Généralement, la JVM produit par défaut une taille de pile combinée d’environ 512 kilo-octets (vous
pouvez changer cette valeur avec le paramètre -Xss de la JVM ou lors de l’appel du constructeur de
Thread). Si vous divisez les 232 adresses par la taille de la pile de chaque thread, vous obtenez une
limite de quelques milliers ou dizaines de milliers de threads. D’autres facteurs, comme les limites du
système d’exploitation, peuvent imposer des limites plus contraignantes.
=Java FM.book Page 120 Mardi, 7. avril 2009 7:11 07
Chapitre 6 Exécution des tâches 121
Jusqu’à un certain point, ajouter plus de threads permet d’améliorer le débit mais, au-
delà de ce point, créer des threads supplémentaires ne fera que ralentir, et en créer un de
trop peut totalement empêcher l’application de fonctionner. Le meilleur moyen de se
protéger de ce danger consiste à fixer une limite au nombre de threads qu’un programme
peut créer et à tester sérieusement l’application pour vérifier qu’elle ne tombera pas à
court de ressources, même lorsque cette limite sera atteinte.
Le problème de l’approche "un thread par tâche" est que la seule limite imposée au nombre
de threads créés est la fréquence à laquelle les clients distants peuvent lancer des requêtes
HTTP. Comme tous les autres problèmes liés à la concurrence, la création infinie de
threads peut sembler fonctionner parfaitement au cours des phases de prototypage et
de développement, ce qui n’empêchera pas le problème d’apparaître lorsque l’application
sera déployée en production et soumise à une forte charge.
Un utilisateur pervers, voire des utilisateurs ordinaires, peut donc faire planter votre
serveur web en le soumettant à une charge trop forte. Pour une application serveur
supposée fournir une haute disponibilité et se dégrader correctement en cas de charge
importante, il s’agit d’un sérieux défaut.
6.2 Le framework Executor
Les tâches sont des unités logiques de travail et les threads sont un mécanisme grâce
auquel ces tâches peuvent s’exécuter de façon asynchrone. Nous avons étudié deux
politiques d’exécution des tâches à l’aide des threads – l’exécution séquentielle des tâches
dans un seul thread et l’exécution de chaque tâche dans son propre thread. Toutes les
deux ont de sévères limitations : l’approche séquentielle implique une mauvaise réactivité
et un faible débit et l’approche "une tâche par thread" souffre d’une mauvaise gestion des
ressources.
Au Chapitre 5, nous avons vu comment utiliser des files de taille fixe pour empêcher
qu’une application surchargée ne soit à court de mémoire. Un pool de threads offrant
les mêmes avantages pour la gestion des threads, java.util.concurrent en fournit une
implémentation dans le cadre du framework Executor. Comme le montre le Listing 6.3,
la principale abstraction de l’exécution des tâches dans la bibliothèque des classes Java
est non pas Thread mais Executor.
Listing 6.3 : Interface Executor.
public interface Executor {
void execute(Runnable command);
}
Executor est peut-être une interface simple, mais elle forme les fondements d’un
framework souple et puissant pour l’exécution asynchrone des tâches sous un grand
nombre de politiques d’exécution des tâches. Elle fournit un moyen standard pour
=Java FM.book Page 121 Mardi, 7. avril 2009 7:11 07
1 / 12 100%
La catégorie de ce document est-elle correcte?
Merci pour votre participation!

Faire une suggestion

Avez-vous trouvé des erreurs dans linterface ou les textes ? Ou savez-vous comment améliorer linterface utilisateur de StudyLib ? Nhésitez pas à envoyer vos suggestions. Cest très important pour nous !