Summary: A brief introduction to Java Threads as accessible from Processing.
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:
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.
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();
}
![]() |
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();
}
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.
![]() |
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.
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() {
}
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.
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.
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.
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() {
}
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 dieci ellissi 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. 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){}
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){}
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.
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() {
}
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;
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);
}
}
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.
Si estenda la lettura
di comandi da tastiera con due thread che prendono
comandi da stdin.
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() {
}
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.