Asynkrona meddelanden och trådpooler

Det här avsnittet behandlar det mycket vanligt förekommande fallet att det asynkrona meddelandet består av en uppgift som ska utföras, till exempel ett Runnable-objekt vars run() ska exekveras. Vi ska titta på några olika sätt att skapa trådar som utför uppgiften meddelandet beskriver.

Litteratur

Lea: Concurrent Programming in Java, avsnitt 4.1

Allt i samma tråd (4.1.2)

Som exempel tar vi en webserver. Hur den fungerar i detalj saknar betydelse, det räcker att veta att den lyssnar efter anrop från webläsare och svarar genom att skicka tillbaks den efterfrågade HTML-filen. Om hela webservern kördes i en och samma tråd skulle den kunna se ut så här (i väldigt grova drag):
class WebServer { //Very incomplete.
    protected final RequestHandler handler = new RequestHandler();

    public void server() {
        for (;;) {
            Request r = waitForRequestFromBrowser();
            handler.handleRequest(r);
        }
    }

}
Webservern ligger i en evig loop där den väntar på anrop från webläsare. När det kommer ett anrop skickas det vidare till ett objekt av klassen RequestHandler som avkodar begäran från browsern, läser in den efterfrågade HTML-filen och skickar tillbaks den över nätet. Det asynkrona meddelandet är anropet handler.handleRequest(). Sändaren är ett objekt av klassen WebServer och mottagaren är objektet handler.

Ovanstående konstruktion av webservern är urusel eftersom den inte kan svara på ett anrop innan den är helt klar med föregående anrop. Svarstiden kommer att bli på tok för lång, en användare som surfar till en sajt vilken hanteras av ovanstående webserver kommer förmodligen att anta att sajten är nere eftersom det tar så lång tid att få svar. Dessutom kommer sannolikt webserverns inbuffert att bli full av alla väntande anrop, vilket leder till att nya anrop tappas. Detta är ett praktexempel på när det lönar sig att göra ett program flertrådat.

En tråd per meddelande (4.1.3)

Webserverns svarstid förkortas avsevärt om varje meddelande körs i en egen tråd. Det kan kodas så här:
class WebServer { //Very incomplete.

    public void server() {
        for (;;) {
            final Request r = waitForRequestFromBrowser();
            new Thread(new Runnable() {
                public void run() {
                    new RequestHandler().handleRequest(r);
                }
            }).start();
        }
    }

}
Nu kommer en ny tråd att skapas och startas på varje varv i for-loopen. Allt den nya tråden gör är att exekvera det asynkrona meddelandet handleRequest(). Varje gång ett meddelande skickas skapas alltså en ny tråd som kör meddelandet och sedan dör. Detta är en mycket bättre lösning än den föregående, men den är knappast den bästa. Problemet är dels att det tar ganska lång tid att skapa en ny tråd och starta den. Det innebär att svarstiden blir något längre än om tråden som skulle exekvera meddelandet redan fanns. Dessutom kan denna strategi leda till att för många trådar skapas om det kommer många anrop i snabb följd. Detta kan till exempel orsaka att webservern krashar på grund av att minnet tar slut.

Trådpool (4.1.4)

Den slutgiltiga lösningen (som faktiskt ofta används av bland annat webservrar) är att skapa ett antal trådar vilka lägger sig och väntar på ett meddelande, utför det när det kommer och sedan lägger sig och väntar på nästa. En sådan lösning beskrivs av bilden på sid 291 i kursboken och kan se ut så här:
class WebServer { //Very incomplete.
    protected final WorkerPool workers = new WorkerPool(10);

    public void server() {
        for (;;) {
            final Request r = waitForRequestFromBrowser();
            workers.execute(new Runnable() {
                public void run() {
                    new RequestHandler.handleRequest(r);
                }
            });
        }
    }

}

class WorkerPool {
    protected final Channel workQueue = new Channel();

    public void execute(Runnable r) {
        workQueue.put(r);
    }

    public WorkerPool(int nWorkers) {
        for(int i=0; i<nWorkers; i++) {
            activateWorker();
        }
    }

    protected void activateWorker() {
        Runnable runLoop = new Runnable() {
            public void run() {
                for (;;) {
                    Runnable r = (Runnable)workQueue.take();
                    r.run();
                }
            }
        };
        new Thread(runLoop).start();
    }

}
Klassen WorkPool innehåller ett antal trådar som utför de meddelanden som skickas till dem via metoden execute(). Klassen Channel kan vara en vanlig buffer i stil med den BufferArray vi tidigare använt.

Det finns ett flertal frågeställningar att fundera kring när en trådpool ska implementeras. Här är några av dem: