L’un des intérêts des threads est qu’ils permettent d’améliorer les performances d’un programme lorsqu’il est exécuté sur une machine multicoeur (autrement dit à peu près n’importe quelle machine aujourd’hui). Il existe plusieurs “patrons” de parallélisation de code qu’il faut connaitre, nous allons nous intéresser pour le moment au plus simple d’entre eux, la parallélisation de boucle, et nous allons le faire sur un problème très simple, le comptage du nombre d’occurrences d’une valeur donnée dans un tableau.
Exercice
Récupérez l’archive du TP
précédent. Exécutez et lisez le code dans le répertoire
exo3_nbOccurs/
.
Implémentez la méthode nbOccursParallel2
qui calcule
le nombre d’occurences en utilisant deux threads qui se partagent la
première et la deuxième moitié du tableau. Exécutez et vérifiez que le
résultat est correct et le speedup raisonnable.
Implémentez la méthode nbOccursParallelN
qui calcule
le nombre d’occurrences en utilisant N
threads. Exécutez et
vérifiez que le résultat est correct et le speedup raisonnable
(attention, la création de thread coûte cher, vous serez loin d’un
speedup de N).
Ré-implémentez la méthode nbOccursParallelN
en
utilisant un ExecutorService
et un ArrayList
de Future.
Map Reduce est un patron de parallélisation de boucle très utilisé en analyse de données pour lequel un certain nombre d’intergiciels (middleware) ont été développés (le plus célèbre étant certainement Hadoop). Le but de cet exercice n’est pas de découvrir l’un de ces intergiciels, mais de comprendre ce qu’est Map Reduce et découvrir quelques applications possibles.
C’est la combinaison de deux fonctions d’ordre supérieur (des
fonctions qui prennent en argument des fonctions, comme vous avez du en
voir en programmation fonctionnelle) - map f coll
renvoie
une nouvelle collection dont les items sont les f(x)
où
x
parcours la collection coll
. Par exemple, si
la collection coll
est un tableau [x1,...,xn]
,
map f coll
renvoie le tableau
[f(x1),...,f(xn)]
. - reduce plus zero coll
est
la “somme au sens de plus
et zero
” des
éléments de la collection coll
. Ce n’est pas nécessairement
une somme de nombres au sens de l’addition sur les entiers (ou les
flottants). On peut aussi “sommer en concaténant” des chaines de
caractères (et dans ce cas zero
est la chaîne de caractère
vide), moyenner des valeurs pondérées (plus
est la moyenne
pondérée), composer des chemins dans un graphe, etc.
Récupérez le code et allez dans
exo1_map_reduce/
. Que font les fonctions
nbItems
et concat
?
Utilisez la fonction mapReduceSequential
pour
redéfinir la fonction nbOccurs
de l’exercice précédent.
Testez votre code.
Optionnel (si vous avez un peu d’avance): à
l’aide de mapReduceSequential
, définissez une fonction
nearestNeighbor(x, distance, coll)
qui renvoie le (ou
plutot, “un”) plus proche voisin de x au sens de distance parmi les
items de coll
.
Donnez une version parallèle mapReduceParallel2
de
mapReduceSequential
utilisant deux threads, puis une
version mapReduceParallelN
utilisant N
threads.
Donnez une version mapReduceStream
de
mapReduceSequential
dont l’argument coll
est
un flot (Stream
), puis utilisez le parallélisme sur les
flots pour rendre votre code parallèle (voir un tutoriel ici)
La simulation numérique est l’un des meilleurs clients du calcul parallèle. Pour en donner un petit aperçu sans s’embarquer dans des schémas numériques trop matheux, on va s’intéresser à quelque chose que vous avez déjà vu (je crois?) avec M. Formenti: les automates cellulaires. Ce sera aussi l’occasion de réviser ce qu’on a vu la semaine dernière.
Exercice
Récupérez le code et allez dans
exo2_autocell/
. Donnez une nouvelle version séquentielle
nthConfigRecycling
de nthConfig
qui évite
d’allouer de manière répétée des tableaux en “recyclant” le tableau
utilisé précédemment. Comparez le temps d’exécution et le
résultat.
Définissez la méthode nthConfigurationParallel
.
Faites d’abord avec ce que vous connaissez, en créant puis supprimant
des threads (ou des tâches) à chaque calcul d’une
configuration.
Pour “recycler” des threads, il suffit de les mettre en attente des autres threads (au moins, des deux voisins) à la fin de chaque itération. À vous de jouer! Vous pourrez utiliser une barrière de synchronisation qui bloque tous les threads, ou les sémaphores de la semaine dernière, qui permettent d’attendre seulement que les deux voisins aient terminé (et permet donc de paralléliser un peu plus).