iBaffiPro ha scritto:
Sono curioso di vedere la tua soluzione.
Ok, ho ripreso il codice che avevo fatto. Se si vuole modificare una lista di oggetti in modo "parallelizzabile" (modificando quella lista, NON creando una nuova lista), si può fare usando il fork-join pool. Ma va fatto oculatamente, scrivendo la implementazione una volta sola e permettendo invece di "parametrizzare" il suo comportamento in modo opportuno.
Innanzitutto si fa una functional interface.
@FunctionalInterface
public interface ElementMutator<T> {
T apply(int index, T value);
}
Perché ho fatto una
nuova interfaccia? Semplicemente perché tra le 43 functional interface in java.util.function non ce n'è purtroppo una con quella forma (
(int,T) --> T ). C'è il mio
cheat-sheet su queste 43 interfacce.
Nota che questa interfaccia NON ha alcuna nozione di lista o collezione. Riceve solamente
un oggetto (e l'indice a cui si trova l'oggetto nella lista, che "potrebbe" tornare utile in certi casi) e può/deve restituire:
- un nuovo oggetto, se gli oggetti sono immutabili
- lo stesso oggetto modificato come stato, se gli oggetti sono mutabili
Se l'indice non servisse, si potrebbe definire il tutto usando un più semplice UnaryOperator<T> la cui forma è T --> T.
Poi la implementazione della modifica sulla lista con il fork-join pool.
import java.util.List;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;
public class ForkJoinListMutator {
public static final int DEFAULT_SEQ_THRESHOLD = 10000;
private static final ForkJoinListMutator defaultInstance =
new ForkJoinListMutator(ForkJoinPool.commonPool());
private final ForkJoinPool forkJoinPool;
public ForkJoinListMutator(ForkJoinPool forkJoinPool) {
this.forkJoinPool = forkJoinPool;
}
public static ForkJoinListMutator getDefault() {
return defaultInstance;
}
public <T> void mutate(List<T> list, ElementMutator<T> mutator) {
mutate(list, DEFAULT_SEQ_THRESHOLD, mutator);
}
public <T> void mutate(List<T> list, int seqThreshold, ElementMutator<T> mutator) {
MutateTask<T> mainTask = new MutateTask<>(list, seqThreshold, mutator);
forkJoinPool.invoke(mainTask);
}
private static class MutateTask<T> extends RecursiveAction {
private static final long serialVersionUID = 1L;
private final List<T> list;
private final int start;
private final int end;
private final int seqThreshold;
private final ElementMutator<T> mutator;
public MutateTask(List<T> list, int seqThreshold, ElementMutator<T> mutator) {
this(list, 0, list.size(), seqThreshold, mutator);
}
public MutateTask(List<T> list, int start, int end, int seqThreshold, ElementMutator<T> mutator) {
this.list = list;
this.start = start;
this.end = end;
this.seqThreshold = seqThreshold;
this.mutator = mutator;
}
@Override
protected void compute() {
final int length = end - start;
if (length <= seqThreshold) {
computeSequentially();
} else {
MutateTask<T> leftTask = new MutateTask<>(list, start, start+length/2, seqThreshold, mutator);
leftTask.fork();
MutateTask<T> rightTask = new MutateTask<>(list, start+length/2, end, seqThreshold, mutator);
rightTask.compute();
leftTask.join();
}
}
private void computeSequentially() {
for (int i = start; i < end; i++) {
list.set(i, mutator.apply(i, list.get(i)));
}
}
}
}
Un ForkJoinListMutator incapsula semplicemente un ForkJoinPool ed è immutabile. Lo stesso oggetto ForkJoinListMutator lo puoi RI-usare più volte per modificare tante liste anche di tipi differenti. Perché sono i due metodi
mutate che sfruttano i generics (e non il ForkJoinListMutator in sé).
La API è molto semplice: puoi ottenere un ForkJoinListMutator di "default" (che usa il "common" pool di default) oppure creare un nuovo ForkJoinListMutator con un ForkJoinPool esplicito (magari configurato diversamente dal common).
Poi basta usare uno dei due mutate(), con/senza seqThreshold esplicito.
Quindi es.:
ForkJoinListMutator listMutator = ForkJoinListMutator.getDefault();
Se hai una lista di stringhe e vuoi fare il
reverse di tutte le stringhe:
List<String> lista = //........
listMutator.mutate(lista, (i, str) -> new StringBuilder(str).reverse().toString());
Se avessi una lista di LocalDate e vuoi aggiungere 1 giorno a tutte le date:
List<LocalDate> lista = //........
listMutator.mutate(lista, (i, data) -> data.plusDays(1));
Se (e ripeto SE) il ForkJoinListMutator possa risultare più performante di una modifica puramente sequenziale con un banale for-each, dipende da 2 fattori: 1) quanti elementi ci sono nella lista, 2) quanto "pesa" computazionalmente il lavoro di modifica di ciascun elemento.
Per dire: se hai solo 1000 stringhe e vuoi fare il reverse di ciascuna, allora sicuramente il ForkJoinListMutator NON velocizza un bel niente.
CONCLUSIONI:
In generale, è così che devi ragionare quando c'è da fare cose di questo tipo. Si realizza un certo algoritmo (una volta sola!) e lo si rende "parametrabile". Parametrabile non solo in termini di semplici "dati" (es. quel
seqThreshold) ma anche e soprattutto come "
comportamenti". Cioè devi saper "staccare" l'algoritmo in sé da quella che è la logica particolare da fare per ciascun caso specifico che può essere anche molto variabile e differente. Lo si fa applicando il principio di astrazione e tipicamente tramite l'uso delle interfacce.
Io l'ho fatto con una interfaccia che è in modo specifico una
functional interface.
Domanda aggiuntiva-extra: nel mio esempio di mutazione di un List<String> ho usato un StringBuilder per il reverse. Se la lista avesse es. 1000000 di stringhe, crea purtroppo 1000000 di oggetti StringBuilder che poi butta via. Ma nel contesto del computeSequentially() che lavora sequenzialmente su es. 10000 (default) oggetti, si POTREBBE benissimo riusare lo stesso oggetto StringBuilder (ad esempio resettando il suo length). Nota però che NON puoi banalmente usare un StringBuilder totalmente "globale", perché la lambda di implementazione viene chiamata da più/molti thread differenti, che causerebbe solo grandi disastri.
Modificando leggermente il MutateTask (e modificando/aggiungendo metodi mutate() ), cosa pensi si potrebbe aggiungere per fare in modo che la lambda (o comunque la implementazione della functional interface) possa RI-usare uno/più oggetti di "contesto" ma SOLO all'interno di ciascun "run" del computeSequentially()? Se non ci arrivi ... te lo dico io.