Al momento la soluzione non esiste. Nel repository pcd-2025-2026 la cartella assignments/assignment-02/ contiene solo il README.md del docente (v1.0.0-20260412): nessuna delle tre versioni richieste è stata implementata, non c'è una cartella src né la relazione doc/report.pdf. Questa pagina descrive quindi cosa va costruito e da quale materiale di laboratorio partire, non un'architettura già realizzata. Va aggiornata appena il codice esiste.
La consegna chiede una libreria FSStatLib con un metodo asincrono getFSReport(D, MaxFS, NB) che calcola, ricorsivamente su una directory D: il numero totale di file e la distribuzione delle dimensioni in NB bande sull'intervallo [0, MaxFS] più una banda per i file oltre MaxFS (quindi NB + 1 intervalli). Sono richieste tre versioni indipendenti — il README rimarca esplicitamente di non generalizzare o riusare codice tra le versioni se questo compromette la disciplina di ciascun approccio — più un'estensione interattiva opzionale (stop + aggiornamenti live) obbligatoria per puntare al 30L.
Non implementata. La base di partenza è il materiale Vert.x di lab-activities/lab-activity-07/pcd-lab-07/src/main/java/pcd/lab07/vertx/: in particolare Step1_basic.java e Step2_withverticle.java (event loop e verticle), Step3_chaining.java, Step4_compo.java e Step5_promise.java (composizione di Future/Promise, il cuore della versione asincrona di getFSReport), Step6_withblocking_simple.java e varianti Step6b/6c/6d (come trattare operazioni bloccanti — l'I/O su filesystem — senza bloccare l'event loop).
Non implementata. La base è lab-activities/lab-activity-08/pcd-lab-08/src/main/java/pcd/lab08/rx/: creazione di flussi (Test02a–Test02e), scheduler con subscribeOn/observeOn (Test03a–Test03c), integrazione con Swing via PublishSubject (Test04a_swing_pubsub.java, utile per l'estensione interattiva) e le strategie di backpressure (Test06a–Test06d). L'idea attesa: un flusso dei file visitati, trasformato con operatori (es. flatMap per la ricorsione sulle sottodirectory, scan/aggregazione per le statistiche) — vedi Cap. 11.
Non implementata. La base è lab-activities/lab-activity-06/pcd-lab-06/src/main/java/pcd/lab06/virtual_threads/: TestVT.java (creazione con Thread.ofVirtual() e mutua esclusione con ReentrantLock, che a differenza di synchronized nelle versioni Java meno recenti non causa pinning del carrier thread), TestScalabilityVT.java e TestScalabilityVT2.java (scalabilità in numero di thread). L'idea attesa: un virtual thread per directory (o per file), I/O bloccante scritto in stile diretto, risultati parziali fusi alla fine o accumulati in una struttura condivisa protetta.
Non implementata. Richiederebbe: possibilità di fermare la generazione del report e aggiornamenti dinamici delle statistiche, con una piccola GUI (start/stop + area di visualizzazione). Ogni versione ha il suo meccanismo naturale di cancellazione e notifica (vedi §2.5).
Manca anche doc/report.pdf (analisi del problema dal punto di vista concorrente + strategia adottata), che è parte del deliverable e tipicamente la prima cosa che il docente apre all'orale.
Non essendoci ancora codice, queste sono le scelte di progetto che andranno compiute — e che all'orale andranno difese. Per ciascuna è indicato il capitolo di teoria che la copre.
getFSReport deve essere asincronoLa scansione ricorsiva di un filesystem è I/O-bound e di durata imprevedibile: un metodo sincrono bloccherebbe il chiamante (ad es. l'event dispatch thread di una GUI). L'API deve restituire subito un "contenitore del risultato futuro" — Future/Promise nella versione event-loop, un Single/Observable in Rx, oppure incapsulare l'attesa in un virtual thread. È la motivazione di fondo di tutta la Parte II del corso: Cap. 10 — Programmazione asincrona.
Nella versione Vert.x il vincolo di disciplina è uno solo ma rigido: nessuna chiamata bloccante sull'event loop. Le letture del filesystem vanno fatte con l'API asincrona (o delegate con executeBlocking a un worker pool, come in Step6c_withblocking_threadpool.java). Il problema concorrente più delicato è il completamento: la ricorsione sulle sottodirectory genera un fan-out dinamico di operazioni asincrone e il report va risolto solo quando tutte sono terminate — composizione di Future (Future.all/chaining, come in Step4_compo.java) o un contatore di operazioni pendenti gestito, senza lock, dal singolo thread dell'event loop. Il vantaggio da rivendicare: le statistiche si aggiornano da un solo thread, quindi niente race condition per costruzione. Teoria: Cap. 10.
Nella versione reattiva la scelta chiave è modellare i file come flusso di eventi, non come collezione: sorgente dei path (cold observable), flatMap per appiattire la ricorsione sulle sottodirectory (con eventuale parallelismo via subscribeOn per sotto-flusso, come mostrato in Test03a_sched_subscribeon.java), operatori di aggregazione per contatori e istogramma, observeOn per consegnare i risultati sul thread giusto. Con un flusso potenzialmente enorme di eventi va discussa la backpressure (Flowable e strategie buffer/drop/sample, Test06b–Test06d). Per gli update live: scan emette lo stato incrementale delle statistiche a ogni file. Teoria: Cap. 11 — Programmazione reattiva.
Qui la disciplina è opposta: si torna al codice bloccante in stile diretto — un virtual thread per directory, I/O bloccante normale — perché il blocco di un virtual thread non occupa il carrier thread (Cap. 10). Le scelte da difendere: (a) granularità dei task (per directory vs per file: migliaia di VT costano poco, come mostrano TestScalabilityVT*.java); (b) aggregazione dei risultati — o ogni VT restituisce statistiche parziali fuse dal padre con join (nessuno stato condiviso), o si aggiorna una struttura condivisa protetta da lock/atomics, tornando ai temi di mutua esclusione di Cap. 4 e Cap. 5; (c) uso di ReentrantLock invece di synchronized per evitare il pinning (pattern già presente in TestVT.java).
L'estensione interattiva costringe a confrontare i modelli di cancellazione: in Rx è nativa (dispose() della subscription interrompe il flusso); nella versione event-loop serve un flag controllato tra un passo asincrono e l'altro (l'event loop non si può interrompere dall'esterno); con i virtual threads si usa interrupt() e la propagazione ai figli. Gli aggiornamenti live sono naturali in Rx (il flusso è già lo stream di aggiornamenti), mentre nelle altre versioni serve un canale di notifica esplicito verso la GUI, sempre consegnando sull'event dispatch thread (pattern di Test04a_swing_pubsub.java). Teoria: Cap. 10 e Cap. 11.
Il README lo impone: ogni versione deve essere fedele alla propria disciplina. È una scelta da esplicitare anche nella relazione — ad esempio, condividere una classe "visitor del filesystem" sincrona tra le tre versioni snaturerebbe sia la versione event-loop (che la userebbe bloccando) sia quella Rx (che perderebbe la natura di flusso).
getFSReport deve essere asincrono? Cosa restituisce esattamente al chiamante?Perché la scansione è I/O-bound e di durata arbitraria: un'API sincrona bloccherebbe il chiamante. Il metodo restituisce subito un "posto per il risultato futuro": una Future/Promise (event loop), un Single<Report> o Observable (Rx), o incapsula il lavoro in un virtual thread su cui poi fare join. La differenza tra invocazione e completamento è il concetto chiave del Cap. 10.
Si blocca l'event loop: tutti gli altri eventi (timer, callback, altre richieste) restano in coda e Vert.x segnala il thread bloccato. Si evita usando l'API asincrona del filesystem oppure executeBlocking, che sposta il lavoro su un worker pool e riconsegna il risultato all'event loop — è esattamente la progressione degli step Step6* del lab 07.
È il punto più delicato: il fan-out è dinamico (ogni directory scoperta genera nuove operazioni). Due strategie: comporre le Future dei figli (Future.all ricorsivo: la Future di una directory si completa quando si completano quelle di tutte le sottodirectory), oppure un contatore di operazioni pendenti incrementato/decrementato solo dall'event loop — che essendo single-thread non richiede sincronizzazione. Quando arriva a zero si risolve la Promise del report.
Perché tutte le callback che aggiornano i contatori girano sullo stesso thread dell'event loop: c'è concorrenza tra operazioni I/O ma un solo thread tocca lo stato. È il vantaggio strutturale del modello: si baratta il parallelismo sullo stato con l'assenza di race per costruzione (Cap. 10).
La callback inverte il controllo e annidandosi produce codice illeggibile con gestione errori duplicata. La Promise reifica il risultato futuro in un oggetto componibile: chaining lineare (compose/then), propagazione automatica degli errori, combinatori (all, any). Async-await è zucchero sintattico che restituisce lo stile diretto sopra le Promise. Nel lab: Step3_chaining.java, Step5_promise.java.
Con flatMap: ogni directory emessa viene mappata nel flusso dei suoi elementi (file e sottodirectory, ricorsivamente) e i sotto-flussi vengono appiattiti in un unico flusso di file. flatMap non garantisce l'ordine — irrilevante qui, contiamo occorrenze — e permette di sottoscrivere i sotto-flussi su scheduler diversi ottenendo parallelismo (Cap. 11, §FlatMap).
subscribeOn e observeOn: dove li usereste in FSStat?subscribeOn decide il thread dove gira la sorgente (una sola volta, vale per tutta la catena): qui Schedulers.io() perché la scansione è I/O-bound. observeOn cambia il thread da quel punto in giù: lo si usa prima della subscribe che aggiorna la GUI, per consegnare gli aggiornamenti sull'event dispatch thread. Nel lab: Test03a/Test03b.
Cold: ogni subscriber fa ripartire la scansione dall'inizio; diventa hot solo se si fa da ponte con un PublishSubject (es. per la GUI, Test04a). La backpressure serve se il produttore (scansione veloce di molti file piccoli) supera il consumatore (es. rendering della GUI): con Flowable si sceglie una strategia — buffer, drop, sample/throttle per gli update live, dove perdere aggiornamenti intermedi è accettabile (Test06b–Test06d).
Un virtual thread che si blocca su I/O viene smontato dal carrier thread, che resta libero: bloccare non spreca più un thread OS, quindi "un thread per task" torna praticabile anche con decine di migliaia di task (TestScalabilityVT*.java). Il pinning è l'eccezione: in certi casi (storicamente i blocchi synchronized, o chiamate native) il VT resta inchiodato al carrier; per questo in TestVT.java il monitor usa ReentrantLock.
Due opzioni da saper confrontare: (a) nessuno stato condiviso — ogni VT calcola statistiche parziali della propria directory e il padre le fonde dopo il join dei figli: più pulita, in stile fork/join; (b) struttura condivisa (array di contatori per banda) aggiornata da tutti i VT, protetta con ReentrantLock o AtomicLong/LongAdder: meno allocazioni ma riapre i problemi di sezione critica dei Cap. 4–5. Va detta la scelta e il perché.
Rx: dispose() della subscription — la cancellazione risale la catena e ferma la sorgente. Event loop: non si può interrompere una callback in corso; serve un flag consultato a ogni passo asincrono prima di lanciarne altri. Virtual threads: interrupt() propagato ai VT figli, che escono al prossimo punto bloccante controllando l'interruzione. È la domanda perfetta per mostrare che i tre modelli sono davvero diversi.
Gli aggiornamenti Swing vanno solo sull'event dispatch thread: in Rx basta observeOn con lo scheduler Swing, altrove SwingUtilities.invokeLater. Per la consistenza conviene pubblicare snapshot immutabili delle statistiche (copia dei contatori) invece di far leggere alla GUI la struttura viva mentre altri thread la modificano.
Risposta attesa: dipende dal criterio. Virtual threads: codice più semplice e diretto, ottimo per I/O-bound massivo. Rx: vince sull'estensione interattiva, perché stop e update live sono primitivi del modello. Event loop: massima economia di thread e niente sincronizzazione, ma composizione della ricorsione più laboriosa. L'importante è motivare col problema (I/O-bound, fan-out dinamico, esigenza di update), non con preferenze generiche.
Perché ogni versione deve esibire la disciplina del proprio paradigma: un'astrazione comune (es. un visitor sincrono del filesystem) costringerebbe la versione event-loop a bloccare, quella Rx a rinunciare al flusso, quella VT a un'inversione di controllo inutile. L'assignment valuta la padronanza dei tre stili, non l'ingegneria del riuso.
Il punto debole principale è che l'assignment non è stato ancora svolto: nessuna delle tre versioni esiste nel repository, né la relazione. All'orale la discussione è costruita sul proprio codice: senza deliverable non c'è discussione. La priorità è implementare, non ripassare.
getFSReport(D, MaxFS, NB) e un programma di esempio per ciascuna versione.doc/report.pdf è parte del deliverable: analisi concorrente del problema + strategia per versione. Scriverla dopo il codice, ma prevedere il tempo.Step4_compo.java e Step5_promise.java prima di scrivere questa parte.Observable/Flowable, cosa succede in overflow con ciascuna strategia (Test06a–Test06d). Se si usa sample/throttle per gli update, saper dire cosa si perde.ReentrantLock, cosa cambia nelle versioni recenti di Java. Ripassare da Cap. 10 e TestVT.java.