:: Enseignements :: ESIPE :: E4INFO :: 2019-2020 :: Collections Concurrentes ::
![[LOGO]](http://igm.univ-mlv.fr/ens/resources/mlv.png) |
Examen de Collection Concurrente - Avril 2020
|
Exercice 1 - Fork/Join Summary (9)
Lorsque l'on est dans un environement avec plein de capteurs IoT, il existe une structure de données
appelée SummaryIndex (le nom est pas terrible) qui permet de recevoir les valeurs des différents
capteurs (chaque capteur à un index), les stocker dans un buffer circulaire (un tableau) par capteur et
de péridoquement calculer la moyenne sur les valeurs gardés dans les tableaux.
Note: dans la vrai vie, on utilise maintenant plutôt des HyperLogLog mais c'est plus compliqué.
On se propose d'accélérer le calcul des moyennes de chaque capteur dans la structure de données
en utilisant un algorithme fork/join.
La classe ci-dessous implante cette structure de donnée
-
Le constructeur créer un tableau suivant le nombre de capteurs (entryLength), chaque
Entry contient un tableau des valeurs précédentes, un cursor pour savoir où insérer
la prochaine valeur ainsi qu'un champ average pour stocker la moyenne.
Le tableau des valeurs (data) est initialisé avec NaN qui représente le fait que pour
l"instant il n'y a pas de valeurs calculées. De même, le champ average est initialisé
à NaN.
-
La méthode add ajoute une donnée (value) à l'index d'un capteur
(entryIndex).
-
La méthode sumSummary renvoie la somme des données (en évitant les NaN) et en même
temps mets à jour la moyenne des valeurs de chaque capteur.
summaryStatistics() sur un stream calcul la somme et la moyenne des valeurs de ce stream.
Le but de cet exercice est d'améliorer la méthode
sumSummary, on va pour cela,
écrire une nouvelle version nommée
parallelSumSummary utilisant la technique
fork/join permettant en parallèle de calculer les moyennes et la somme totale.
import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
public class SummaryIndex {
private static class Entry {
private double average;
private int cursor;
private final double[] data;
private Entry(int dataLength) {
this.average = Double.NaN;
double[] data = new double[dataLength];
Arrays.fill(data, Double.NaN);
this.data = data;
}
}
private final Entry[] entries;
public SummaryIndex(int entryLength, int dataLength) {
var entries = new Entry[entryLength];
for(var i = 0; i < entries.length; i++) {
entries[i] = new Entry(dataLength);
}
this.entries = entries;
}
public void add(int entryIndex, double value) {
var entry = entries[entryIndex];
var cursor = entry.cursor;
entry.data[cursor] = value;
entry.cursor = (cursor + 1) % entry.data.length;
}
public double average(int entryIndex) { // pas utilisée dans l'exercice
return entries[entryIndex].average;
}
public double sumSummary() {
var sum = 0.0;
for(var i = 0; i < entries.length; i++) {
var entry = entries[i];
var stats = Arrays.stream(entry.data).filter(v -> !Double.isNaN(v)).summaryStatistics();
var average = stats.getAverage();;
entry.average = average;
if (!Double.isNaN(average)) {
sum += stats.getSum();
}
}
return sum;
}
public static void main(String[] args) {
var summaryIndex = new SummaryIndex(20_000, 200);
var random = new Random(0);
for(var i = 0; i < 10_000_000; i++) {
summaryIndex.add(i % 20_000, random.nextInt(100));
}
System.out.println(summaryIndex.sumSummary());
//System.out.println(summaryIndex.parallelSumSummary());
}
}
-
Dans un premier temps, écrire une méthode double sequentialSumSummary(int from, int to)
permettant de mettre à jour les moyennes des entries entre [from, to[ et de calculer
la somme des valeurs.
Pour tester, vérifier que sequentialSumSummary(0, entryLength) à la même résultat que
sumSummary().
-
Ecrire la classe Task qui hérite de la classe RecursiveTask et implante
algorithme fork/join.
L'algorithme est le suivant: si la distance entre from et to est inférieur
ou égale à 100, on utilisera la version séquentielle, sinon on coupera (virtuellement) le
tableau au millieu entre from et to, on crée deux sous tâche pour chaque
partie de tableau et on applique la logique fork/join
-
Enfin ajouter la méthode parallelSumSummary() qui récupère le pool
fork/join par défaut puis envoie une tâche demandant le calcul sur toutes les entries.
Vérifier que les méthodes parallelSumSummary() et sumSummary() renvoie
bien le même résultat.
-
En fait, la structure ne vient pas avec une méthode sumSummary mais une méthode
averageSummary qui calcule les moyennes de chaque capteur (comme avant) et renvoie
la moyenne globale de toutes les valeurs de tous les capteurs (donc pas la somme).
Voici le code de la méthode averageSummary
public double averageSummary() {
var sum = 0.0;
var count = 0L;
for(var i = 0; i < entries.length; i++) {
var entry = entries[i];
var stats = Arrays.stream(entry.data).filter(v -> !Double.isNaN(v)).summaryStatistics();
var average = stats.getAverage();
entry.average = average;
if (!Double.isNaN(average)) {
sum += stats.getSum();
count += stats.getCount();
}
}
return sum / count;
}
En utilisant le même principe écrire la méthode parallelAverageSummary.
Note: cette question n'a pas beaucoup de points (2), donc ne passer pas une éternité dessus
si vous ne voyez pas comment faire !
Exercice 2 - COWList (8)
Une liste Copy-On-Write (COW ne veut pas dire vache ici !) est une liste d'élements où à chaque fois que l'on ajoute (ou l'on retire)
un élement, on duplique le tableau contenant l'ensemble des éléments.
Lorsque l'on crée la liste, on commence avec un tableau d'objet de taille 0 et à chaque fois que l'on ajoute un élements,
on duplique le tableau précédent avec une taille supérieur de 1, et on place l'élement à la fin du nouveau tableau.
Note: il existe une méthode Arrays.copyOf(array, newLength).
-
Ecrire une classe COWList générique avec une méthode add qui permet d'ajouter un élement dans la liste
et une méthode size (le nombre d'élements dans la liste)
-
Ecrire un main de test qui créé 4 threads ajoutant chacune 2_500 entiers dans une même liste
et qui affiche la taille de la liste une fois que toute les threads ont terminé.
Rappel: il faut faire un join avant d'afficher la taille !
-
Expliquer pourquoi le code que vous avez écrit est pas thread-safe (en commentaire dans le code).
-
Créer une version thread-safe de la classe COWList nommée SynchronizedCOWList qui utilise des blocs synchronized
pour rendre le code thread-safe.
Attention: lorsque vous tester le main de SynchronizedCOWList à bien vérifier que vous créez une SynchronizedCOWList
et pas une COWList !
Attention 2: SynchronizedCOWList doit être thread-safe quelque soit le main que vous pouvez écrire,
cela ne doit pas fonctionner juste avec votre main.
-
Créer une nouvelle version de la classe COWList nommée LockFreeCOWList qui utilise l'API des VarHandle
pour rendre la classe thread safe.
Vérifier que votre main fonctionne correctement !
Exercice 3 - Pour les plus balèze: Generateur pseudo-aléatoire lock-free (3)
On souhaite modifier la classe RandomNumberGenerator ci-dessous pour la rendre thread-safe sans utiliser ni section critique ni verrou (lock-free donc).
public class RandomNumberGenerator {
private long x;
public RandomNumberGenerator(long seed) {
if (seed == 0) {
throw new IllegalArgumentException("seed == 0");
}
x = seed;
}
public long next() { // Marsaglia's XorShift
x ^= x >>> 12;
x ^= x << 25;
x ^= x >>> 27;
return x * 2685821657736338717L;
}
public static void main(String[] args) {
var rng = new RandomNumberGenerator(1);
for(var i = 0; i < 5_000; i++) {
System.out.println(rng.next());
}
}
}
-
Expliquer comment l'implantation ci-dessus n'est pas thread-safe !
-
Créer une classe LockFreeOneRandomNumberGenerator qui utilise l'API des VarHandles et la méthode compareAndSet
pour créer une version thread-safe de l'API.
Attention à bien modifier le code pour limiter les écritures concurrentes !
Attention2: Vérifier bien que l'affichage du main de RandomNumberGenerator et LockFreeOneRandomNumberGenerator
sont identiques lorsqu'il n'y a qu'une thread !
-
Créer une classe LockFreeTwoRandomNumberGenerator qui utilise la méthode getAndSet au lieu de la méthode compareAndSet
de l'API des VarHandles ce qui devrait simplifier votre code.
-
Comparer les implantations de LockFreeOneRandomNumberGenerator et LockFreeTwoRandomNumberGenerator et indiquer selon vous quelle
est la meilleure implantation.
© Université de Marne-la-Vallée