MokaByte
Numero 05 - Febbraio 1997
|
|||
|
threads di Java |
||
Luca Bertoncello |
prima parte | ||
La scorsa puntata
si era parlato, in linea teorica, di come funziona un sistema distribuito,
ossia di come una rete di calcolatori può cooperare per svolgere
un lavoro. In pratica il problema si riduceva a dividere il carico di lavoro
su tutte le macchine della rete e di prendere i risultati alla fine. Spingendo
al massimo il ragionamento è pensabile che la macchina "centrale"
che riceve gli ordini dall'utente sia un baraccone che sa solo come dividere
il problema, darlo in pasto alle altre macchine, prendere i risultati da
esse, proporle all'operatore e prendersi onori e meriti. Bene, in questa
puntata faremo proprio qualcosa di simile, anche se l'applicazione è
quantomai semplice. Sviluppo di una calcolatrice. Prendiamo dunque
in esame il problema di una calcolatrice. Essa deve poter svolgere le operazioni
che le vengono richieste. In questo caso, per evitare di duplicare inutilmente
il codice la nostra calcolatrice può solo fare addizioni e sottrazioni,
ma è pensabile che si possa creare una routine che chiami più
volte l'addizione per fare la moltiplicazione, tutto questo però
ci porterebbe fuori strada. Noi si vuole solo vedere come è possibile
fare un programma che distribuisca i processi in rete. In questa situazione
abbiamo quindi 3 programmi:
Add,
ossia un programma che passa il suo tempo ad addizionare i due dati che
gli vengono passati.
Sub,
ossia l'analogo programma che sottrae il secondo dato dal primo.
La calcolatrice
vera e propria. Questa procedura è costituita da un minuscolo
parser (frutto di 5 minuti di lavoro al minimo delle potenzialità
di un programmatore) e da una procedura che manda i dati inseriti dall'operatore
alle due classi di cui sopra. Come si era spiegato in precedenza, il modo
più semplice per far colloquiare più applicazioni distribuite
su una rete è usare una o più porte Socket. In questo caso
abbiamo riservato la porta 15000 per la Add e la 15001
per la Sub. Controllate nella vostra rete se queste porte sono libere
e se non lo sono basta che cambiate questi numeri nei listati seguenti.
Vediamo quindi nel dettaglio la procedura principale della classe Add.
public static void Wait() throws Exception{In questa funzione, come si può vedere, vengono presi due dati dallo Stream in, definito in precedenza per prendere dati dalla porta 15000, si convertono le stringhe il long (usando la funzione atol, definita altrove), si sommano i dati e si scrive il risultato nello stream out, definito sempre per mandare i dati sulla porta 15000. Come si può notare il cuore della classe è banale. Attorno ad essa ci sono le funzioni che servono per inizializzare il socket e gli stream che potrete vedere in fondo all'articolo dove troverete i listati completi delle tre classi. Evitiamo di analizzare la classe Sub, in quanto, a parte l'operazione "-" al posto della "+" è identica alla Add e buttiamoci a capofitto nella classe che viene fisicamente utilizzata dall'operatore. Il cuore della nostra calcolatrice è il seguente:String op1,op2;
long a1,a2,ris;
op1=op2="";
op1=in.readLine();
op2=in.readLine();
a1=atol(op1);
a2=atol(op2);
ris=a1+a2;
out.println(ris);
}
public static void Calc() throws Exception{Questa funzione scrive a video le operazioni disponibili (Add, Sub, Quit) e accetta una stringa. A seconda che l'ordine inserito sia Add/Sub o Quit chiede gli operandi o esce dalla procedura. Una volta chiesti gli operandi chiama una funzione che passa questi dati alla classe appropriata tramite il socket definito. Vediamo per esempio il caso della Add. La procedura che addiziona è definita così:String Op,Op1,Op2;
DataInputStream In;
Op=Op1=Op2="";
In=new DataInputStream(System.in);
System.out.println("Calcolatrice MultiThread\n");
System.out.println("Inserire l'operazione da fare [Add/Sub/Quit]");
Op=In.readLine();
if(Op.compareTo("Quit")!=0){
System.out.println("Inserire il primo operando");
Op1=In.readLine();
System.out.println("Inserire il secondo operando");
Op2=In.readLine();
if(Op.compareTo("Add")==0)
Add(Op1,Op2);
else
if(Op.compareTo("Sub")==0)
Sub(Op1,Op2);
else
System.out.println("Operazione non valida");
}
else
Loop=false;
}
Qui i dati vengono semplicemente spediti attraverso lo stream definito e ci si pone in attesa della risposta, quindi si propone a video il risultato dell'operazione. Come si può vedere è abbastanza semplice. Ma che succederebbe se si facesse una serie di operazioni in contemporanea? Beh, alla televisione direbbe "Ci sentiamo alla prossima puntata", ma noi siamo buoni e vi diamo una piccola anticipazione. Semplicemente si manderebbero tutti i dati che sono disponibili alle varie procedure, si attenderebbero i primi risultati e si ripeterebbe il ciclo finchè non si giunge alla fine. Bene, il prossimo mese affronteremo appunto questo discorso. Con quello che abbiamo faremo una procedura per risolvere una certa espressione, eseguendo contemporaneamente alcune operazioni.public static void Add(String op1,String op2) throws Exception{
String ris;
ris="";
A_out.println(op1);
A_out.println(op2);
ris=A_in.readLine();
System.out.println(op1+"+"+op2+"="+ris);
}
Listato completo della classe CalcVediamo adesso di spiegare come funziona in soldoni una calcolatrice che si appoggia alla teoria citata nell'articolo, ovvero che delega le operazioni a microprogrammi sparsi in giro per il mondo.
import java.awt.*;
import java.io.*;
import java.net.*;
public class Calc{
public static DataInputStream A_in; // Input Stream per Add
public static PrintStream A_out; // Output Stream per Add
public static Socket A_client; // Socket di comunicazione per Add
public static DataInputStream S_in; // Input Stream per Sub
public static PrintStream S_out; // Output Stream per Sub
public static Socket S_client; // Socket di comunicazione per Sub
public static boolean Loop;
/* Questa procedura esegue l'addizione dei due operandi passati.
Per fare ciò manda i due dati alla funzione che esegue materialmente l'addizione
(classe Add) attraverso il canale Socket A_client e i suoi stream.
Infine legge il risultato che la classe Add pone sullo stesso Socket
*/
public static void Add(String op1,String op2) throws Exception{
String ris;
ris="";
A_out.println(op1);
A_out.println(op2);
ris=A_in.readLine();
System.out.println(op1+"+"+op2+"="+ris);
}
/* Analogamente alla funzione Add, questa procedura esegue la funzione Sub.
Passa i dati alla classe Sub tramite il canale Socket S_client e i suoi stream,
infine, come la Add, legge il risultato mandato dalla classe Sub.
*/
public static void Sub(String op1,String op2) throws Exception{
String ris;
ris="";
S_out.println(op1);
S_out.println(op2);
ris=S_in.readLine();
System.out.println(op1+"-"+op2+"="+ris);
}
/* Funzione che gestisce la calcolatrice.
La procedura è estremamente semplice: si limita a chiedere l'operazione (add, sub o quit)
e i parametri.
In base al tipo di operazione richiesto chiama la procedura corretta, oppure termina
il ciclo.
*/
public static void Calc() throws Exception{
String Op,Op1,Op2;
DataInputStream In;
Op=Op1=Op2="";
In=new DataInputStream(System.in);
System.out.println("Calcolatrice MultiThread\n");
System.out.println("Inserire l'operazione da fare [Add/Sub/Quit]");
Op=In.readLine();
if(Op.compareTo("Quit")!=0){
System.out.println("Inserire il primo operando");
Op1=In.readLine();
System.out.println("Inserire il secondo operando");
Op2=In.readLine();
if(Op.compareTo("Add")==0)
Add(Op1,Op2);
else
if(Op.compareTo("Sub")==0)
Sub(Op1,Op2);
else
System.out.println("Operazione non valida");
}
else
Loop=false;
}
/* A questa funzione vanno passato due parametri:
- Host dove si trova la Add
- Host dove si trova la Sub */
public static void main(String args[]){
try{
A_client = new Socket(args[0],15000);
S_client = new Socket(args[1],15001);
A_out = new PrintStream(A_client.getOutputStream());
A_in = new DataInputStream(A_client.getInputStream());
S_out = new PrintStream(S_client.getOutputStream());
S_in = new DataInputStream(S_client.getInputStream());
Loop=true;
while(Loop)
Calc();
}
catch (Exception e)
{
System.out.println("Calc: Errore di comunicazione");
}
}
}
>Listato completo della classe Add
import java.awt.*;
import java.io.*;
import java.net.*;
public class Add{
public static ServerSocket serv; // Socket di comunicazione
public static Socket channel; // Canale usato per comunicare
public static DataInputStream in; // Stream di Input dati
public static PrintStream out; // Stream di Output dati
public static long atol(String s){ // Converte una stringa in un long
Long tempInt;
long k;
if((s.charAt(0)>='0') && (s.charAt(0)<='9')){
tempInt = Long.valueOf(s);
k=tempInt.longValue();
}
else
k=0;
return(k);
}
/* Questa procedura è il cuore del sistema. Essa attende i dati dal socket aperto
e li somma, mandando poi il risultato al mittente tramite lo stesso socket
*/
public static void Wait() throws Exception{
String op1,op2;
long a1,a2,ris;
op1=op2="";
op1=in.readLine();
op2=in.readLine();
a1=atol(op1);
a2=atol(op2);
ris=a1+a2;
out.println(ris);
}
public static void main(String args[]){
try
{
// Apertura dei socket, definizione degli stream
serv=new ServerSocket(15000);
channel=serv.accept();
out = new PrintStream(channel.getOutputStream());
in = new DataInputStream(channel.getInputStream());
// Ciclo che attende i dati e li somma.
while(1==1)
Wait();
serv.close();
}
catch(Exception e)
{
System.out.println("Add: Errore di comunicazione");
}
}
}
Listato completo della classe Sub
import java.awt.*;
import java.io.*;
import java.net.*;
public class Sub{
public static ServerSocket serv; // Socket di comunicazione
public static Socket channel; // Canale usato per comunicare
public static DataInputStream in; // Stream di Input dati
public static PrintStream out; // Stream di Output dati
public static long atol(String s){ // Converte una stringa in un long
Long tempInt;
long k;
if((s.charAt(0)>='0') && (s.charAt(0)<='9')){
tempInt = Long.valueOf(s);
k=tempInt.longValue();
}
else
k=0;
return(k);
}
/* Questa procedura,analogamente alla sua omonina nella classe Add, attende i dati
dal socket aperto e li somma, mandando poi il risultato al mittente
*/
public static void Wait() throws Exception
{
String op1,op2;
long a1,a2,ris;
op1=op2="";
op1=in.readLine();
op2=in.readLine();
a1=atol(op1);
a2=atol(op2);
ris=a1-a2;
out.println(ris);
}
public static void main(String args[])
{
try
{
// Apertura dei socket, definizione degli stream
serv=new ServerSocket(15001);
channel=serv.accept();
out = new PrintStream(channel.getOutputStream());
in = new DataInputStream(channel.getInputStream());
// Ciclo che attende i dati e li somma.
while(1==1)
Wait();
serv.close();
}
catch(Exception e)
{
System.out.println("Sub: Errore di comunicazione");
}
}
}
Per
prima cosa prendiamo, come esempio, la seguente espressione:
(10+15)x(8-2) Questa semplice sequenza di operazioni può essere facilmente scomposta in più parti: |
Fig
1
|
(10+15)
(8-2)
Si veda la fig.1 per un chiarimento della cosa.
Queste due operazioni
vengono mandate ai due programmini che le svolgono contemporaneamente
e il risultato viene immagazzinato in due variabili che conterranno quindi
i valori di 25 e 6 rispettivamente.
A questo punto
si chiama il programma che esegue l'operazione di moltiplicazione passando
i risultati calcolati precedentemente ed esso, diligentemente, risponderà
150, che è il risultato al quale si puntava.
A questo punto
è d'obbligo una piccola riflessione:
C'è
un effettivo vantaggio in tutto ciò?
Certo che si!
Basta infatti fare due righe di conto. Supponiamo che per eseguire ogni
operazione (sia essa add, sub o mult) si impieghi un tempo x (questa supposizione
è falsa, però accettiamola per brevità di calcolo).
E' facile desumere
che calcolando l'espressione con i metodi classici otterremmo un tempo
di:
x (per la
add) + x (per la sub) + x (per la mult) = 3x Se invece usiamo questo
nuovo metodo abbiamo:
x (per la
add E la sub) + x (per la mult) = 2x
Chiaramente
in questo caso il vantaggio è pressochè nullo, ma pensate
cosa succede applicando questo metodo a operazioni più complesse
(non necessariamente aritmetiche). Per esempio pensiamo ad una centrale
elettrica: ci sono un numero imprecisato di computer che controllano le
varie macchine e riferiscono i loro dati a un calcolatore che fa i suoi
conti e decide determinate operazioni.
Le macchine
che prendono i dati possono essere molto semplici in quanto non hanno molto
da fare, ma con i costi oramai raggiunti è più conveniente
mettere macchine potenti piuttosto che non andare a cercare macchine vecchie
da mettere li, e quindi esse hanno a disposizione la possibilità
di svolgere anche altri compiti oltre al semplice monitoraggio: si mettono
a disposizione del calcolatore centrale per alleviargli il peso.
Questi riceve
i dati dalle varie macchine e, per fare i suoi calcoli lancia procedure
residenti in alcune parti della rete (di cui sono membri anche le macchine
che monitorano). I risultati gli tornano indietro con una velocità
molto maggiore e c'è subito un risparmio di tempo. Inoltre è
necessario tenere presente che la macchina che fa un calcolo può
a sua volta chiamare un altro programmino. Per esempio la procedure Mult
potrebbe fare il suo lavoro chiamando più volte la procedure Add.
|
||
|
||
MokaByte ricerca
nuovi collaboratori
|
||
|