Componenti:
L’obiettivo è quello di affrontare il problema descritto nell’assignment 1, utilizzando quattro diversi approcci di programmazione asincrona, descritti di seguito.
Si è deciso di uniformare l’interfaccia degli approcci, sfruttando opportune astrazioni, sia per la versione CLI, che per la versione GUI.
interface SourceAnalyzer {
Future<Report> getReport(Directory directory);
ObservableReport analyzeSources(Directory directory);
}
Nel caso di getReport(Directory directory)
, verrà restituita una Future, che sarà completata, in modo asincrono, con il report dell’analisi.
Questo permette, in tutti i casi, di sottomettere la computazione e poter successivamente attendere, bloccando opportunamente il Thread che vuole ottenere il risultato.
Un approccio alternativo è quello di aggiungere un Runnable al metodo, che verrà eseguito quando la computazione sarà completata. Questo permette di non dover attendere un risultato, ma il codice specificato, sarà eseguito al termine del calcolo delle statistiche (e.g una stampa a video del risultato).
Invece, per quanto riguarda analyzeSources(Directory directory)
, verrà restituito un oggetto osservabile, che permette di registrare, ai due componenti attivi (GUI e SourceAnalyzer), degli Handler agli eventi.
L’approccio mediante Executor è stato implementato mediante l’utilizzo di un ForkJoinPool. Per la divisione del lavoro, sono stati individuati i seguenti Recursive Task:
Questo approccio facilita la versione CLI, permettendo di esplorare ricorsivamente, con gli opportuni RecursiveTask
, aggregando successivamente i risultati parziali.
Solo alla fine viene viene ritornato il risultato finale.
Invece, per la versione GUI, è stato necessario utilizzare Monitor, in modo da poter notificare e stampare a video le statistiche incrementate gradualmente.
L’approccio mediante l’utilizzo di Virtual Threads riutilizza l’idea con cui è stata realizzata l’implementazione Executor.
La differenza principale è l’utilizzo di un newVirtualThreadPerTaskExecutor
, i cui task sottomessi sono delle Callable
.
Anche in questo sono individuate i due task principali:
Come per l’approccio precedente, anche in questo caso l’implementazione favorisce la versione CLI, permettendo di esplorare ricorsivamente, con gli opportuni Callable
, aggregando successivamente i risultati parziali e minimizzando le corse critiche.
Invece, per la versione GUI, viene utilizzato il Monitor, come descritto in precedenza.
L’approccio a Event Loop è stato implementato utilizzando la libreria Vertx. L’architettura realizzata prevede un singolo Verticle che si occupa di eseguire l’esplorazione ricorsiva delle directory e calcolare il report dell’analisi. Questo approccio permette di evitare corse critiche, riducendo il codice necessario per la sincronizzazione.
Il flow dell’esecuzione è il seguente:
Il problema principale di questo approccio è quello di capire quando la computazione è terminata, dal momento che le esecuzioni delle computazioni sono asincrone. L’implementazione realizzata prevede di mantenere un contatore che viene incrementato ad ogni chiamata asincrona e decrementato al completamento. In questo modo l’ultima chiamata asincrona decrementerà porta il contatore a 0, permettendo di notificare il completamento della computazione.
Per quanto riguarda l’implementazione della versione GUI viene riutilizzato quasi completamente l’approccio CLI, aggiungendo una comunicazione attraverso l’EventBus
di Vertx.
In questo modo, per terminare la computazione tramite l’interfaccia, è sufficiente inviare un messaggio sul Bus, che verrà ricevuto dal Verticle.
L’approccio Reactive è stato implementato utilizzando la libreria RxJava.
La parte principale di questo approccio è la creazione di un Observable
, che permette di esplorare la directory di partenza.
Ad esso sono aggiunte le opportune operazioni di Map e Filter, per ottenere il contenuto dei File e poterli aggregare.
Si è creato un Observable
, invece che un Flowable
, poiché lo stream dei files è lazy, così non è necessario gestire il meccanismo di Backpressure.
Le differenze tra CLI e GUI sono minime. Nel secondo caso è necessario notificare l’Observer per poter stampare a video i progressi intermedi.
Condizioni di testing:
La soluzione più performante in termini di tempo è quella basata su Executor, seguita subito da quella basata su Virtual Threads.
Il risultato che l’implementazione a Event Loop è la più lenta era atteso, poiché la computazione è svolta da un singolo thread.
N. | Executor (ms) | VirtualThread (ms) | EventLoop (ms) | Reactive (ms) |
---|---|---|---|---|
1 | 4077 | 5785 | 52132 | 18412 |
2 | 3354 | 3414 | 52171 | 16965 |
3 | 3114 | 3072 | 50270 | 17005 |
4 | 3044 | 3105 | 51961 | 16907 |
5 | 3210 | 3159 | 51622 | 17104 |
6 | 3130 | 3070 | 52242 | 18175 |
7 | 3036 | 3141 | 51835 | 18094 |
8 | 2901 | 3088 | 50788 | 17191 |
9 | 2975 | 3235 | 51232 | 16776 |
10 | 3141 | 3136 | 50780 | 16813 |
mean | 3198,2 | 3420,5 | 51503,3 | 17344 |
Anche nelle implementazioni per la GUI vediamo un andamento delle performance paragonabile, notando un incremento generale dei tempi di esecuzione.
N. | Executor (ms) | VirtualThread (ms) | EventLoop (ms) | Reactive (ms) |
---|---|---|---|---|
1 | 5081 | 7155 | 55118 | 21740 |
2 | 4684 | 5415 | 52816 | 21099 |
3 | 4640 | 4941 | 53943 | 21213 |
4 | 4888 | 4946 | 53893 | 20279 |
5 | 4717 | 4713 | 52778 | 20931 |
6 | 4482 | 4713 | 52957 | 20284 |
7 | 4441 | 4885 | 52737 | 22182 |
8 | 4670 | 4628 | 52047 | 20236 |
9 | 4546 | 4740 | 54733 | 20310 |
10 | 4436 | 4859 | 52925 | 20783 |
mean | 4658,5 | 5099,5 | 53394,7 | 20905,7 |