Apleturi Java (III): Fire de executie

La Multi Ani si anul 2000 sa fie pentru toata lumea mai bun!

In episodul de luna aceasta vom studia ceva specific limbajului Java, si anume crearea si folosirea firelor de executie.
Apletul deseneaza niste cercuri rosii in miscare la fiecare click pe suprafata lui. Care este legatura lui cu firele de executie? Raspunsul este urmatorul: la fiecare click pe suprafata apletului se genereaza un nou fir de executie. Dar haideti sa aflam mai multe despre firele de executie:

Un fir de executie (thread) executa o serie de instructiuni. Un fir poate trai pe tot parcursul apletului sau doar cateva milisecunde. De asemenea un fir poate da nastere altui fir sau poate omori un fir existent.

Chiar si in apleturile din episoadele trecute exista unul sau mai multe fire de executie "ascunse", create de browser. Atunci cand are loc un eveniment de gen click de mouse, sau cand se apeleaza o functie ca repaint(), este executat un anumit fir ascuns. Firele ascunse sunt create si executate automat de limbaj si sunt transparente pentru programator. Daca rulati orice aplet in modul debugger al mediului de dezvoltare Java pe care il folositi, veti vedea cateva fire de executie create automat.
Exista si situatii in care programatorul are nevoie sa creeze el insusi propriile fire de executie. Trebuie stiut ca in timp ce un fir ruleaza in cadrul apletului, respectivul fir nu poate face altceva. Daca apletul tine un fir prea mult, poate bloca browserul. De aceea daca apletul are nevoie sa ruleze o metoda a sa pentru mai mult timp, este necesar ca un nou fir sa fie creat. Iata care sunt situatiile cele mai frecvent intalnite in care este nevoie de un nou fir de executie:

- Timp indelungat pentru initializare (functia init() a apletului). De exemplu, apletul are nevoie de ceva timp pentru a incarca o imagine. Un fir este folosit pentru ca sistemul sa poata primi alte evenimente.
- Task-uri repetitive sau care trebuie sa dureze o anumita perioada de timp. De exemplu pentru animatii. La fiecare cateva milisecunde este afisat un nou frame. Firul deseneaza frame-ul, sta o perioada de timp, apoi reia procesul.
- Evenimente asincrone. De exemplu un click de mouse. Daca utilizatorul apasa butonul mouse-ului, un nou fir este creat pentru a reda un nou frame dintr-o imagine.
- Mai multe task-uri. Firele sunt folosite pentru a face mai mult de un lucru o data. De exemplu un fir controleaza o animatie in timp ce altul face un calcul.

Pentru a crea si controla firele de executie se folosesc clasa Thread si interfata Runnable din pachetul java.lang.
Exista doua metode pentru a crea un fir:

1. Implementand interfata Runnable. In acest caz nu este nevoie sa creati o noua clasa. De exemplu daca vreti sa creati un fir chiar in cadrul apletului, faceti apletul sa implementeze aceasta interfata (amintiti-va ca o clasa nu poate mosteni mai multe clase, deci nu poate mosteni si clasa Applet, si clasa Thread).
2. Mostenind clasa Thread. O clasa nou creata mosteneste clasa Thread si firul va incepe sa ruleze conform functiei run() (veti vedea imediat in exemplu).

Nu este aproape nici o diferenta intre cele doua metode. In general metoda a doua se foloseste atunci cand doriti ca firul sa ruleze din clasa-aplet. Daca firul va rula in propria lui clasa, va fi mai convenabila prima metoda.

Dar pentru a intelege mai bine, sa vedem ce contin clasa Thread si interfata Runnable (pentru aceasta cititi si fisierele Thread.java si Runnable.java din pachetul java.lang), si pe urma voi prezenta exemple concrete.

Interfata Runnable are o singura metoda: public abstract void run(). Ea trebuie definita obligatoriu de clasa care implementeaza interfata Runnable. Functia run() va contine codul care va fi executat de firul de executie respectiv.
Clasa Thread implementeaza interfata Runnable si contine (printre altele) urmatoarele metode:
- mai multi constructori;
- start() - face ca firul de executie sa inceapa sa ruleze. In acest moment Java Virtual Machine va apela functia run();
- functia run() care suprascrie functia run() a interfetei Runnable; acesta functie trebuie suprascrisa cu codul ce se doreste a fi executat de firul respectiv;
- stop() - forteaza oprirea firului de executie.

Crearea si controlul unui fir de executie folosind prima metoda se face parcurgand urmatorii pasi:
1. se declara clasa-aplet ca implementand interfata Runnable;
2. se declara obiectul de tip Thread;
3. se scrie functia init() in care se construieste firul;
4. se scrie functia de pornire a firului;
5. se scrie functia de oprire a firului;
6. se suprascrie metoda run();
Exemplul urmator creeaza un fir pentru rularea unui contor care numara de la 1 la 100:


import java.awt.*;
import java.applet.*;

public class RunnableExample extends Applet
    implements Runnable { //pasul 1

  Thread thread; //pasul 2
  int count; //variabila contor
  String displayStr; //sirul care va fi afisat in functia paint
  Font font;

  public void init() { //pasul 3
    font = new Font("TimesRoman", Font.PLAIN, 72);
    setFont(font);
    count = 0;
    displayStr = "";
    thread = new Thread(this); //se creeaza firul apeland un constructor
  }

  public void start() { //pasul 4
    thread.start(); //se porneste firul
  }

  public void stop() { //pasul 5
    thread.stop();
  }

  public void run() { //pasul 6
    while (count < 100) { //executa firul atata vreme cat count<100
      ++count;
      displayStr = String.valueOf(count); //transforma variabila int in String
      repaint(); //redeseneaza
      try {
       
thread.sleep(100); //firul sta 100 milisecunde, dupa care reia corpul lui while
 
    }
 
    catch (InterruptedException e) {
 
    }
 
  }
 
}

 
public void paint(Graphics g) {
 
  g.drawString(displayStr, 50, 130);
 
}

}

(Nu va bateti capul cu secventa try - catch de mai sus, va voi explica mai pe larg intr-un episod in care voi trata exceptiile in Java.)
Nota. Nu este obligatoriu ca metoda stop() (pasul 5) sau linia thread.stop() sa existe, Java va opri automat firul atunci cand nu va mai exista nici o referinta catre el. Totusi este recomandat sa o scrieti. Apelul thread.start() este obligatoriu, fara el firul nu va porni,adica nu se va executa functia run(). Insa nu este obligatoriu ca el sa fie in functia public void start(). Putea sa fie in alta parte unde se stie ca se executa inaintea apelului celorlalte functii care apeleaza firul, de exemplu putea fi ultima linie a functiei public void init().
Daca ati urmarit cu atentie ce am explicat pana acum, acest exemplu ar trebui sa vi se para chiar usor. Pentru intrebari, scrieti la mirela.andronescu@er.dntis.ro.

Haideti acum sa vedem cum se face acelasi lucru folosind cea de a doua metoda. Vom parcurge urmatorii pasi:
1. cream clasa ThreadExample care mosteneste clasa Applet;
2. scriem functia init() care va crea un fir prin apelul constructorului clasei MyThread;
3. cream clasa MyThread care mosteneste clasa Thread;
4. declaram campurile clasei MyThread, printre care obligatoriu trebuie sa fie un obiect de tipul ThreadExample sau Applet;
5. cream constructorul clasei MyThread care are ca parametru o clasa-aplet;
6. in acest constructor apelam functia start() pentru a porni firul de executie;
7. suprascriem functia run().
Iata mai jos codul acestui exemplu:


import java.awt.*;
import java.applet.*;

public class ThreadExample extends Applet { //pasul 1
  String displayStr;
  Font font;

  public void init() {
    font = new Font("TimesRoman", Font.PLAIN, 72);
    setFont(font);
    displayStr = "";
    new MyThread(this);  //pasul 2  
  }

  public void paint(Graphics g) {
    g.drawString(displayStr, 50, 150);
  }

}

class MyThread extends Thread { //pasul 3
  ThreadExample applet; //pasul 4
  int count;
 
  MyThread(ThreadExample applet) { //pasul 5
    this.applet = applet;
    start();
//pasul 6
 
}

 
public void run() { //pasul 7
 
  count = 0;
 
  while (count < 100) {
 
    ++count;
 
    applet.displayStr = String.valueOf(count);
 
    applet.repaint();
 
    try {
 
      sleep(100);
 
    }
 
    catch (InterruptedException e) {
 
    }
 
  }
 
}

}


Nota1: Scrieti intreaga sursa de mai sus intr-un singur fisier. Puteti crea doua fisiere, cate unul pentru fiecare clasa, dar pentru aceasta trebuie sa mai adaugati in fisierul-aplet linia: import MyThread;, trebuie sa declarati clasa MyThread ca publica si trebuie sa va asigurati ca ambele fisiere sunt in directorul CLASSES.
Nota2: Se mai obisnuieste ca in clasa ThreadExample sa existe linia MyThread thread; (declararea unei variabile de tip MyThread). Astfel, apelul constructorului (pasul 2) va fi thread=new MyThread(this);. De asemenea metodele public void start() si public void stop() pot exista (la fel ca la prima metoda), dar nu este obligatoriu. Va recomand sa rulati totusi codul si intr-un fel si in altul, pentru a intelege mai bine functionarea firelor de executie in Java.

Este de remarcat urmatorul lucru: constructorul clasei MyThread are ca argument un obiect de tip ThreadExample, care este apletul de unde firul de executie va fi rulat. Acest lucru este necesar pentru ca firul sa poata comunica cu apletul.
Apoi, priviti functia run(). Clasa-fir acceseaza acum obiectul de tip aplet pentru a-i transmite noua valoare a sirului si pentru a apela functia repaint(). In prima versiune, firul era asociat cu clasa-aplet, altfel spus facea parte din ea, iar acum este un proces complet separat.

Acum aveti destule cunostinte despre firele de executie pentru a putea intelege sursa apletului pe care l-ati vazut ruland la inceputul articolului. Vom folosi a doua modalitate deoarece, asa cum va spuneam, la fiecare click se va genera un nou fir de executie, deci la un moment dat pot rula mai multe fire de executie, depinzand de cate click-uri au fost date in ultimul timp. Vom parcurge urmatorii pasi:
1. cream clasa OvalThreadExample care mosteneste clasa Applet;
2. declaram variabila Image offImage;
3. declaram variabila Graphics offGraphics;
Vom folosi aceste variabile pentru a evita licarirea ecranului in timpul animatiei. O animatie se realizeaza prin redesenarea cadrelor. Daca acest lucru se face direct pe ecran, se va produce o licarire a ecranului (puteti incerca!). Acest lucru poate fi inlaturat prin tehnica descrisa in urmatorii pasi:
4. se creeaza o imagine (careia i se mai spune buffer si care la noi este variabila offImage) care contine imaginea curenta a apletului (sau numai o parte a suprafetei sale).
5. se ia apoi contextul grafic al acestei imagini (in variabila offGraphics), care se adapteaza pentru fiecare cadru in parte;
6. se apeleaza in metoda paint() functia drawImage (care este din clasa Graphics la fel ca si drawLine, drawOval etc.) avand ca parametru imaginea offImage.
7. se scrie functia update(Graphics g). Aceasta functie este apelata automat la executia functiei repaint(). (repaint() apeleaza automat update si aceasta apeleaza paint. Pentru mai multe detalii cititi fisierul Component.java). Functia update va fi deci apelata cu noul context grafic, adica offGraphics, dupa ce acesta a fost modificat.
Pasii 3-7 sunt folositi deci pentru a inlatura licarirea imaginii.
8. se apeleaza functia public boolean mouseDown(Event e, int x, int y). Aceasta functie face urmatorul lucru: atunci cand se constata o apasare a butonului mouse-ului, se executa corpul acestei functii. x si y sunt coordonatele mouse-ului unde a avut loc evenimentul. Functia returneaza true daca s-a inregistrat evenimentul respectiv. Alte functii care reactioneaza la anumite evenimente mai sunt: mouseDrag, mouseEnter, mouseExit, mouseUp, cu aceiasi parametri. Ele sunt metode tot ale clasei Component. (Pentru exersarea acestor metode va propun urmatorul exercitiu: modificati apletul din episodul trecut: Triunghiul lui Sierpinski astfel incat cand utilizatorul da click undeva pe suprafata apletului, varful cel mai apropiat sa se deplaseze in acel punct. Astept sa-mi trimiteti prin mail raspunsul! :))
9. in corpul metodei de la pasul 8 se scrie deci codul ce se va executa atunci cand se da un click, si anume se creeaza un nou fir, deci se apeleaza constructorul clasei-fir;
10. se creeaza clasa DrawTarget care mosteneste clasa Thread si care are acelasi rol ca si clasa MyThread din exemplul anterior.
11. printre variabilele sale trebuie sa fie si una de tip Applet sau clasa-aplet;
12. se scrie constructorul clasei DrawTarget, care ca avea ca parametri clasa Applet, coordonatele mouse-ului unde s-a dat click si contextul grafic care urmeaza sa fie modificat;
13. in constructor se apeleaza si functia start() pentru pornirea firului de executie;
14. se scrie functia run();
15. in functia run() se seteaza culoarea de foreground pe alb pentru contexul offGraphics;
16. se apeleaza functia setXORMode, care seteaza modul de desenare al acestui context grafic sa alterneze intre culoarea setata la pasul 15 (alb) si culoarea setata aici (rosu). (vezi clasa Graphics);
17. in doua cicluri for realizeaza desenarea cercurilor in 8 pasi cand se maresc si tot 8 cand se micsoreaza;
18. se apeleaza functia repaint() a apletului;
19. se opreste executia firului pentru 200 de milisecunde.

import java.applet.Applet;
import java.awt.*;

public class OvalThreadExample extends Applet { //pasul 1
  Image offImage; //pasul 2
  Graphics offGraphics; //pasul 3

  public void init() {
    offImage=createImage(size().width,size().height); //pasul 4;
    offGraphics=offImage.getGraphics(); //pasul 5
  }

  public void paint(Graphics g) {
    g.drawImage(offImage,0,0,null); //pasul 6
  }

  public void update(Graphics g) { //pasul 7
    paint(g);
  }

  public boolean mouseDown(Event e, int x, int y) { //pasul 8
    new DrawTarget(this,x,y,offGraphics); //pasul 9
    return true;
  }

}

class DrawTarget extends Thread { //pasul 10
  int xPos,yPos; //pozitia unde se vor desena cercurile
  Applet applet; //pasul 11
  Graphics offGraphics;

  public DrawTarget(Applet a, int x, int y, Graphics g) { //pasul 12
    xPos=x;
    yPos=y;
    applet=a;
    offGraphics=g;
    start(); //pasul 13
  }

  public void run() { //pasul 14
    int i;
    int r;
    offGraphics.setColor(Color.white); //pasul 15
    offGraphics.setXORMode(Color.red); //pasul 16
    for(r=0,i=10;i>-20;i-=20) // i=(10,-10) //pasul 17
      for(r+=i;(r<90)&&(r>0);r+=i) { // r=(10,20...80,80,70...10)
        offGraphics.fillOval(xPos-r,yPos-r,2*r,2*r);
        applet.repaint(); //pasul 18
        try {
          sleep(200); //pasul 19
        }
        catch (InterruptedException e) {
        }
    }
  }

}

Ei, de data aceasta nu a mai fost asa usor, nu-i asa? (m-am gandit ca ar fi bine sa incepeti anul cu mult avant, ca sa o tineti tot asa pana la sfarsitul lui... glumesc :)))
Asupra
tehnicii de buffering voi reveni cand voi mai prezenta apleturi cu animatii, pentru ca vom folosi acelasi procedeu, iar asupra firelor de executie voi reveni luna viitoare, cand vom intra mai amanuntit in sincronizarea firelor de executie.
Pana atunci va urez spor la invatat si succese multe si astept sa-mi scrieti raspunsuri la probleme, intrebari, propuneri si nu in ultimul rand critici la adresa mirela.andronescu@er.dntis.ro.

1