Connexions

You are here: Home » Content » Thread e I/O non bloccante
Content Actions

Thread e I/O non bloccante

Module by: Davide Rocchesso

Summary: A brief introduction to Java Threads as accessible from Processing.

Thread: definizione

Un thread (di controllo) è una lista di istruzioni eseguite sequenzialmente da un programma. I thread di un programma condividono uno stesso spazio di indirizzamento. Pur avendo stack e variabili locali separate, condividono le variabili globali. I thread sono "leggeri", nel senso che la creazione, distruzione e sincronizzazione sono relativamente economiche grazie alla condivisione dello spazio di indirizzamento. Le ragioni per organizzare un programma in un certo numero di thread possono essere molteplici:
  • Certi programmi si scrivono più semplicemente, in special modo le collezioni di compiti debolmente connessi (cioè largamente indipendenti).
  • I programmi interattivi risultano più efficienti laddove il servizio dell'input o il display dell'output sono organizzati in thread distinti.
  • I programmi sono potenzialmente parallelizzabili su architetture multi-processore o multi-core.
  • Il problema in esame richiede parti di programma in comunicazione asincrona tra loro.
  • E' utile imporre una struttura modulare al codice.
I/O non bloccante Una delle motivazioni forti per la programmazione concorrente mediante thread è l'ottenimento di servizi non bloccanti per Input/Output. Quando l'applicazione ha necessità di effettuare un I/O, è opportuno che non si blocchi, in modo da consentire che altre operazioni non dipendenti da quell'I/O possano essere effettuate. Una tecnica di gestione dell'I/O non bloccante, utilizzata ad esempio nei microcontrollori usati nelle board per il physical computing, è il polling, cioè la verifica ciclica dell'accadimento di eventi su un insieme di dispositivi di input. La ciclicità del polling è gestita da un timer. L'I/O non bloccante si può realizzare mediante i thread. La lettura di un certo dispositivo si può assegnare ad un certo thread il quale si blocca in attesa dei dati. Gli altri thread, che non dipendono dalla lettura del dato, possono però procedere in maniera concorrente.
Diagrammi di attivazione Il codice Processing seguente invoca il metodo stampa() sull'oggetto cl della classe Classe1.

class Classe1 {
 void stampa() {
   for (int i=0; i< 100; i++) println("yep");
 } 
}

void setup() {
 Classe1 cl = new Classe1();
 cl.stampa(); 
}

	
Il flusso di elaborazione si può rappresentare graficamente con il diagramma di attivazione di Figura 1, il quale presenta evidentemente un singolo thread.
atti1.png
Figura 1: Diagramma di attivazione a thread singolo
Se invece la classe viene realizzata come estensione della classe Thread, allora è possibile procedere all'attivazione di un thread secondario mediante invocazione del metodo start(). Il codice va riscritto come

class Classe2 extends Thread{
 void run() {
   for (int i=0; i< 100; i++) println("yep");
 } 
}

void setup() {
 Classe2 cl = new Classe2();
 cl.start(); 
}

	
Si noti che il metodo stampa() ora si chiama run(). Il metodo start() esiste nelle superclassi di Classe2 (nella classe Thread, e si occupa dell'invocazione del metodo run(). Questa volta il flusso di elaborazione, riportato in Figura 2 presenta due thread.
atti2.png
Figura 2: Diagramma di attivazione a doppio thread
Nota: Esiste un secondo modo di dichiarare e attivare un thread, mediante implementazione della interfaccia Runnable di Java. E' questa modalità che bisogna usare se si vuole attivare un nuovo thread su un oggetto di una classe che è già dichiarata come estensione di un'altra classe.

Thread: utilizzazione

Esempio 1: Thread temporizzati 
La classe Thread possiede un metodo sleep() che la mette in stato blocked per un certo numero di millisecondi. Ciò consente di programmare in maniera compatta e agevole flussi di eventi tra loro indipendenti. Ad esempio, il codice che segue produce il disegno di ellissi di due colori diversi, e ogni colore corrisponde ad un diverso intervallo di tempo (i.e., 1000 e 1300 millisecondi) tra due eventi successivi di disegno.

class TimerThread extends Thread{
  int timediff; // quanto temporale
  color col;  
  
  TimerThread(color c, int td) {
   timediff = td;
   col = c;
  }
  
 void run() {
   while(true) {
     fill(col);
     try {
       ellipse(int(random(100)), int(random(100)), 
         int(random(20)), int(random(20)));  
       sleep(timediff);
     } catch (Exception e) {println("Exception in sleep");}
   }
 } 
}

void setup() {
 TimerThread tt1 = new TimerThread(color(120,120,0),1000);
 TimerThread tt2 = new TimerThread(color(0,120,120),1300);
 tt1.start(); 
 tt2.start();
}

void draw() {
}


	
Si nota che il metodo sleep() di Java esige di essere invocato all'interno di un costrutto try catch(), cioè di una sezione di codice che consenta la cattura delle eccezioni. La gestione delle eccezioni consente di affrontare delle condizioni che alterano il normale flusso di esecuzione di un programma. Nell'esempio, la sleep() può fallire e sollevare (throw) una eccezione che, in questo caso, è gestita mediante la mera scrittura di una messaggio sulla console.
Problem 1
Si aggiunga al codice di Esempio 1 una classe che si occupa di ridipingere a intervalli regolari lo sfondo, in maniera da evitare la sovrapposizione delle ellissi.

I/O non bloccante basato su thread

Quando l'oggetto interattivo ha bisogno di leggere o scrivere da/su file, dispositivi, o network socket, è opportuno che essa non si blocchi completamente in attesa dei dati. Con i thread, ciò viene risolto elegantemente attivando un thread separato che gestisce I/O asincrono, e si blocca laddove è necessario.
Esempio 2: Lettura di comandi da tastiera 
Si supponga di dover disegnare in maniera automatica e ripetuta delle ellissi nella finestra grafica, e di voler controllare gli attributi di tali ellissi mediante parole chiave immesse da tastiera. Conviene separare il compito di lettura e interpretazione (parsing) del flusso di caratteri che viene dalla tastiera dal compito di produzione dell'output grafico. Ciò si può realizzare come segue, nel caso semplice in cui le parole accettate siano "rosso", "verde", e "blu" corrispondenti a diverse colorature delle ellissi prodotte.

StringBuffer stdin;
boolean linea;
color colore;

void keyReleased() {
  char c = key;
  if (c!='\n') {
    stdin.append(c);
  }
  else linea=true;
}

class ColorInput extends Thread {
  String results;
  char c;
 
  void run()  {
     while(true) {
       if (linea) {
            println(stdin);
            results=stdin.toString();
            stdin.setLength(0);	    
            linea = false;
            if (results.equals("rosso")) {
                colore = color(255, 0, 0);
            }
             if (results.equals("verde")) {
                colore = color(0, 255, 0);
            }
            if (results.equals("blu")) {
                colore = color(0, 0, 255);
            }          
          }
         try {
         sleep(5); // to relief the cpu from active waiting
         } catch (Exception e) {println("Exception in sleep");} 
        }
    }
  }

class TimerThread extends Thread{
  int timediff; // quanto temporale
  
  TimerThread(int td) {
   timediff = td;
  }
  
 void run() {
   while(true) {
     try {   
       fill(colore);
       ellipse(int(random(100)), int(random(100)), 
         int(random(20)), int(random(20)));  
       sleep(timediff);
     } catch (Exception e) {println("Exception in sleep");}
   }
 } 
}

void setup() {
 stdin = new StringBuffer();
 TimerThread tt1 = new TimerThread(100);
 ColorInput ci = new ColorInput();
 ci.start();
 tt1.start(); 
}

void draw() {
}


	  
Sono presenti, in questo caso, due diverse estensioni della classe Thread. La prima estensione fa una attesa attiva di linee di testo, impostando il colore ogniqualvolta viene rilevata una linea di testo contenente una delle tre parole chiave riconosciute. L'invocazione di sleep(5) rende questa attesa attiva meno onerosa per la CPU. L'altro thread, invece, si occupa di disegnare un'ellisse al secondo. In Processing è difficile realizzare un input bloccante da tastiera, in quanto non è accessibile direttamente lo stream System.in, sul quale in Java si può normalmente applicare lettura bufferizzata bloccante.
Nota: Per una introduzione all'I/O in Java si veda Java Input and Output.
Viceversa, Processing invita ad una programmazione event-based fornendo gli event handler keyReleased(), keyPressed(), e keyTyped(). E' possibile però fare un input bloccante di una linea da file di testo con un codice del tipo
try {
   BufferedReader stdiin = createReader("nomefile");
   println(stdiin.readLine());
 }catch(Exception e){}
	  
dove createReader() è una funzione di Processing che crea un BufferedReader object da un file o da una URL. Essa consente una leggera semplificazione rispetto al codice Java
try {
   FileReader is = new FileReader("nomefile");
   BufferedReader stdiin = new BufferedReader( is );
   println(stdiin.readLine());
 }catch(Exception e){}
	  
Per semplificare la lettura da file di testo, locali o remoti, Processing mette a disposizione la funzione loadStrings(), che carica tutte le linee di file di testo in un array di tipo String[]. Questo metodo può essere utile se il file non è troppo grande o dinamicamente variabile.
Problem 2
Il codice che segue effettua la lettura periodica di una linea di testo da un sito generatore di testo. Lo si estenda aggiungendo un thread che visualizza queste frasi sulla finestra grafica con animazione tipografica.

class sentenceReader extends Thread{
  int timediff; // quanto temporale
  
  sentenceReader(int td) {
   timediff = td;
  }
  
 void run() {
    while(true) {
      try {
       BufferedReader stdiin = createReader("http://www.essl.at/cgi-bin/swrap/cgis/lexikon-orakel.pl");
       for (int i=0; i<13; i++) stdiin.readLine();
       println(stdiin.readLine());
       sleep(timediff);
      }catch(Exception e){}
    }
  }
}

void setup() {
 sentenceReader st = new sentenceReader(2000);
 st.start();
}

void draw() {
}


	    

Sincronizzazione

Si supponga di dover estendere la lettura di comandi da tastiera con due thread che prendono comandi da stdin. I comandi possono essere consumati dall'uno o dall'altro dei thread, ma uno stesso comando non può essere consumato da entrambi. In questo caso si possono presentare problemi di race condition, cioè configurazioni di codice concorrente che danno luogo a risultati dipendenti dalla sequenza di scheduling attribuita ai vari thread. In particolare, la sezione critica
	    results=stdin.toString();            
            stdin.setLength(0);
            linea = false;
	
può dare luogo a comportamenti inconsistenti se interrotta dallo scheduler per passare il controllo dall'uno all'altro thread. Questi problemi si possono risolvere imponendo la non interrompibilità delle sezioni critiche di codice, per mezzo della parola chiave synchronized. Nella fattispecie, si può introdurre una classe con metodi sincronizzati:

class IO extends Thread {
 String results;

 synchronized  String acquire() {
   results=stdin.toString();
   linea = false;
   stdin.setLength(0);
   return(results);
 }
}

	
I metodi invocati su un oggetto di questa classe non sono interrompibili da metodi invocati sullo stesso oggetto. In ambito di transazioni bancarie, per esempio, un metodo prelievo() dovrà essere sincronizzato per evitare inconsistenze in presenza di prelievi multipli, come possono essere effettuati da co-titolari dello stesso conto. La sincronizzazione di thread è un argomento complesso, per l'approfondimento del quale esistono libri specializzati.
Problem 3
Si estenda la lettura di comandi da tastiera con due thread che prendono comandi da stdin.
[ Click for Solution 3 ]
Solution 3
Nel codice seguente si noti l'uso della getName() per stampare il nome del thread interessato.

StringBuffer stdin;
boolean linea;
color colore;
IO io = new IO();

void keyReleased() {
  char c = key;
  if (c!='\n') {
    stdin.append(c);
  }
  else linea=true;
}

class IO extends Thread {
 String results;

 synchronized  String acquire() {
   results=stdin.toString();
   linea = false;
   stdin.setLength(0);
   return(results);
 }
}

class ColorInput extends Thread {
  String results;
  char c;
 
  void ColorInput (IO inout) {
    io = inout;
  }
  void run()  {
     while(true) {
       if (linea) {
            results=io.acquire();   
            println(this.getName() + results);
            if (results.equals("rosso")) {
                colore = color(255, 0, 0);
            }
             if (results.equals("verde")) {
                colore = color(0, 255, 0);
            }
            if (results.equals("blu")) {
                colore = color(0, 0, 255);
            }          
          }
         try {
         sleep(2); // to relief the cpu from active waiting
         } catch (Exception e) {println("Exception in sleep");} 
        }
    }
  }

class TimerThread extends Thread{
  int timediff; // quanto temporale
  
  TimerThread(int td) {
   timediff = td;
  }
  
 void run() {
   while(true) {
     try {   
       fill(colore);
       ellipse(int(random(100)), int(random(100)), 
         int(random(20)), int(random(20)));  
       sleep(timediff);
     } catch (Exception e) {println("Exception in sleep");}
   }
 } 
}

void setup() {
 stdin = new StringBuffer();
 TimerThread tt1 = new TimerThread(100);
 ColorInput ci = new ColorInput();
 ColorInput ci2 = new ColorInput();
 ci.start(); ci2.start();
 tt1.start(); 
}

void draw() {
}


	  
[ Hide Solution 3 ]

Reference

Il capitolo 9 del libro Visualizing Data -- Ben Fry; O'Really 2007 fornisce esempi di acquisizione asincrona di dati da file e da siti remoti, con l'utilizzazione di thread e di sincronizzazione.

Comments, questions, feedback, criticisms?

Discussion forum

Send feedback