• Aucun résultat trouvé

Fork / Join

N/A
N/A
Protected

Academic year: 2022

Partager "Fork / Join"

Copied!
22
0
0

Texte intégral

(1)

Fork / Join

Rémi Forax

(2)

Vocabulaire

Concurrence

Exécuter plusieurs calculs en même temps

Parallèle

Un calcul est décomposé et exécuté sur plusieurs threads

Vectorisé

Une opération simple (+, min, etc) est exécutée par un même thread sur plusieurs données adajcences en mémoire

(sur 128 bits, 256 bits, etc)

Distribué

Un calcul est décomposé et exécuté sur plusieurs machines

(3)

Start / Join

Historiquement, le modèle utilisé est start / join

Exemple, faire *2 aux valeurs d’un tableau avec 5 threads

var threads = IntStream.range(0, 5) .mapToObj(i -> new Thread(() -> { var startIndex = ...

var endIndex = ...

for(var i = startIndex; i < endIndex; i++) { array[i] *= 2;

} }))

.toList();

for(var thread: threads) { thread.start(); } for(var thread: threads) { thread.join(); }

Recopié du modèle fork() / waitpid() sous Unix

(4)

Le modèle start / join

Avec un tableau de 48 cases

[0-9[ [9-18[ [18-28[ [28-38[ [38-48[

start start

start start

start

join join join join join

0 48

(5)

Problèmes du modèle start / join

Le modèle start / join marche bien

si toutes les données sont connus en amont (upfront)

Marche bien pour un tableau, moins bien pour un arbre rouge/noir car il faut tout parcourir pour voir toutes les données

si le calcul est homogène en temps

Si le calcul est long seulement pour certain index, une thread peut être encore entrain de faire des calculs alors que les autres ont finis

=> problème de répartition de la charge de calcul

(6)

Divide and Conquer

Solution naive “divide and conquer”,

évite d’avoir à connaitre tout les données en amont

process(int startIndex, int endIndex) if endIndex – startIndex < SMALL // for(...)

return

var middle = (startIndex + endIndex) >>> 1;

start process(startIndex, middle);

start process(middle, endIndex);

join join }

(7)

Fork / Join

Evolution de la solution naive “divide and conquer”,

on évite d’avoir des threads en attentes

process(int startIndex, int endIndex if endIndex – startIndex < SMALL // for(...)

return ...

var middle = (startIndex + endIndex) >>> 1;

fork process(startIndex, middle);

var result2 = process(middle, endIndex);

var result1 = join

return combine result1 result2 }

(8)

Le modèle fork / join

Avec un tableau de 48 cases

[0-24[ [24-48[

0 48

[0-12[ [12-24[ [24-36[ [36-48[

[0-6[ [6-12[

join

join

join

join

(9)

Problème du modèle fork / join

On a toujours le problème de la répartition de la charge de calcul si le calcul est pas homogène en temps

Solution: découpler la partie de calcul effectuer de la thread qui effectue le calcul

=> pool de thread + work stealing

(10)

Thread Pool avec Work Stealing

Le nombre de threads est fixe

Chaque thread à sa queue de tâche

Si un thread a fini toutes les tâches de sa queue (si la queue est vide), il va voler (steal) des tâches à la fin dans les queues des autres threads

=> Le fait d’aller voler des tâches permet

d’équilibrer la charge de travail

(11)

fork / join + work stealing

Avec un tableau de 48 cases

[0-24[ [24-48[

0 48

[0-12[ [12-24[ [24-36[ [36-48[

[0-6[ [6-12[ [12-18[ [18-24[ [24-30[ [30-36[ [36-42[ [42-48[

steal

submit

submit submit submit submit submit submit submit

(12)

Régler fork / join

Sélection de SMALL

Si SMALL trop gros, pas assez de tâches, pas assez de parallèlisme

Si SMALL trop petit, trop de tâches, trop d’allocations + queue trop grande

process(int startIndex, int endIndex if endIndex – startIndex < SMALL // for(...)

return ...

...

}

(13)

Régler fork / join (2)

Combine doit être une opération associative

On ne peut pas décomposer le calcul

si combine(combine(a, b), c) != combine (a, combine(b, c))

process(int startIndex, int endIndex ...

var middle = (startIndex + endIndex) >>> 1;

fork process(startIndex, middle);

var result2 = process(middle, endIndex);

var result1 = join

return combine result1 result2 }

La moyenne (a, b) → (a + b) / 2 est pas associative !

(14)

Fork / Join en Java

(15)

java.util.concurrent.ForkJoinPool

Le pool de threads (l’ExecutorService) qui fait du work stealing s’appel ForkJoinPool

Pour créer un ForkJoinPool

new ForkJoinPool(5) // avec 5 threads

Il existe déjà un ForkJoinPool par défaut,

ForkJoinPool.commonPool()

(16)

Tâches du ForkJoinPool

Le ForkJoinPool exécute des ForkJoinTask

RecursiveAction pour les tâches sans valeur

RecursiveTask<V> pour les tâches avec une valeur

Exécuter la tâche initiale sur le ForkJoinPool

execute(RecursiveAction)

L’exécution est asynchrone

invoke(RecursiveAction|RecursiveTask)

Bloque la thread courante et attend l’exécution de la tâche

submit(RecursiveAction|RecursiveTask) → Future

Renvoie un Future (Future.get() permet d’avoir la valeur)

(17)

Rappel sur la suite de Fibonacci

Calcul de la suite de Fibonacci en récursif (stupide mais simple)

int fibonacci(int n) if (n <= 1) {

return n;

}

return fibonacci(n – 1) + fibonacci(n – 2);

}

Idée: on peut utiliser le pattern Fork / Join pour

faire le calcul en parallèle

(18)

Exemple de RecursiveTask

Calcul de Fibonacci en récursif (stupide mais simple)

private static class Fibonacci extends RecursiveTask<Integer> { private final int n;

private Fibonacci(int n) { this.n = n; } @Override

protected Integer compute() { if (n <= 1) {

return 1;

}

var fib1 = new Fibonacci(n - 1);

var fib2 = new Fibonacci(n - 2);

f1.fork(); // soumet au ForkJoinPool

var result2 = fib2.compute(); // calcul dans la thread courante var result1 = f1.join(); // attend le résultat de la tâche return result1 + result2; // le + est bien associatif

}}

(19)

Exemple de RecursiveTask (2)

Calcul de Fibonacci en récursif (stupide mais simple)

private static class Fibonacci extends RecursiveTask<Integer> { private final int n;

private Fibonacci(int n) { this.n = n; } @Override

protected Integer compute() {

} }

var pool = ForkJoinPool.commonPool();

var fibo = new Fibonacci(15);

var result = pool.invoke(fibo); // on attend la fin du calcul

(20)

Exemple de RecursiveAction

Multiplier par 2 les valeurs du tableaux

private static class MultiplyBy2 extends RecursiveAction { private final int[] array;

private final int start;

private final int end;

private MultiplyBy2(...) { this.array = array; this.start = start; this.end = end; } @Override

protected void compute() { if (end – start < 1024) {

for(var i = start; I < end; i++) { array[i] *= 2; } return;

}

var middle = (start + end) >>> 1;

var mul1 = new MultiplyBy2(array, start, middle);

var mul2 = new MultiplyBy2(array, middle, end);

mul2.fork();

mul1.compute();

mul2.join();

} }

(21)

Exemple de RecursiveAction (2)

Multiplier par 2 les valeurs du tableaux

private static class MultiplyBy2 extends RecursiveAction { private final int[] array;

private final int start;

private final int end;

private MultiplyBy2(...) { this.array = array; this.start = start; this.end = end;

}

@Override

protected void compute() { ...

} }

var pool = ForkJoinPool.commonPool();

var fibo = new MultiplyBy2(array, 0, array.length);

var future = pool.submit(fibo); // on récupère un future

(22)

Relation avec les Streams

Lorsque l’on utilise un Stream parallèle

stream.parallel(). …

le calcul est fait dans le ForkJoinPool par défaut

ForkJoinPool.commonPool()

L’implantation utilise la méthode

Spliterator.trySplit() du Spliterator du Stream

pour faire la décomposition récursive

Références

Documents relatifs

Tout d'abord, bien sûr, par votre abonnement, mais aussi par votre participation, vos critiques, vos conseils qui en feront ce que nous souhaitons qu'elle soit

En effet les notions qui sont alors utilisées (celles vues précédemment) il ne les connaît pas et là, il perd du temps à essayer de comprendre, à se souvenir de ce que il a fait

(b) Une famille est g´ en´ eratrice si et seulement si le rang de la matrice est ´ egal au nombre de lignes (ici 3), donc d’apr` es le calcul pr´ ec´ edent la famille n’est

On dit souvent que les joueurs de tennis ont un bras plus court que l’autre.. 1 Note les deux mots qui se prononcent de la

Un entier n est appelé réversible s'il est un multiple k de l'entier m obtenu en lisant n de droite à gauche.Comme on écarte toute écriture non standard des entiers m et n

On constate donc que pour un angle 2x donné quelconque, pour un point S quelconque sur la bissectrice, et pour toute longueur de segment d, également quelconque mais suffisante

[r]

N’oublions pas la recherche de Stainless and Nickel Alloys (filiale d’Arcelor-Mittal, ex Imphy Alloys) qui reste le leader mondial sur l’Invar ® , mais aussi