Metodologie di Programmazione: Lezione 13
Una sequenza di passi eseguiti da un computer in ordine sequenziale, cioè uno dopo l'altro, è detta execution thread o brevemente thread. I programmi considerati finora sono eseguiti nel modello di programmazione detto single-threaded, cioè in un solo thread di esecuzione. In molte situazioni però l'uso di più thread di esecuzione è conveniente e a volte indispensabile. In un'applicazione con GUI, se le animazioni e i video fossero eseguiti nello stesso thread che gestisce l'interazione con l'utente, l'applicazione potrebbe risultare scarsamente reattiva agli input dell'utente. Un'applicazione che risponde a eventi o a comunicazioni remote, come ad esempio un server web, può usare centinaia di thread di esecuzione per rispondere ad altrettante richieste contemporanee emulando il comportamento di centinaia di server e minimizzando così i tempi di risposta. Un web browser può usare decine di thread per scaricare simultaneamente altrettante risorse (immagini, video e altro) contenute in una pagina, ammortizzando i tempi di attesa delle connessioni remote e velocizzando così la visualizzazione della pagina.
In una macchina monoprocessore le esecuzioni di più thread si intrecciano tra loro con rapidi scambi tra i rispettivi flussi di esecuzione. Ma in ogni dato istante, al più un solo thread è in esecuzione. Invece in una macchina multiprocessore o multicore, più thread possono essere in esecuzione simultaneamente. Mentre un thread è eseguito da un processore, un altro thread può essere eseguito da un altro processore. Quindi nelle moderne macchine multicore l'esecuzione dei thread più avvenire in parallelo offrendo così un ulteriore motivo per l'uso di più thread, cioè la possibilità di velocizzare le computazioni.
Purtroppo questi importanti benefici non sono, in generale, gratis. La programmazione di un'applicazione che vuole sfruttare al meglio i vantaggi offerti dall'uso di più thread può non essere affatto facile. Come avremo modo di vedere la programmazione concorrente (o multithreading), cioè la programmazione di più thread di esecuzione, può essere molto più delicata e complessa di quella di un singolo thread.
In questa prima lezione dedicata alla programmazione concorrente in Java, introduciamo solamente i concetti di base illustrati con alcuni semplici esempi. Nelle lezioni successive approfondiremo l'argomento e discuteremo esempi più significativi.
Quando la JVM viene lanciata c'è un unico1 thread di esecuzione il quale invoca il metodo main
di una classe. Tutti i programmi visti finora sono eseguiti esclusivamente in questo thread che è detto appunto main thread. Tuttavia, durante l'esecuzione il programma può creare altri thread esplicitamente o implicitamente. Come vedremo in una prossima lezione, l'uso di una GUI comporta la creazione implicita di uno o più thread di esecuzione che non sono direttamente visibili dal main thread. Dal main thread possiamo creare esplicitamente un thread tramite la classe Thread
in java.lang
. Ogni oggetto Thread
rappresenta uno specifico thread di esecuzione. Il programma che si vuole eseguire nel thread può essere specificato in vari modi, quello preferibile è definire il programma tramite l'interfaccia Runnable
, in java.lang
, e creare il thread tramite il costruttore:
Thread(Runnable target)
Thread
che quando verrà fatto partire invocherà, nel nuovo thread, il metodo run
di target
.Runnable
ha il solo metodo void run()
che serve proprio a definire un metodo che esegue un programma o task in un qualche thread di esecuzione. Quindi i passi per creare un nuovo thread ed eseguirlo sono i seguenti.
Thread
impostando il task che deve essere eseguito ed eventualmente altre proprietà del thread.void start()
. L'invocazione di start()
ritorna immediatamente, non attende che l'esecuzione di run()
nel nuovo thread abbia termine. L'esecuzione di run()
avverrà in modo concorrente alla continuazione dell'esecuzione del thread che ha iniziato il nuovo thread (e di eventuali altri thread).Vediamo un semplice esempio in cui dal main thread viene creato e iniziato un nuovo thread che stampa 50 linee di testo e allo stesso tempo anche il main thread stampa 50 linee di testo.
package mp.concur;
/** Classe per semplici esempi sui threads */
public class TestThread {
public static void main(String[] args) {
Runnable task = () -> { // Task da eseguire in un nuovo thread
for (int i = 0 ; i < 50 ; i++)
out.println("Nuovo "+i);
};
Thread t = new Thread(task); // Crea un nuovo thread
t.start(); // Inizia l'esecuzione del nuovo thread
for (int i = 0 ; i < 50 ; i++)
out.println("MAIN "+i);
}
}
Il risultato potrebbe essere qualcosa del tipo:
Nuovo 0
Nuovo 1
Nuovo 2
Nuovo 3
Nuovo 4
Nuovo 5
Nuovo 6
Nuovo 7
Nuovo 8
Nuovo 9
Nuovo 10
Nuovo 11
Nuovo 12
Nuovo 13
Nuovo 14
Nuovo 15
Nuovo 16
MAIN 0
MAIN 1
MAIN 2
Nuovo 17
Nuovo 18
MAIN 3
MAIN 4
MAIN 5
MAIN 6
MAIN 7
MAIN 8
MAIN 9
. . .
Come si vede i passi del thread principale e del nuovo thread si intrecciano in modo disordinato, mantenendo però l'ordinamento proprio di ogni singolo thread. Probabilmente, ad ogni esecuzione del programma si otterrà un intreccio differente. Questo perché l'ordine di esecuzione dei passi di due o più thread dipende da molti fattori tra cui le sofisticate ottimizzazioni dinamiche operate dal JIT della JVM. Quest'ultime rendono praticamente impossibile prevedere quale sarà l'ordine con cui si intrecceranno i passi dei diversi thread.
L'esecuzione di un nuovo thread inizia con l'invocazione del suo metodo start
, il quale a sua volta invocherà il metodo run
nel nuovo thread. Il metodo start
di un thread può essere invocato una sola volta, le volte successive lancia l'eccezione IllegalThreadStateException
. Quindi un thread può essere usato per eseguire un task una sola volta e non può più essere riusato. Però il task, al pari di qualsiasi programma, può fare qualsiasi cosa, come creare altri thread o rimanere in esecuzione continuativamente. Un thread termina non appena termina l'esecuzione del suo run()
. E questo può accadere in qualsiasi modo, o eseguendo un return
, o raggiungendo l'ultima istruzione o a causa di un'eccezione non catturata.
In una versione iniziale di Java fu introdotto il metodo stop
che può essere invocato da un thread per terminare bruscamente un altro thread. Per motivi di compatibilità è ancora presente ma ormai da molto tempo il suo uso è fortemente sconsigliato, come si dice, il metodo è deprecato (deprecated). Le ragioni sono molteplici, sintetizzando si può dire che il metodo stop
interrompe brutalmente un task senza la cooperazione del task stesso e questo può lasciare oggetti o risorse in uno stato inconsistente. Si pensi ad esempio, all'interruzione brutale di un task che sta effettuando una transazione bancaria o una prenotazione di un volo aereo. In generale i metodi deprecati devono essere considerati come errori di progettazione che non sarebbero mai dovuti esistere. Per ragioni di compatibilità non si possono eliminare ma nessun programma attuale dovrebbe usarli neanche nei casi in cui il loro uso può sembrare innocente.
Quindi non c'è nessun modo adeguato per forzare la terminazione di un thread. Però si può chiedere la terminazione invocando il metodo void interrupt()
. L'effetto, se il thread sul quale è invocato è ancora in esecuzione, è che un flag interno booleano interrupted status è impostato a true
. Il task eseguito dal thread può usare il metodo boolean isInterrupted()
per sapere se c'è una richiesta di interruzione del thread ed agire di conseguenza. Ma se il task non fa attenzione al interrupted status, le richieste di interruzione non avranno effetto.
Consideriamo come esempio un thread contatore che dopo ogni secondo stampa il numero di millisecondi da quando è iniziata la sua esecuzione.
/** Aspetta che sia passato il numero di millisecondi specificato
* @param millis numero di millisecondi di attesa */
public static void waitFor(long millis) {
long time = System.currentTimeMillis();
while (System.currentTimeMillis() - time < millis) ;
}
public static void main(String[] args) {
Thread counter = new Thread(() -> {
long start = System.currentTimeMillis();
while (true) { // Ogni secondo stampa il numero di millisecondi
waitFor(1000); // passati dalla partenza del thread
out.println(System.currentTimeMillis() - start);
}
});
counter.start();
waitFor(4000);
counter.interrupt();
}
Il thread principale fa partire il thread contatore, aspetta per 4 secondi e poi chiede l'interruzione del thread. Se proviamo ad eseguire il programma,
1000
2000
3000
4000
5000
6000
7000
8000
9000
10000
. . .
ci accorgiamo che il thread contatore non termina. Affinché il thread sia effettivamente terminabile è necessario che faccia un controllo esplicito (e periodico) del interrupted status.
Thread counter = new Thread(() -> {
long start = System.currentTimeMillis();
while (true) { // Ogni secondo stampa il numero di millisecondi
waitFor(1000); // passati dalla partenza del thread
out.println(System.currentTimeMillis() - start);
if (Thread.currentThread().isInterrupted()) break;
}
out.println("Conteggio terminato");
});
counter.start();
waitFor(4000);
counter.interrupt();
Per ottenere il Thread
che è correntemente in esecuzione si usa il metodo statico static Thread currentThread()
. Cioè Thread.currentThread()
ritorna l'oggetto Thread
del thread che sta eseguendo la detta invocazione. Provando la nuova versione del programma otteniamo,
1000
2000
3000
4000
5000
Conteggio terminato
Ora il thread contatore non appena si accorge che c'è una richiesta di interruzione termina la propria esecuzione.
L'implementazione che abbiamo dato del contatore non è soddisfacente perché spreca la maggior parte del tempo di esecuzione nelle attese. Durante le attese non fa nulla di significativo ma toglie tempo di esecuzione ad eventuali altri thread. Per casi come questi è conveniente usare il metodo statico
static void sleep(long millis) throws InterruptedException
InterruptedException
.sleep
perché così dà la possibilità alla JVM di dare più tempo all'esecuzione di altri thread. Inoltre non c'è più bisogno di controllare l'interrupted status, perché se c'è una richiesta di interruzione anche se è stata fatta prima di invocare sleep
quest'ultimo lancerà InterruptedException
.
Thread counter = new Thread(() -> {
long start = System.currentTimeMillis();
while (true) { // Ogni secondo stampa il numero di millisecondi
try { // passati dalla partenza del thread
Thread.sleep(1000);
} catch (InterruptedException e) { break; }
out.println(System.currentTimeMillis() - start);
}
out.println("Conteggio terminato");
});
counter.start();
waitFor(4000);
counter.interrupt();
In ogni istante un thread si trova in uno specifico stato, dei sei stati sotto descritti, che si può ottenere con il metodo Thread.State getState()
NEW
new
, ma non è stato ancora invocato il metodo start
.RUNNABLE
start
, il thread è eseguibile. Ma dipende dal sistema operativo e dalla JVM se è in esecuzione attualmente. Si trova in questo stato se è in esecuzione o non è in esecuzione ma è eseguibile, cioè pronto per essere eseguito.BLOCKED
WAITING
wait
. Uscirà da questo stato quando ci sarà una notifica circa il soddisfacimento della condizione.TIMED_WAITING
sleep
.TERMINATED
run
termina o normalmente o a causa di un'eccezione non catturata. Una volta che un thread entra in questo stato, vi rimane per sempre.
Gli stati NEW
e TERMINATED
sono, rispettivamente, lo stato iniziale e quello terminale, mentre gli altri sono stati intermedi. Durante la sua vita un thread può passare più volte per ognuno degli stati intermedi. Quando un thread si trova in BLOCKED
, WAITING
o TIMED_WAITING
, è temporaneamente inattivo, non è in esecuzione e il suo consumo di risorse è minimo.
Uno dei motivi che rendono la programmazione multithreading conveniente, se non indispensabile, è il mantenimento della reattività (responsiveness) della UI, cioè dell'interfaccia utente. Si immagini un'applicazione che su richiesta dell'utente deve eseguire un'operazione che può richiedere molto tempo (ad es. a causa di calcoli intensi o per l'accesso a risorse remote), se usa un unico thread, l'utente non può interagire con la UI mentre sta elaborando l'operazione, né può interrompere l'operazione stessa. Come minimo occorrono due thread, uno per gestire la UI e l'altro per eseguire l'operazione.
In seguito vedremo esempi riguardanti interfacce grafiche. Adesso consideriamo un semplice esempio relativo all'interfaccia testuale di un'applicazione che permette all'utente di digitare un intero n e di ottenere l'n-esimo numero di Fibonacci Fn, anche per valori di n molto grandi. Per poter calcolare anche solo F200 il tipo long
non è sufficiente, occorre la precisione illimitata dei BigInteger
in java.math
. Ovviamente le operazioni aritmetiche con il tipo BigInteger
sono significativamente più lente che con il tipo long
. Quindi se chiediamo alla nostra applicazione di calcolare F100000 o peggio F1000000, queste operazioni potrebbero richiedere molto tempo. Perciò il thread principale sarà usato per gestire l'interazione con l'utente (lettura dell'input) e un altro thread sarà usato per calcolare Fn.
public static void fibonacci() {
Scanner input = new Scanner(System.in);
out.println("Digita n per calcolare F_n o 0 per terminare");
Thread comp = null;
while (true) {
long n = input.nextLong();
if (n <= 0) break;
if (comp != null) comp.interrupt();
comp = new Thread(() -> {
BigInteger a = BigInteger.valueOf(0);
BigInteger b = BigInteger.valueOf(1);
for (long i = 1; i < n; i++) {
BigInteger c = b.add(a);
a = b;
b = c;
if (Thread.interrupted()) {
out.println("Calcolo di F_"+n+" interrotto");
return;
}
}
out.println("F_"+n+" = "+b);
});
comp.start();
}
out.println("Fine");
}
Per semplicità il controllo sugli errori è omesso. È stato usato il metodo statico Thread.interrupted()
invece di Thread.currentThread().isInterrupted()
. L'unica differenza tra i due metodi è che il primo re-imposta l'interrupted status sempre a false
dopo la lettura. Provando fibonacci()
potremmo avere,
Digita n per calcolare F_n o 0 per terminare
100
F_100 = 354224848179261915075
200
F_200 = 280571172992510140037611932413038677189525
1000
F_1000 = 43466557686937456435688527675040625802564660517371780402481729089536555
41794905189040387984007925516929592259308032263477520968962323987332247
1161642996440906533187938298969649928516003704476137795166849228875
1000000
7
Calcolo di F_1000000 interrotto
F_7 = 13
0
Fine
Avere informazioni riguardo ai thread può essere utile per tenere sotto controllo le operazioni in multithreading, per misurare le prestazioni o per il testing. Il modo più semplice di ottenere alcune di queste informazioni è direttamente dall'oggetto Thread
/** Ritorna una stringa con informazioni relative al thread specificato.
* @param th un thread
* @return una stringa con informazioni relative al thread */
public static String threadInfo(Thread th) {
ThreadGroup group = th.getThreadGroup();
return String.format("Id %2d Daemon %s %-13s Group %-10s Name %s",
th.getId(), (th.isDaemon() ? "Y" : "N"), th.getState(),
(group != null ? group.getName() : "-"), th.getName());
}
Il metodo ThreadGroup getThreadGroup()
ritorna il ThreadGroup
del thread. Se non è specificato al momento della creazione, è quello del thread in cui il thread è stato creato. Può essere utile per determinare l'origine del thread, ad es. se è un thread del sistema o creato dall'utente. Ad ogni thread
è assegnato un numero di identificazione, detto id, che si ottiene con long getId()
, però quando il thread termina il suo id può essere assegnato ad un altro thread. Una proprietà importante di un thread è quella di essere o non essere un daemon thread. La JVM non termina finché c'è almeno un thread non daemon attivo, se invece sono rimasti attivi solamente thread daemon, la JVM termina immediatamente. Un thread può essere impostato come daemon tramite void setDaemon(boolean on)
, ma solamente prima di invocare start()
. Il metodo boolean isDaemon()
permette di sapere se il thread è daemon. Infine il nome di un thread si ottiene con String getName()
e può essere impostato al momento della creazione con Thread(Runnable target, String name)
o successivamente con void setName(String name)
.
Tutti i thread attualmente attivi, cioè quelli partiti e non ancora terminati, si ottengono con il metodo statico della classe Thread
: Map<Thread,StackTraceElement[]> getAllStackTraces()
. La mappa ritornata associa ad ogni thread attivo la sua stack trace momentanea. La stack trace è la traccia dello stack delle chiamate, cioè la pila delle chiamate di metodi e costruttori che determina l'attuale punto di esecuzione, ne vedremo un esempio fra poco. Per adesso ci limitiamo ad ottenere dalla mappa l'insieme dei thread attivi e per ognuno le informazioni su di esso.
/** @return una stringa con informazioni sui thread attualmente attivi */
public static String liveThreadInfo() {
String s = "";
for (Thread t : Thread.getAllStackTraces().keySet())
s += threadInfo(t) + "\n";
return s;
}
Provando il metodo potremmo ottenere (su un'altra macchina o sotto altre condizioni l'output può essere diverso)
Id 2 Daemon Y WAITING Group system Name Reference Handler
Id 1 Daemon N RUNNABLE Group main Name main
Id 4 Daemon Y RUNNABLE Group system Name Signal Dispatcher
Id 10 Daemon Y RUNNABLE Group main Name Monitor Ctrl-Break
Id 3 Daemon Y WAITING Group system Name Finalizer
Come si vede, tra i thread attivi solamente quelli del gruppo main
non sono daemon. Miglioriamo ora il metodo liveThreadInfo
così che possa anche riportare le stack trace fino a una specificata profondità
/** Ritorna una stringa con informazioni sui thread attualmente attivi.
* @param stackDepth la massima profondità delle stack trace
* @return una stringa con informazioni sui thread attualmente attivi */
public static String liveThreadInfo(int stackDepth) {
String[] s = {""}; // Per aggirare il vincolo dell'effettivamente final
Thread.getAllStackTraces().forEach((t,st) -> {
s[0] += threadInfo(t) + "\n";
for (int i = 0 ; i < st.length && i < stackDepth ; i++)
s[0] += " "+st[i]+"\n";
});
return s[0];
}
Provandolo con un limite 5 sulla profondità delle stack trace, otteniamo
Id 2 Daemon Y WAITING Group system Name Reference Handler
java.lang.Object.wait(Native Method)
java.lang.Object.wait(Object.java:502)
java.lang.ref.Reference$ReferenceHandler.run(Reference.java:157)
Id 1 Daemon N RUNNABLE Group main Name main
java.lang.Thread.dumpThreads(Native Method)
java.lang.Thread.getAllStackTraces(Thread.java:1603)
test.TestThreads.liveThreadInfo(TestThreads.java:82)
test.TestThreads.main(TestThreads.java:120)
sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
Id 4 Daemon Y RUNNABLE Group system Name Signal Dispatcher
Id 10 Daemon Y RUNNABLE Group main Name Monitor Ctrl-Break
java.net.InetAddress.<clinit>(InetAddress.java:289)
java.net.InetSocketAddress.<init>(InetSocketAddress.java:187)
java.net.ServerSocket.<init>(ServerSocket.java:237)
java.net.ServerSocket.<init>(ServerSocket.java:128)
com.intellij.rt.execution.application.AppMain$1.run(AppMain.java:89)
Id 3 Daemon Y WAITING Group system Name Finalizer
java.lang.Object.wait(Native Method)
java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)
java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164)
java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)
Come c'era da aspettarsi quando è stato fatto il dump (cioè l'istantanea) dei thread attivi, il main thread stava eseguendo getAllStackTraces()
che a sua volta era impegnato nell'esecuzione di dumpThreads()
che evidentemente è un metodo privato di Thread
. Si noti che per alcuni thread di sistema che operano a basso livello, come Signal Dispatcher
, la stack trace non è disponibile.
Facciamo ora una prova creando nuovi thread e invocando liveThreadInfo(1)
dal main thread e poi da un nuovo thread,
new Thread(() -> waitFor(10_000), "Nuovo 1").start();
new Thread(() -> {
try {
Thread.sleep(10_000);
} catch (InterruptedException e) {}
}, "Nuovo 2").start();
out.println(liveThreadInfo(1));
new Thread(() -> out.println(liveThreadInfo(1)), "Nuovo 3").start();
potremmo ottenere
Id 4 Daemon Y RUNNABLE Group system Name Signal Dispatcher
Id 1 Daemon N RUNNABLE Group main Name main
java.lang.Thread.dumpThreads(Native Method)
Id 12 Daemon N TIMED_WAITING Group main Name Nuovo 2
java.lang.Thread.sleep(Native Method)
Id 11 Daemon N RUNNABLE Group main Name Nuovo 1
java.lang.System.currentTimeMillis(Native Method)
Id 2 Daemon Y WAITING Group system Name Reference Handler
java.lang.Object.wait(Native Method)
Id 3 Daemon Y WAITING Group system Name Finalizer
java.lang.Object.wait(Native Method)
Id 10 Daemon Y RUNNABLE Group main Name Monitor Ctrl-Break
java.net.PlainSocketImpl.socketAccept(Native Method)
Id 13 Daemon N RUNNABLE Group main Name Nuovo 3
java.lang.Thread.dumpThreads(Native Method)
Id 4 Daemon Y RUNNABLE Group system Name Signal Dispatcher
Id 12 Daemon N TIMED_WAITING Group main Name Nuovo 2
java.lang.Thread.sleep(Native Method)
Id 11 Daemon N RUNNABLE Group main Name Nuovo 1
test.TestThreads.waitFor(TestThreads.java:29)
Id 2 Daemon Y WAITING Group system Name Reference Handler
java.lang.Object.wait(Native Method)
Id 3 Daemon Y WAITING Group system Name Finalizer
java.lang.Object.wait(Native Method)
Id 10 Daemon Y RUNNABLE Group main Name Monitor Ctrl-Break
java.net.PlainSocketImpl.socketAccept(Native Method)
Id 14 Daemon N RUNNABLE Group main Name DestroyJavaVM
Nel secondo report, cioè quello eseguito nel thread nuovo 3
, manca il main thread perché in quel momento era già terminato. Però il programma non termina finché non terminano tutti i thread non daemon (nel nostro caso richiede circa 10 secondi). Si noti che il secondo report, che è stato eseguito immediatamente dopo che il main thread è terminato, ha catturato un'istantanea del thread DestroyJavaVM
che è eseguito non appena il main thread termina.
Nelle prossime lezioni vedremo molti esempi significativi d'uso della programmazione multithreading o concorrente.
[Balance] Scrivere un programma che dati due interi k e n, crea k thread che fanno tutti lo stesso calcolo: determinano se n è un numero primo o meno. Ogni thread quando termina stampa il suo nome, il risultato del test di primalità e il tempo in millisecondi che ha impiegato. Per il test di primalità si può usare il metodo mp.util.Utils.prime
. Provare il programma con primi grandi, ad es. 1_000_000_000_000_000_003L
e variando il numero k di thread. I tempi dei diversi thread sono simili? Come variano al variare di k?
[SleepOrNotSleep] Scrivere un programma che dato un booleano s e un intero k, crea k thread di tipo contatore (come quelli sopra) e se s è true
usano il metodo sleep
altrimenti usano il metodo waitFor
, e crea un thread come quelli dell'esercizio precedente (con input un primo grande). Provare il programma variando s e k. Che differenze si osservano? A cosa sono dovute?
[Fibonacci+] Modificare il programma fibonacci
in modo tale che possa mantenere in esecuzione fino a K thread per il calcolo di numeri di Fibonacci, dove K è un parametro fissato. Il programma dato sopra è relativo a K = 1. Quando l'utente chiede di effettuare un nuovo calcolo, il programma controlla quanti thread sono attualmente in esecuzione, se sono meno di K ne crea uno per il nuovo calcolo, altrimenti prima di creare il nuovo thread interrompe quello più vecchio in esecuzione. Per determinare se un thread è vivo (cioè non è ancora terminato) si può usare il metodo boolean isAlive()
.
[Monitor] Creare un daemon thread che periodicamente, cioè ogni tot secondi, stampa un report dei thread attualmente attivi. Provarlo ad esempio nei programmi degli esercizi precedenti.
[Monitor+] Migliorare il daemon thread dell'esercizio precedente così che possa produrre report più versatili secondo i parametri: ordine di stampa delle info dei thread (tramite Comparator<Thread>
), filtro per i thread da monitorare (tramite Predicate<Thread>
), profondità delle stack trace, possibilità di registrare i thread terminati e report di quest'ultimi.
[Parallel] Per stimare le prestazioni di esecuzione concorrente e parallela (cioè con più processori in parallelo) si possono creare k thread ognuno dei quali esegue lo stesso calcolo e periodicamente, diciamo una volta ogni secondo, stampa lo stato di avanzamento del calcolo. Come calcolo si può usare il test di primalità mp.util.Utils.prime
con input 1_000_000_000_007L
ripetuto in un loop. Lo stato di avanzamento è il conteggio delle volte che è stato eseguito il test nell'ultimo periodo di un secondo. I thread dovrebbero essere daemon e devono partire il più possibile contemporaneamente. Il main potrebbe attendere un po' di secondi prima di terminare. Sperimentare variando il numero di thread k = 1,2,4,8,16,32. Come varia la somma dei conteggi dei k thread al variare di k? Perché all'aumentare di k tale somma prima cresce significativamente e poi rimane pressoché invariata?
19 Apr 2016