MokaByte Numero 05 - Febbraio 1997

 
Step-to-Step nei
threads di Java
 
di
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{

            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);

        }
 

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:
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;

}
 

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ì:
 

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);

}
 
 
 

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.
Listato completo della classe Calc
 
 

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");

            }

    }

}
 

Vediamo 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.
 
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 rivista web su Java

MokaByte ricerca nuovi collaboratori
Chi volesse mettersi in contatto con noi può farlo scrivendo a mokainfo@mokabyte.it