Laboratorio di reti I: Il multithreading Stefano Brocchi [email protected] 22 ottobre, 2008 Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 1 / 40 Il multithreading in Java Uso della concorrenza La concorrenza Spesso è conveniente realizzare programmi che vengano eseguiti concorrenzialmente Questo può essere conveniente per vari motivi: Allocare solo una piccola parte del programma alla gestione di una determinata risorsa o di un evento specifico (come nel caso di interfacce interattive) Distribuire efficientemente il carico di lavoro fra le risorse (come nel caso di multiprocessori o sistemi distribuiti) Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 2 / 40 Il multithreading in Java Uso della concorrenza I thread La concorrenza può essere realizzata sia tramite l’uso di molteplici processi o più thread (lightweight processes) Dato che i thread fanno parte di un unico processo che li contiene, il loro uso offre molti vantaggio rispetto alla gestione di più processi: Creazione, avvio e context switch molto più rapidi Occupazione di minori risorse (i thread condividono un’unica tabella di processo) Più semplice coordinazione e condivisione dei dati grazie allo spazio di indirizzi comune Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 3 / 40 Il multithreading in Java Il multithreading in Java I thread in Java Java fa uso del multithreading per la realizzazione della concorrenza Esistenza di primitive per la gestione della sincronizzazione Lo scheduling dei thread avviene tramite condivisione del tempo di CPU (round robin) a priorità Si ha l’impressione che tutti i thread vengano eseguiti contemporaneamente Un thread è rappresentato dalla classe Thread o dalle sue derivate Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 4 / 40 Il multithreading in Java Il multithreading in Java La classe Thread Un oggetto Thread si può creare tramite vari costruttori tra cui: public Thread() Costruttore vuoto public Thread(Runnable target) Costruttore che richiede un oggetto di tipo runnable; questa è un’interfaccia con il solo metodo public void run(). Vedremo come con questo costruttore si specifica che il codice da eseguire concorrentemente è nell’oggetto target Esistono costruttori che permettono di specificare un nome del thread. Se questo non viene specificato la JVM ne genererà uno automaticamente Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 5 / 40 Il multithreading in Java Il multithreading in Java I metodi start e run I metodi fondamentali per l’avvio concorrenziale di un thread sono public void start() e public void run() Il metodo start esegue il metodo run del thread concorrentemente al thread corrente; può essere eseguito al più una volta per ogni thread Il metodo run deve essere riscritto nelle classi che estendono Thread; il metodo run di Thread richiama il run dell’oggetto di tipo Runnable passato alla creazione, se disponibile Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 6 / 40 Il multithreading in Java Il multithreading in Java Realizzare il multithreading Come si possono quindi sfruttare le funzionalità della classe Thread per realizzare la concorrenza ? Due possibili metodi Estendere la classe Thread; riscrivere il metodo run specificando cosı̀ il codice da eseguire concorrentemente Implementare l’interfaccia runnable scrivendo un metodo run che specifichi il codice da eseguire concorrentemente; utilizzare un oggetto di tipo Thread per avviare l’esecuzione parallela Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 7 / 40 Il multithreading in Java Il multithreading in Java Realizzare il multithreading Vediamo un esempio di multithreading estendendo Thread public class myThread extends Thread { public void run() { // Altro codice (1) } public static void main(String[] args) { myThread mt = new myThread(); mt.start(); // Altro codice (2) } } Il codice nelle posizioni 1 e 2 verrà eseguito concorrentemente Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 8 / 40 Il multithreading in Java Il multithreading in Java Realizzare il multithreading Vediamo invece un esempio di multithreading senza estendere Thread public class myThread2 implements Runnable { private Thread t = new Thread(this); public void run() { // Altro codice (1) } public void start() { t.start(); } public static void main(String[] args) { myThread2 mt = new myThread2(); mt.start(); // Altro codice (2) } } Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 9 / 40 Il multithreading in Java Il multithreading in Java Realizzare il multithreading Non estendere Thread può essere necessario quando la classe ha già una classe padre Notare che nel precedente codice i metodi di Thread non possono essere utilizzati per il controllo del thread realizzato Soluzioni: riscrivere i metodi necessari ed inoltrarli all’oggetto thread utilizzato (nell’esempio t) oppure rendere possibile l’accesso a tale thread (es. con un metodo getThread) Solitamente estendendo Thread la realizzazione risulta un po’ più semplice Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 10 / 40 Il multithreading in Java Il multithreading in Java Metodi di Thread Altri metodi della classe Thread sono: public static void sleep(long millis) Mette il thread corrente in attesa per millis millisecondi; genera un’InterruptedException se nel frattempo il thread viene interrotto public void interrupt() Se il thread è in attesa (per esempio per causa di un’istruzione sleep), lo interrompe; altrimenti imposta nel thread un flag che segnali lo stato di interruzione public boolean isInterrupted() Ritorna true se lo stato del thread è interrotto Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 11 / 40 Il multithreading in Java Il multithreading in Java Metodi di Thread static void join() Mette il thread corrente in attesa per la terminazione del thread su cui viene chiamato il metodo; può generare una InterruptedException public boolean isAlive() Ritorna true se il thread è attualmente in esecuzione public void setPriority(int newpriority) Imposta la priorità del thread a newpriority (valori maggiori indicano maggiore priorità) public String getName() Ritorna il nome associato al thread Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 12 / 40 Il multithreading in Java Il multithreading in Java Esempio di thread Supponiamo di voler avviare due thread che eseguano delle operazioni in parallelo I due thread si dovranno arrestare quando l’utente preme un qualsiasi tasto della tastiera Realizziamo i thread estendendo la classe Thread public class myThread extends Thread Creiamo un metodo per imporre la terminazione ed un flag per tenerne conto: private boolean requestStop = false; public void arresta() { requestStop = true; } Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 13 / 40 Il multithreading in Java Il multithreading in Java Esempio di thread Nel metodo run implementiamo il codice che un singolo thread deve eseguire: public void run() { System.out.println(getName() + " attivato"); while (! requestStop) { // Operazioni da eseguire } System.out.println(getName() + " terminato"); } Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 14 / 40 Il multithreading in Java Il multithreading in Java Esempio di thread A questo punto il codice che li richiama dovrà avviare i due thread ed arrestarli dopo la ricezione di qualsiasi byte da standard input: public static void main(String[] args) throws IOException { myThread mt = new myThread(); myThread mt2 = new myThread(); mt.start(); mt2.start(); System.in.read(); mt.arresta(); mt2.arresta(); System.out.println("Thread principale terminato "); } Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 15 / 40 Il multithreading in Java Il multithreading in Java Esempio di thread A volte per imporre la terminazione di un thread è necessario inoltre richiamare una interrupt(): il thread potrebbe essere fermo su un’istruzione sleep() o wait() Il seguente Thread per esempio attende 10 secondi e stampa un messaggio, oppure segnala se viene interrotto prima: public void run() { try { Thread.sleep(10000); System.out.println(getName() + ": passati 10 s."); } catch (InterruptedException e) { System.out.println(getName() + ": interrotto"); } } public void arresta() { interrupt(); } Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 16 / 40 Il multithreading in Java Il multithreading in Java Thread demoni In Java è possibile definire dei thread demoni che terminino automaticamente quando tutti i thread non demoni hanno finito la loro esecuzione I thread demoni sono utili quando si ha necessità di un servizio sempre attivo che però non ha senso di continuare se gli altri thread terminano Per esempio un thread che cattura l’input (detto anche listener) di un’interfaccia grafica Un thread può essere dichiarato demone o meno tramite una chiamata al metodo setDaemon(boolean d) prima della partenza del thread. Il metodo isDaemon() serve per sapere se un thread è demone Thread avviati da thread demoni sono per default demoni Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 17 / 40 Il multithreading in Java Esercizio Esercizio Creare una classe che stia in ascolto di input da tastiera ed alla pressione dei tasti 1, 2 o 3 crei ed esegua un thread orologio Un thread orologio stampa su schermo un messaggio del tipo ’Thread-n: avviato da tot secondi’ ogni 1, 2 o 3 secondi a seconda del tasto premuto per il suo avvio Al momento della pressione del tasto ’k’ (kill) un thread orologio deve essere terminato immediatamente; deve essere stampato un messaggio contenente il suo tempo di esecuzione del tipo ’Thread-n terminato a tot ms dall’avvio’ Con la pressione del tasto ’q’ (quit) tutti i thread devono terminare dopo la stampa del messaggio temporale successivo Usare System.currentTimeMillis() per ottenere l’ora corrente Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 18 / 40 Il multithreading in Java Problematiche della concorrenza e sincronizzazione Problematiche della concorrenza L’ordine di esecuzione dei thread dipende da molteplici fattori non prevedibili al momento della programmazione Questo non determinismo può dar luogo a diverse problematiche in quanto l’esecuzione con dati in input uguali può dar luogo a risultati diversi E’ necessario sincronizzare l’accesso alle risorse condivise per evitare situazioni di incoerenza dei dati Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 19 / 40 Il multithreading in Java Problematiche della concorrenza e sincronizzazione Problematiche della concorrenza: esempio Consideriamo ad esempio un sistema di prenotazione di posti in un cinema Rappresentiamo il cinema con un oggetto Cinema contenente un vettore di booleani posti che segnala se un posto è occupato Consideriamo un metodo per la prenotazione prenota che specifica in un vettore i posti da prenotare Il metodo prenota può essere richiamato concorrentemente da più thread rappresentanti le diverse interfacce di prenotazione Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 20 / 40 Il multithreading in Java Problematiche della concorrenza e sincronizzazione Problematiche della concorrenza: esempio Consideriamo la seguente implementazione di prenota public void prenota(int[] p) { for (int i = 0; i < p.length; i++) { if (! posti[p[i]]) { posti[p[i]] = true; } else { // Errore ! } } } Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 21 / 40 Il multithreading in Java Problematiche della concorrenza e sincronizzazione Problematiche della concorrenza: esempio Consideriamo che i due thread Thread1 e Thread2 cerchino di prenotare il posto 100 inizialmente libero Un possibile interlacciamento delle istruzioni potrebbe essere: Thread1: if (! posti[100]) Thread2: if (! posti[100]) Thread2: posti[100] = true; Thread1: posti[100] = true; Cosı̀ facendo lo stesso posto rimarrebbe prenotato per due persone diverse ! Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 22 / 40 Il multithreading in Java Problematiche della concorrenza e sincronizzazione Sincronizzazione E’ necessario che alcuni metodi vengano eseguiti da un solo thread alla volta In Java questo si realizza con la parola chiave synchronized; un metodo può essere dichiarato synchronized aggiungendo questa parola chiave alla firma del metodo Di tutti i metodi dichiarati synchronized di un oggetto, soltanto uno alla volta può essere in esecuzione Nell’esempio il problema della condivisione di risorse può essere risolto dichiarando prenota come synchronized: public void synchronized prenota(int[] p) Se più metodi accedono a risorse condivise sarà necessario dichiararli tutti synchronized se si vuole garantire la mutua esclusione Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 23 / 40 Il multithreading in Java Problematiche della concorrenza e sincronizzazione La parola chiave synchronized In Java ogni oggetto possiede un campo lock che può essere assegnato ad un solo thread alla volta Quando un metodo synchronized entra in esecuzione il thread corrente prende possesso del lock dell’oggetto Un altro thread che cerca di eseguire un metodo sincronizzato sullo stesso oggetto viene fermato e rimane in attesa del lock Il lock viene rilasciato alla terminazione del metodo synchronized Metodi synchronized possono essere riscritti non synchronized e viceversa Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 24 / 40 Il multithreading in Java Problematiche della concorrenza e sincronizzazione La parola chiave synchronized Si possono sincronizzare, invece di interi metodi, anche solo dei blocchi di codice; la sincronizzazione può avvenire inoltre su di un qualsiasi oggetto public void metodo(int[] array) { synchronized(array) { // Esegui codice sincronizzato } // Esegui codice non sincronizzato } Visto che il codice sincronizzato può mettere in attesa un altro thread è bene sincronizzare solo le parti che effettivamente necessitano di mutua esclusione Lo svantaggio di usare sincronizzazione in metodi non synchronized è che questo non compare nella firma del metodo e deve essere quindi documentato in altro modo Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 25 / 40 Il multithreading in Java Problematiche della concorrenza e sincronizzazione Blocchi synchronized: esempio Riconsideriamo l’esempio della prenotazione del cinema; desideriamo realizzare un metodo prenota che prenoti dei posti tutti nella stessa fila, e che quindi richieda l’accesso esclusivo soltanto a quella parte della matrice Questo si può ottenere creando un oggetto per ogni fila del cinema e richiedendo il lock solo su quella fila Utilizziamo un vettore di oggetti per rappresentare le file. Possiamo utilizzare il tipo Object visto che l’unica cosa che ci interessa è l’uso del lock Consideriamo un metodo fila che dato un vettore di posti restituisca l’oggetto corrisponde alla loro fila (e eventualmente lanci un’eccezione se i posti sono su più file) Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 26 / 40 Il multithreading in Java Problematiche della concorrenza e sincronizzazione Blocchi synchronized: esempio L’implementazione potrebbe quindi essere la seguente public class Cinema { // Costruttore e campo posti omessi private Object[] file; private Object fila(int[] p) { ... } public void prenota(int[] p) { Object f = fila(p); synchronized(f) { for (int i = 0; i < p.length; i++) { if (! posti[p[i]]) { posti[p[i]] = true; } else { /* Errore ! */ } } } } } Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 27 / 40 Il multithreading in Java Problematiche della concorrenza e sincronizzazione Importanza della sincronizzazione Durante l’accesso a risorse condivise la sincronizzazione è importante anche per operazioni molto semplici, in quanto anche una singola istruzione ad alto livello dovrà essere scomposta in più istruzioni macchina per essere eseguita Consideriamo un metodo con un’unica istruzione per l’incremento di un contatore di una unità: count = count + 1; In linguaggio macchina questa istruzione verrà realizzata con una procedura come la seguente: 1. Carica count in un registro 2. Incrementa il registro 3. Salva il registro come count Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 28 / 40 Il multithreading in Java Problematiche della concorrenza e sincronizzazione Importanza della sincronizzazione Se il metodo viene eseguito concorrentemente da due thread si possono generare situazioni di errore Consideriamo la seguente possibile situazione di esecuzione, considerando due thread T1 e T2 ed un valore iniziale per count di 3 1. 2. 3. 4. 5. 6. T1: T2: T2: T2: T1: T1: Carica count in un registro (registro di T1 = 3) Carica count in un registro (registro di T2 = 3) Incrementa il registro (registro di T2 = 4) Salva il registro come count (count = 4) Incrementa il registro (registro di T1 = 4) Salva il registro come count (count = 4) Dopo due esecuzioni del metodo di incremento count è salito solo di un’unità ! Per questo la sincronizzazione è pressochè sempre necessaria per la modifica di risorse condivise Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 29 / 40 Il multithreading in Java Coordinazione tra processi: uso di wait e notify Altri tipi di sincronizzazione Tramite il codice sincronizzato su di un oggetto si può imporre ad un thread di stare in attesa se un altro ha un lock sullo stesso oggetto Spesso può essere necessario mettere in attesa un processo anche in base a condizioni più complesse Consideriamo il problema del produttore e consumatore con buffer limitato; se il buffer si riempie il produttore dovrà aspettare che almeno un oggetto venga consumato prima di produrne un altro In Java questo si può ottenere tramite i metodi di Object wait e notify Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 30 / 40 Il multithreading in Java Coordinazione tra processi: uso di wait e notify Il metodo wait Il metodo wait può essere richiamato solo su oggetti sul quale si è sincronizzati, altrimenti si ottiene una IllegalMonitorStateException synchronized(o) { o.wait(); // OK } o.wait(); // Generazione di un’eccezione L’istruzione wait su di un oggetto o esegue le seguenti operazioni: 1. Rilascia il lock di o 2. Mette in attesa il thread corrente finchè non viene eseguita una notify o notifyAll su o 3. Attende che il lock sia nuovamente disponibile 4. Occupa il lock su o e procede con l’esecuzione Esiste anche una versione temporizzata della wait che attende al più un dato tempo Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 31 / 40 Il multithreading in Java Coordinazione tra processi: uso di wait e notify Il metodo notify Come per il metodo wait il metodo notify può essere richiamato solo su oggetti sul quale si è sincronizzati Il metodo notify sblocca un processo in wait sull’oggetto in questione. Prima che il processo possa partire sarà anche necessario che venga rilasciato il lock E’ necessario accertarsi che per ogni thread che esegue una wait ce ne sarà un altro che eseguirà sicuramente una notify altrimenti il thread resterà in attesa indefinitamente Gestire con attenzione anche le situazioni di terminazione anormale come il lancio di un’eccezione Il metodo notifyAll risveglia tutti i thread in attesa; tuttavia solo uno potrà riottenere il lock mentre gli altri torneranno in wait. Rispetto a notify questo metodo è più sicuro ma meno efficiente Se non ci sono thread in attesa i metodi notify e notifyAll non producono alcun effetto Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 32 / 40 Il multithreading in Java Coordinazione tra processi: uso di wait e notify Esempio: produttore e consumatore Vediamo un possibile frammento di codice che implementa un produttore di un paradigma produttore/consumatore con buffer limitato: synchronized(buffer) { while (bufferFull()) { buffer.wait(); } produce(); buffer.notify(); } Notare che l’istruzione wait è dentro un’istruzione while in quanto non si può garantire che alla sua terminazione la condizione di avanzamento (! bufferFull()) sia verificata Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 33 / 40 Il multithreading in Java Coordinazione tra processi: uso di wait e notify Esempio: produttore e consumatore Del tutto simmetrico è il corrispondente frammento di codice nel consumatore synchronized(buffer) { while (bufferEmpty()) { buffer.wait(); } consume(); buffer.notify(); } Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 34 / 40 Il multithreading in Java Problematiche della concorrenza: deadlock I deadlock Può succedere che un gruppo di thread non possa procedere con l’esecuzione in quanto ognuno di essi è in attesa di una risorsa bloccata da un altro thread del gruppo Un esempio: Il thread 1 ha il lock su A ed è in attesa del lock di B Il thread 2 ha il lock su B ed è in attesa del lock di A In questi casi si parla di una situazione di deadlock Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 35 / 40 Il multithreading in Java Problematiche della concorrenza: deadlock I deadlock Perchè ci sia un deadlock si devono verificare tutte le seguenti condizioni 1. Almeno una risorsa deve essere accessibile da un solo thread alla volta 2. Almeno un thread può essere in attesa di una risorsa mentre ne ha allocata un’altra 3. Una risorsa non può essere prelazionata ad un processo 4. Deve avvenire un’attesa circolare tra un gruppo di thread Evitare che si verifichi anche una sola di queste condizioni vuol dire evitare i deadlock Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 36 / 40 Il multithreading in Java Problematiche della concorrenza: deadlock Evitare i deadlock Esistono varie strategie per evitare i deadlock Implementare dei thread che richiedano contemporaneamente tutte le risorse di cui necessitano per terminare; in questo modo un thread o può essere eseguito fino in fondo o non occupa risorse mentre è in attesa Realizzare una tecnica per permettere ad un thread (magari di priorità maggiore) di sottrarre risorse ad un altro Per evitare l’attesa circolare solitamente occorre una progettazione ad hoc a seconda del tipo di problema In genere evitare i deadlock può essere comunque un problema complesso Evitare i deadlock non garantisce l’avanzamento di tutti i thread: può succedere che un thread non avanzi perchè le risorse necessarie sono sempre possedute da altri. In questo caso si parla di starvation Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 37 / 40 Il multithreading in Java Esercizio (2) Esercizio (2) Realizzare una classe che, ricevendo in input da tastiera dei nomi di file di testo, ne esegua delle copie con tutti i caratteri maiuscoli (vedi esercizio sulle eccezioni) Mentre il programma è in attesa di input i file già inseriti dovranno essere processati Saranno necessari un thread per l’input da tastiera, uno che esegue la conversione ed un buffer condiviso a dimensione limitata che contiene i nomi dei file ancora da processare Se nell’input viene specificato il comando ’\quit’ il programma deve smettere di richiedere dati in ingresso, aspettare la terminazione dell’elaborazione sui file specificati e terminare Il programma deve stampare su schermo informazioni riguardanti la sua esecuzione come i file convertiti o eventuali situazioni di errore (es. file not found); questi non devono venir visualizzati mentre l’utente sta inserendo input ma tra un comando e l’altro Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 38 / 40 Il multithreading in Java Esercizio (2) Esercizio (2): testing Per simulare il nondeterminismo tipico della concorrenza che per programmi semplici come questo spesso non si percepisce, imporre delle attese temporali casuali nei metodi che procedono con l’elaborazione Eseguire dei test con particolari dati in input per testare i casi che potrebbero risultare in un errore (es. riempimento del buffer, file inesistenti, verificarsi di errori di IO o altre eccezioni) Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 39 / 40 Il multithreading in Java Esercizio (2) Esercizio (2): funzioni aggiuntive Progettare e pensare come potrebbero esser realizzate le seguenti funzionalità: Eseguire più thread contemporaneamente per l’elaborazione sui file Possibilità di specificare con il comando ’\file nomefile’ di leggere dei nomi di file da processare dal file specificato. La lettura da tale file deve nuovamente avvenire in parallelo alla lettura da tastiera Implementare un meccanismo per cui se viene eseguito il comando ’\stop’ da tastiera, il programma deve terminare immediatamente; i file parzialmente convertiti dovranno essere cancellati, ed un messaggio dovrà essere visualizzato per specificare quali file non sono stati processati Realizzare un altro thread che durante l’esecuzione del programma stampi a intervalli di tempo abbastanza regolari la situazione di avanzamento (es. Processando file.txt: 7263 / 14029 byte processati) sempre rispettando quanto richiesto sull’IO su schermo Stefano Brocchi Laboratorio di reti I: Il multithreading 22 ottobre, 2008 40 / 40