Synkrona meddelanden

Det här avsnittet handlar om implementation av synkrona meddelanden. Utgångspunkten är att en tråd har skickatt ett meddelande till en annan tråd, till exempel på något av de sätt som togs upp i avsnittet om asynkrona meddelanden. Frågan nu är hur vi ska få sändaren att vänta tills mottagaren blir klar med meddelandet och skickar tillbaks ett svar.

Litteratur

Lea: Concurrent Programming in Java, avsnitt 4.3

Completion Callback, Sändaren väntar inte men tar emot svar (4.3.1)

Denna lösning går ut på att sändaren skickar ett meddelande till någon annan tråd, varefter båda trådarna fortsätter exekvera helt oberoende av varandra. När meddelandet är färdigbehandlat anropar mottagaren någon metod hos sändaren för att visa att uppdraget är utfört och eventuellt returnera ett resultat. Som exempel tar vi en applikation som ska läsa in en fil. Eftersom det kan vara en tidsödande uppgift kan det vara lämpligt att utföra den i en separat tråd så att applikationen kan reagera på användarens åtgärder även medans inläsningen pågår. Då filinläsning är en fråga om IO och inte CPU-tid kan dessutom den totala exekveringstiden säkert minska till följd av att inläsningen sker i en egen tråd. Inläsningstråden kommer då att utnyttja någon IO-krets medans processorn kan hantera andra trådar. Först definierar vi sändaren (FileReaderClient) och mottagaren (FileReader):
interface FileReader {
    void read(String filename, FileReaderClient client);
}

interface FileReaderClient {
    void readCompleted(String filename, byte[] data);
    void readFailed(String filename, IOException ex);
}
En implementation av gränssnitten kommer nedan. Notera för det första att tråden som hanterar inläsningen skapas i "servern", dvs FileReader-objektet. Detta är den naturliga implementationen eftersom tråden som hanterar inläsningen då skapas av objektet som hanterar inläsningen. Notera även att om "callback" inte önskas kan det undvikas genom att "klienten" skickar null som FileReaderClient. Här kommer implementationen:
class FileReaderApp implements FileReaderClient { // Fragments
    protected FileReader reader = new AFileReader();

    public void readCompleted(String filename, byte[] data) {
        // ... use data ...
    }

    public void readFailed(String filename, IOException ex){
        // ... deal with failure ...
    }

    public void actionRequiringFile() {
        reader.read("AppFile", this);
    }

    public void actionNotRequiringFile() {
        ...
    }

}

class AFileReader implements FileReader {

    public void read(final String fn, final FileReaderClient c) {
        new Thread(new Runnable() {
            public void run() { doRead(fn, c); }
        }).start();
    }

    protected void doRead(String fn, FileReaderClient client) {
        byte[] buffer = new byte[1024]; // just for illustration

        try {
            FileInputStream s = new FileInputStream(fn);
            s.read(buffer);
            if (client != null) client.readCompleted(fn, buffer);
        }
        catch (IOException ex) {
            if (client != null) {
                client.readFailed(fn, ex);
            }
        }

    }

}
Ytterligare en intressant detalj är att metoderna readCompleted() och readFailed() i klientobjektet kommer att exekveras av servertråden, dvs inte av den tråd som anropade actionRequiringFile() och  därmed startade inläsningen. Detta kan leda till problem om klienttråden försöker använda filen innan den är färdiginläst. Kan detta ske duger det inte att, som detta styckes rubrik anger, klienten tar emot ett svar men struntar i att vänta på det. Problemet kan lösas genom att klienten anropar wait() innan den inlästa filen används och servern anropar notifyAll() i metoden readCompleted() (och readFailed()). Används denna lösning försvinner dock mycket av fördelarna som nämndes i början av detta stycke med att använda en separat tråd för inläsningen. Om någon form av signalering mellan trådarna ska användas kan det dessutom vara klokare att använda någon av nedanstående två metodiker eftersom de ger en renare design.

Join, Sändaren väntar men tar inte emot svar (4.3.2)

Ett alternativ till ovanstående lösning är att klienten/sändaren anropar join() på servern/mottagaren och sedan själv hämtar resultatet ur serverobjektet. Om så är fallet behövs inget interface för klienten eftersom servern aldrig kommer att anropa den. Servern behöver nu ingen referens till klienten, däremot måste en metod för att hämta filinnehållet läggas till:
interface FileReader {
    void read(String filename);
    byte[] getFile();
}
Implementationen skulle kunna se ut så här:
class FileReaderApp { // Fragments
    protected FileReader reader = new AFileReader();

    public void actionRequiringFile() {
        Thread t = new Thread(new Runnable() {
            public void run() {
                reader.read("Appfile");
            }
        });
        t.start();
        //... Do anything that does not require the file.
        try {
            t.join();
        }
        catch (InterruptedException e) {
            return;
        }
        byte[] buf = reader.getFile();
        if (buf != null) {
            //... Use the file.
        } else {
            //... The file could not be read. Error handling needed!!
        }
    }

    public void actionNotRequiringFile() {
        //...
    }

}

class AFileReader implements FileReader {
    byte[] buffer = null;

    public byte[] getFile() {
        return buffer;
    }

    public void read(final String fn) {
        byte[] buffer = new byte[1024];

        try {
            FileInputStream s = new FileInputStream(fn);
            s.read(buffer);
            this.buffer = buffer;
        }
        catch (IOException ex) {}

    }

}
Notera att sändaren måste skapa tråden som läser in filen, eller åtminstone känna till den, eftersom den ska kunna anropa join() på den. Denna lösning är knappast den bästa på det här problemet eftersom den tenderar att bli rörigare än att använda sig av callback. En lösning med join() kommer bäst till sin rätt om sändaren faktiskt inte har behov av ett svar utan det räcker med att veta att mottagaren har exekverat meddelandet till slut.

Future, Sändaren både väntar och tar emot svar (4.3.3)

En future är en enkel behållare för något objekt och kan betraktas som en skuldsedel på resultatet av någon uppgift (dvs på det objekt den kommer att innehålla). Vi börjar med att definiera två interface:
interface FileContent {
    byte[] getContent();
}
interface FileReader {
    FileContent read(String filename);
}
Tanken är nu att klienten ska anropa read() i en FileReader, vilken gör två saker: Dels drar den igång en tråd som kommer att läsa in filen och placera den i en FileContent. Dels returnerar den denna FileContent till klienten. När klienten sedan behöver filinnehållet anropar den getContent() i den FileContent den fick. Om filinnehållet ännu inte är klart kommer getContent() att anropa wait(). Klienten kommer då att sova tills inläsningstråden som startades av FileReader anropar setContent(), där notifyAll() anropas. Här finns ett sekvensdiagram som illustrerar detta (det blev för stort för att visa här). En implementation av interfacen och klienten kommer här:
public class FileReaderAppFuture { // Fragments
    protected FileReader reader = new AFileReader();

    public void actionRequiringFile() {
        FileContent content = reader.read("Appfile");
        //... Do anything that does not require the file.
        byte[] buf = content.getContent();
        if (buf != null) {
            //... Use the file.
        } else {
            //... Could not get file content. Error handling needed!!
        }
    }

    public void actionNotRequiringFile() {
        //...
    }

}

class AFileReader implements FileReader {

    private static class FutureContent implements FileContent {
        private byte[] content;
        private boolean ready = false;

        public synchronized byte[] getContent() {
            while (!ready) {
                try {
                    wait();
                }
                catch (InterruptedException e) {
                    return null;
                }
            }
            return content;
        }

        public synchronized void setContent(byte[] b) {
            content = b;
            ready = true;
            notifyAll();
        }

    } //End of inner class FutureContent.

    public FileContent read(final String fn) {
        final FutureContent fc = new FutureContent();
        new Thread(new Runnable() {
            public void run() {
                byte[] buffer = new byte[1024];

                try {
                    FileInputStream s = new FileInputStream(fn);
                    s.read(buffer);
                    fc.setContent(buffer);
                }
                catch (IOException ex) {
                    fc.setContent(null);
                }
            }
        }).start();

        return fc;
    }

}
Vår future är alltså klassen FutureContent. Klienten kan när som helst försöka hämta dess innehåll (dvs arrayen content) utan att bry sig om ifall det redan finns där. FurureContent kommer, när inläsningen är klar, att returnera antingen innehållet eller ett felmeddelande. I ovanstående enkla implementation är felmeddelandet bara att content är satt till null. En mer avancerad implementation skulle även kunna spara information om eventuella undantag som kastades då filen lästes in och sedan kasta dem igen då klienten anropar getContent().

En ständigt återkommande fråga är var inläsningstråden ska skapas. I programmet ovan skapas (och startas) den av FileReader.read(). Ett alternativ vore att read() i stället returnerade sin Runnable. Denna skulle då kunna exekveras i en trådpool i stil med den vi tittade på i föregående avsnitt.

Låt oss till sist konstatera att i ovanstående program kan klienten göra vad som helst från det den får en FutureContent returnerad tills den anropar dess getContent(). Vi har allstå inte implementerat dubbel synkron sändning. Det går dock lätt att göra till exempel genom att låta FileReader.read() anropa FutureContent.getContent() och sedan returnera innehållet till klienten.