Devoir maison : Étape 4
- Paralléliser les réquêtes RPC grâce au multithreading (étape optionnelle non évaluée)
Cette étape est réalisable à la suite de la séance 5. Cette étape est optionnelle. Elle ne comptera pas pour l'évaluation du module. Mais si vous voulez faire cette étape et que vous avez besoin d'aide, n'hésitez pas à la demander.
Prérequis pour la réalisation de cette étape :
- avoir terminé l'étape 3 du devoir ;
- avoir compris le tutoriel sur les threads jusqu'à la page « Intrinsic Locks and Synchronization » (≈ 1 h)
- avoir compris le cours sur les threads
Les calculs d'une images de Mandelbrot sont facilement parallélisables. Le calcul fait en chaque point est totalement indépendant des autres points. L'application graphique de l'étape 3 du devoir a déjà divisé le calcul de l'image en plusieurs sections. Mais actuellement ces calculs sont fait les uns après les autres. Dans cette nouvelle étape, nous modifions le code pour que les calculs soient faits en parallèle. Pour cela il faut modifier le serveur RPC et l'application graphique (qui est ici notre client RPC).
Parallélisation du serveur RPC
Le code actuel du serveur RPC suit sur les deux phases suivantes à chaque nouvelle réquête:
- Accepter la connexion du nouveau client ;
- Recevoir sa requêtes, la traiter, y répondre, et fermer la connexion avec ce client.
Donc actuellement, tant que la seconde phase n'est pas terminée, il n'est pas possible d'accepter de nouveaux clients et de traiter leurs requêtes.
Placer le traitement des requêtes RPC dans un nouveau thread
Créer un nouvelle classe RpcServerRunnable qui implémente Runnable pour y déporter tout le code de la seconde phase.
Terminer les modifications pour que le serveur RPC lance cette seconde phase dans un nouveau thread, et retourne immédiatement sur l'acceptation des nouvelles connexions.
Parallélisation de l'application graphique
Le code actuel de l'application graphique lance les requêtes RPC pour le calcul des sections d'images les une après les autres. Nous le modifions pour que chaque requête soit réalisée dans un thread qui utilise une classe Callable.
Placer l'envoi des requêtes RPC dans un nouveau thread
Pour Paralléliser l'envoi des requêtes de calcul pour chaque section:
- Modifiez votre code pour ces requêtes soit réalisées par une classe qui implémente Callable<RpcClient>. Notez que vous n'étes pas obligé d'écrire une classe d'implémentation complète. La méthode RpcClient call() se résume à une seule instruction, donc une classe anonyme ou une expression lambda suffit pour cette modification ;
- Utilisez une liste (ArrayList<FutureTask<RpcClient>>) de tâches pour préparer l'attente du résultat de chaque thread ;
- Créez et démarrer un thread pour chacune de ces tâches ;
- Attendez et récupérez le résultat de tous les threads pour afficher chaque section de l'image de Mandelbrot.
Serveur avec pool de threads
La version actuelle du serveur RPC a un gros défaut: elle crée un nouveau thread dès qu'un client le lui demande. Une surcharge de demandes risque de lancer un nombre de threads trop important pour la configuration de la machine. La solution pour éviter ce problème consiste à démarrer à l'avance un ensemble déterminé de threads. Lorsqu'une nouvelle requête arrive, il y a alors deux possibilités: soit un thread est libre pour traiter la requête, et il la prend en charge, soit il n'y en a pas de disponible, et on met la requête en file d'attente le temps qu'un thread se libère.
Pour gérer de façon raisonnée les threads, java propose un ensemble de services par l’intermédiaire de l'interface ExecutorServices.
Exemples de services offerts:
- Future<?> submit(Runnable task): ajoute une tâches dans la file d'attente du pool ;
- void shutdown(): stoppe le service, en laissant le temps à toutes tâches en traitement ou en attente de se réaliser ;
- boolean isTerminated(): indique si toutes les tâches sont terminées ;
- ...
Pour obtenir une classe qui implémente ces services, nous choisissons d'utiliser la classe Executors. Elle ne contient que des méthodes de classe, qui, pour la plupart, permettent créer des objets implémentant les méthodes d'ExecutorServices (plus précisément, ils implémentent des interfaces héritant de d'ExecutorServices).
Pour créer un pool de nThreads threads, nous utilisons la méthode static ExecutorService newFixedThreadPool(int nThreads).
Traitement des requêtes avec un pool de threads
Pour adapter le code du serveur RPC à cette modification, il faut:
- ajouter avant la boucle qui traite les accept() une ligne pour créer le service: ExecutorService service = Executors.newFixedThreadPool(NBTHREADS); (à vous de définir selon votre machine la valeur de NBTHREADS) ;
- remplacer les instructions qui créent et lancent votre thread par la ligne service.submit(new RpcServerRunnable( ...)); (les ... étant les paramètres utiles au constructeur de RpcServerRunnable).
Votre projet est fini, mais des améliorations de ce programme sont envisageable. Votre code actuel parallélise le calcul d'une image de Mandelbrot en envoyant toutes les requêtes sur le même serveur RPC. Il serait pertinent de fournir à l'application graphique une liste de serveurs pour pouvoir envoyer plus de requêtes en parallèle et vraiment accélérer le calcul de ces images.
$Date: 2019-04-18 14:48:37 +0200 (jeu. 18 avril 2019) $