Dione
Z. KotalaP. Toman: Java
Predchozi (Obsah) Dalsi

16. Vlßkna (threads)

Java umo╛≥uje tzv. multithreading neboli paralelnφ b∞h dvou Φi vφce Φßstφ programu. TΘto vlastnosti vyu╛ije nap°φklad Internetov² prohlφ╛eΦ - m∙╛e nahrßvat obrßzky po sφti, zßrove≥ formßtovat a zobrazovat WWW strßnku a je╣t∞ k tomu spou╣t∞t applet - nebo textov² editor, kter² m∙╛e provßd∞t kontrolu pravopisu, zatφmco u╛ivatel pφ╣e dokument apod.

Ka╛dß paraleln∞ b∞╛φcφ Φßst programu se v Jav∞ naz²vß vlßkno (thread). D∙le╛itΘ je, ╛e jednotlivß vlßkna lze naprogramovat tΘm∞° nezßvisle a pouze v p°φpad∞, ╛e sdφlφ spoleΦnß data nebo pou╛φvajφ stejnΘ prost°edky (za°φzenφ), je t°eba zajistit jejich °ßdnou synchronizaci (viz 16.5.).

Na v∞t╣in∞ dne╣nφch poΦφtaΦ∙ s jednφm procesorem se samoz°ejm∞ nejednß o fyzicky souΦasn² b∞h vlßken, ale jednotlivß vlßkna se na procesoru st°φdajφ - podrobnosti viz 16.4..

16.1. T°φda Thread

Ka╛dΘ vlßkno v Jav∞ je instancφ t°φdy Thread (z balφku java.lang) nebo jejφho potomka. Tato t°φda definuje zßkladnφ metody jako je spu╣t∞nφ, zastavenφ a ukonΦenφ vlßkna.

JednoduchΘ vlßkno se naprogramuje tak, ╛e se vytvo°φ potomek t°φdy Thread, kter² definuje metodu:

   public void run()

obsahujφcφ k≤d, kter² bude provßd∞n paraleln∞. Spu╣t∞nφ vlßkna se provede zavolßnφm metody start() (metoda run() se p°φmo nevolß).

Priklad 16.1.
/* t°φda definujφcφ vlßkno */
class Vlakno extends Thread {                          // (1)
   Vlakno(String jmeno) {                              // (2)
      super(jmeno);                                    // (3)
   }

   public void run() {                                 // (4)
      for (int i=0; i<5; i++) {
         System.out.println("Vlakno: " + getName());   // (5)
         yield();                                      // (6)
      }
   }
}


/* hlavnφ program */
public class Vlakna {
   public static void main(String[] args) {
      Vlakno v1 = new Vlakno("v1");                    // (7)
      Vlakno v2 = new Vlakno("v2");                    // (8)
      v1.start();                                      // (9)
      v2.start();                                      // (10)
   }
}

Tento program vytvo°φ dv∞ vlßkna v1 (7) a v2 (8), instance t°φdy Vlakno (1), a spustφ je (9, 10). Tφm se zahßjφ paralelnφ provßd∞nφ metod run() (4) obou vlßken. Metoda run() simuluje smysluplnou Φinnost tφm, ╛e 5x vypφ╣e na v²stup jmΘno svΘho vlßkna (5) - bylo p°edßno jako parametr konstruktoru (7, 8). V²znam metody yield() (6) viz 16.4.. V²stup programu dopadne takto:

Vlakno: v1
Vlakno: v2
Vlakno: v1
Vlakno: v2
Vlakno: v1
Vlakno: v2
Vlakno: v1
Vlakno: v2
Vlakno: v1
Vlakno: v2

Nejd∙le╛it∞j╣φ metody definovanΘ ve t°φd∞ Thread jsou: (1)

  • Ve°ejnΘ konstruktory:
    Thread(), Thread(Runnable r), Thread(String s),
    Thread(String s, Runnable r),
    Thread(ThreadGroup g, Runnable r, String s),
    Thread(ThreadGroup g, Runnable r),
    Thread(ThreadGroup g, String s) - parametry:

    • r - reference na objekt implementujφcφ metodu run() (viz 16.2.),

    • s - °et∞zec reprezentujφcφ jmΘno vlßkna,

    • g - skupina, do kterΘ bude vlßkno za°azeno (viz 16.7.).

  • public static Thread currentThread() - vracφ referenci na prßv∞ b∞╛φcφ vlßkno,

  • public String getName() - vracφ jmΘno vlßkna,

  • public int getPriority() - vracφ prioritu vlßkna (viz 16.4.),

  • public void join() - Φekß na ukonΦenφ vlßkna,

  • public void resume() - probudφ vlßkno "odstavenΘ" metodou suspend(),

  • public void run() - jßdro vlßkna (def. jako prßzdnß metoda),

  • public void start() - zahßjφ b∞h vlßkna, tj. provßd∞nφ metody run(),

  • public static void sleep(long ms) - uspφ (doΦasn∞ zastavφ) vlßkno na ms milisekund. Pak jej probudφ a vlßkno m∙╛e pokraΦuje v b∞hu,

  • public setPriority(int priorita) - nastavφ prioritu vlßkna (viz 16.4.),

  • public void stop() - ukonΦφ vlßkno,

  • public void suspend() - "odstavφ" vlßkno. OdstavenΘ vlßkno m∙╛e probudit pouze metoda resume(),

  • public static void yield() - umo╛nφ b∞h jinΘho vlßkna (viz 16.4.).

16.2. Rozhranφ Runnable

Rozhranφ Runnable (z balφku java.lang) obsahuje pouze deklaraci metody:

   public void run();

T°φda pomocφ tohoto rozhranφ m∙╛e definovat jßdro vlßkna, metodu run(), p°iΦem╛ sama nemusφ b²t potomkem t°φdy Thread. To je u╛iteΦnΘ zejmΘna v p°φpad∞, ╛e:

  • program bude pot°ebovat pouze jedno dal╣φ vlßkno (krom∞ hlavnφho programu) - je zbyteΦnΘ vytvß°et kv∙li jednΘ instanci vlßkna t°φdu,

  • danß t°φda je potomkem n∞jakΘ t°φdy (p°φpad appletu) a pot°ebuje mφt vlastnosti vlßkna.

Priklad 16.2.
public class Animace extends java.applet.Applet
implements Runnable {
   Thread animator = null;
   int xpos = 0;

   public void init() {
      animator = new Thread(this);           // (1)
      animator.start();
   }

   public void run() {                       // (2)
      while(animator != null) {              // (3)
         repaint();                          // (4)
         try {
            Thread.sleep(80);                // (5)
         } catch(InterruptedException e) {}
      }
   }

   public void paint(java.awt.Graphics g) {  // (6)
      g.drawRect(xpos, 0, 20, 20);
      xpos = (xpos+1) % 100;
   }
}

Applet mß na pozadφ p°ehrßvat animaci. Vytvo°φ proto vlßkno (1) (viz str.16) konstruktorem Thread(Runnable r), kterΘmu p°edß odkaz na sebe - tj. na instanci implementujφcφ rozhranφ Runnable (2).

Metoda run() vykreslφ fßzi animace - volß metodu repaint() (4), poΦkß (uspφ se) na 80 ms (5), a cyklus se opakuje, Φφm╛ vznikß animace - vykreslovßnφ zaji╣╗uje metoda paint() (6) volanß prohlφ╛eΦem na ╛ßdost metody repaint() (4).

16.3. Ze ╛ivota vlßkna

Ka╛dΘ vlßkno se v danΘm okam╛iku nachßzφ v prßv∞ jednom z t∞chto stav∙:

  • NovΘ vlßkno (new thread) - je stav, kdy je vlßkno vytvo°eno, ale je╣t∞ nebylo spu╣t∞no (nejsou je╣t∞ alokovßny systΘmovΘ prost°edky vlßkna).

  • "B∞huschopn²" stav (runnable) - je stav po spu╣t∞nφ metodou start() . V tomto stavu se m∙╛e nachßzet vφce spu╣t∞n²ch vlßken, z nich╛ ale jen jedno je (na poΦφtaΦi s jednφm procesorem) prßv∞ b∞╛φcφ.

  • "Neb∞huschopn²" stav (not runnable) - do tohoto stavu se vlßkno dostane, pokud:

    • je uspßno metodou sleep(),

    • je "odstaveno" metodou suspend(),

    • Φekß v metod∞ wait() (viz 16.5.3.),

    • Φekß na vstupnφ/v²stupnφ za°φzenφ.

  • MrtvΘ vlßkno (dead thread) - je stav po ukonΦenφ metody run() nebo po zavolßnφ metody stop().

P°echody mezi jednotliv²mi stavy znßzor≥uje obrßzek:

vlakna.gif

16.4. Plßnovßnφ (scheduling)

Na poΦφtaΦi s jednφm procesorem, kter² nepodporuje paralelnφ b∞h instrukcφ, se multithreading simuluje tak, ╛e se vlßkna o procesor d∞lφ, podle pravidel, kterß urΦuje plßnovaΦ (scheduler).

Algoritm∙ pro plßnovßnφ je celß °ada. Java pou╛φvß plßnovßnφ podle priority: ka╛dΘ vlßkno mß p°id∞leno Φφslo, prioritu, v rozmezφ konstant MIN_PRIORITY a╛ MAX_PRIORITY (definovanΘ ve t°φd∞ Thread), kde vy╣╣φ Φφslo znamenß vy╣╣φ prioritu. Od priority se odvφjφ tato pravidla:

  1. B∞╛φcφ vlßkno musφ mφt nejvy╣╣φ prioritu.

  2. Pokud je v b∞huschopnΘm stavu vlßkno s vy╣╣φ prioritou ne╛ mß vlßkno b∞╛φcφ, je b∞╛φcφ vlßkno tφmto okam╛it∞ vyst°φdßno.

  3. Vlßkno s ni╛╣φ prioritou m∙╛e b∞╛φcφ vlßkno vyst°φdat jen tehdy, pokud je b∞╛φcφ vlßkno v neb∞huschopnΘm stavu nebo je ukonΦeno, a zßrove≥ na p°id∞lenφ procesoru neΦekß jinΘ vlßkno s vy╣╣φ prioritou.

  4. Pokud na p°id∞lenφ procesoru Φekß vφce vlßken se stejnou prioritou, je dal╣φ vybrßno tak, aby se postupn∞ vyst°φdala v╣echna.

  5. B∞╛φcφ vlßkno m∙╛e navφc dobrovoln∞ poskytnout procesor jin²m vlßkn∙m se stejnou prioritou zavolßnφm metody yield().

Z uveden²ch pravidel tedy vypl²vß, ╛e b∞╛φcφ vlßkno nem∙╛e b²t "donuceno" k vyst°φdßnφ jin²m vlßknem se stejnou nebo ni╛╣φ prioritou, ale pouze s prioritou vy╣╣φ.

Java je ov╣em v tomto ohledu benevolentnφ a dovoluje na operaΦnφch systΘmech, kterΘ to podporujφ (Unix, Windows95/NT), tzv. sdφlenφ Φasu (time slicing), kdy se vlßkna se stejnou prioritou st°φdajφ po pevn∞ p°id∞len²ch Φasov²ch intervalech. Tφm se zamezφ tomu, aby si nap°φklad vlßkno s maximßlnφ prioritou (MAX_PRIORITY) zabralo procesor "na v∞ΦnΘ Φasy".

Priklad 16.3.
Pokud z p°φkladu 16.1. odstranφme °ßdku (6) s metodou yield(), dopadne v²stup na r∙zn²ch operaΦnφch systΘmech takto:


OS s p°id∞lovßnφm Φasu:             OS bez p°id∞lovßnφm Φasu:

Vlakno: v2                  Vlakno: v1
Vlakno: v1                  Vlakno: v1
Vlakno: v2                  Vlakno: v1
Vlakno: v2                  Vlakno: v1
Vlakno: v2                  Vlakno: v1
Vlakno: v2                  Vlakno: v2
Vlakno: v1                  Vlakno: v2
Vlakno: v1                  Vlakno: v2
Vlakno: v1                  Vlakno: v2
Vlakno: v1                  Vlakno: v2

Metoda yield() toti╛ zaruΦuje, ╛e se vlßkna mohou spravedliv∞ st°φdat, a tak i v operaΦnφm systΘmu bez p°id∞lovßnφ Φasu vypadß v²stup, jak bylo uvedeno v p°φkladu 16.1..

Proto╛e sdφlenφ Φasu nenφ v Jav∞ zaruΦeno, nedoporuΦuje se psßt programy, kterΘ jsou na n∞m zßvislΘ.

16.5. Synchronizace

Pot°eba synchronizace vznikß v╣ude tam, kde je mo╛nΘ (av╣ak nep°φpustnΘ) pou╛φvat souΦasn∞ za°φzenφ (nap°. automat na kßvu) nebo sdφlet spoleΦnß data (nap°. skripta na anal²zu).

U automatu je nutnΘ synchronizovat p°φstup lidφ, nebo╗ se nesmφ stßt, aby po vhozenφ mince byl Φlov∞k odstrΦen jin²m "u╛ivatelem", kter² by pak obdr╛el k²╛en² nßpoj. Podobn∞ skripta nem∙╛e Φφst vφce student∙ souΦasn∞, jinak by p°i obracenφ dvou strßnek opaΦn²m sm∞rem do╣lo brzy k jejich destrukci.

16.5.1. KritickΘ sekce

V programovßnφ se Φßsti k≤du, jejich╛ paralelnφ b∞h m∙╛e vyvolat kolizi, naz²vajφ kritickΘ sekce. Provßd∞nφ kritick²ch sekcφ je Φasov∞ zßvislΘ a pokud nejsou synchronizovßny, program "n∞kdy" nepracuje sprßvn∞. Nejjednodu╣╣φm p°φpadem kritickΘ sekce v Jav∞ je metoda:

Priklad 16.4.
Souborov² server musφ synchronizovat p°φstup k soubor∙m na disku - dva u╛ivatelΘ nesmφ zapisovat souΦasn∞ do jednoho souboru a nep°φpustnΘ je takΘ zapisovat do souboru, kter² je prßv∞ Φten.

class Soubor {
   private byte[] disk = {0,0};             // (1)

   public void zapis(byte a, byte b) {      // (2)
      disk[0] = a;                          // (3)
      disk[1] = b;                          // (4)
   }

   public byte[] cti() {                    // (5)
      return new int[] {disk[0], disk[1]};  // (6)
   }
}

Instance t°φdy Soubor (nazveme ji soubor) bude p°edstavovat fyzick² soubor, jeho╛ dvoubytov² obsah (1) je mo╛nΘ Φφst (5) a zapisovat (2). K tomuto souboru budou p°istupovat dv∞ vlßkna A a B - bude-li do souboru vlßkno A zapisovat:

   soubor.zapis(1,2);

a souΦasn∞ vlßkno B z n∞j Φφst:

   byte[] data = soubor.cti();

m∙╛e se stßt, ╛e prom∞nnß data bude nakonec mφt obsah {1,0} namφsto oΦekßvanΘho {1,2}. K p°i°azenφ prom∞nnΘ disk toti╛ dojde v dob∞ "mezi" (3) a (4). Metody (2) a (5) jsou kritickΘ sekce.

Pozn.: KritickΘ sekce jsou v╛dy spojeny se spoleΦn²mi daty nebo za°φzenφm. Principißln∞ nic nebrßnφ souΦasnΘmu b∞hu dvou kritick²ch sekcφ, kterΘ nesdφlφ data a pou╛φvajφ rozdφlnß za°φzenφ.

16.5.2. Synchronizace kritick²ch sekcφ

VylouΦenφ souΦasnΘho b∞hu kritick²ch sekcφ lze provΘst nap°φklad pomocφ tzv. monitor∙. Monitor se obecn∞ sklßdß z dat a funkcφ (metod) jako zßmk∙ (locks) nad daty. V Jav∞ mß ka╛d² objekt vlastnφ jedineΦn² monitor.

P°i vstupu do synchronizovanΘ kritickΘ sekce vlßkno zφskß monitor (zßmek je uzamΦen) a po jejφm opu╣t∞nφ vlßkno monitor uvolnφ (zßmek je odemΦen). Po zφskßnφ monitoru jednφm vlßknem nem∙╛e jinΘ vlßkno zahßjit provßd∞nφ ╛ßdnΘ synchronizovanΘ kritickΘ sekce nßle╛φcφ k tΘmu╛ monitoru (vlßkno, kterΘ monitor vlastnφ, ano - monitory jsou reentrantnφ).

Synchronizovanou kritickou sekcφ v Jav∞ m∙╛e b²t blok (viz 16.5.4.) nebo metoda. Synchronizovanß metoda se v programu oznaΦφ modifikßtorem synchronized. Ka╛dß synchronizovanß sekce je v╛dy provedena jako ned∞liteln² (atomick²) celek, bez mo╛nosti p°eru╣enφ.

Priklad 16.5.
Metody (2) a (5) z p°φkladu 16.4. je pro sprßvnou funkci t°eba deklarovat takto:

synchronized public void zapis(byte a, byte b)
synchronized public byte[] cti()

Potom p°i libovolnΘ sekvenci zßpis∙ a Φtenφ nedojde k zapsßnφ ani naΦtenφ nekonzistentnφho obsahu souboru (prom∞nnΘ disk).

16.5.3. ┌loha producent - konzument

Komplikovan∞j╣φ p°φpad synchronizce nastßvß pokud dochßzφ mezi vlßkny k p°edßvßnφ dat. Krom∞ oznaΦenφ kritick²ch sekcφ pomocφ synchronized se pou╛φvajφ metody:

  • public final void wait() - zp∙sobφ zastavenφ vlßkna do probuzenφ metodou notify() nebo notifyAll() operujφcφ se stejn²m monitorem. D∙le╛itΘ je, b∞hem Φekßnφ v metod∞ wait() je monitor doΦasn∞ uvoln∞n a m∙╛e tudφ╛ b²t spu╣t∞na jinß kritickß sekce v tΘm╛e objektu - to umo╛≥uje zabrßnit zablokovßnφ vlßken (deadlock). (2) Dal╣φ verze tΘto metody umo╛≥ujφ specifikovat dobu Φekßnφ (timeout): public final void wait(long ms), public final void wait(long ms, int ns) (ms - poΦet milisekund, ns - poΦet nanosekund).

  • public final void notifyAll() - probudφ vlßkna, kterß Φekajφ v metod∞ wait() tΘho╛ objektu a tato pak "soupe°φ" o pokraΦovßnφ b∞hu (kritickΘ sekce) na zßklad∞ priority (viz 16.4.).

  • public final void notify() - funguje jako notifyAll(), ale probudφ jen jedno vlßkno (nenφ zaruΦeno, kterΘ).

Tyto metody jsou definovßny ve t°φd∞ Object (viz 11.5.) a mohou b²t volßny pouze v synchronizovan²ch kritick²ch sekcφch.

Priklad 16.6.
Pokud by soubor z p°φkladu 16.4. slou╛il jako (jednomφstnß) fronta, kam by vlßkna umis╗ovala ("produkovala") soubory urΦenΘ k tisku - metodou zapis() (2) - a tiskßrna si je postupn∞ vyzvedßvala ("konzumovala") - metodou cti() (5) - m∙╛e se stßt, ╛e soubor bude vyti╣t∞n vφckrßt nebo naopak nebude vyti╣t∞n v∙bec: nenφ toti╛ nijak zaruΦena sekvence: zßpis - vybrßnφ - zßpis - vybrßnφ, ale m∙╛e dojφt nap°φklad ke t°em zßpis∙m po sob∞ (p°iΦem╛ prvnφ dva soubory se samoz°ejm∞ ztratφ).

T°φdu Soubor je pro tento ·Φel t°eba upravit takto:

class Soubor {
   private byte[] disk = {0,0};
   private boolean volno = true;                      // (1)

   synchronized public void zapis(byte a, byte b) {   // (2)
      while (!volno) try {                            // (3)
         wait();                                      // (4)
      } catch(InterruptedException e) {}

      disk[0] = a;                                    // (5)
      disk[1] = b;                                    // (6)

      volno = false;                                  // (8)
      notifyAll();                                    // (9)
   }

   synchronized public byte[] cti() {                 // (10)
      while (!volno) try {
         wait();                                      // (11)
      } catch(InterruptedException e) {}

      volno = true;
      notifyAll();                                    // (12)

      return new int[] {disk[0], disk[1]};
   }
}

KlφΦovΘ jsou zde metody wait() (4,11) a notifyAll() (9,12) a p°φznak volno (1), kter² indikuje zda je fronta volnß (true) nebo ne (false).

Metoda zapis() (2) nejprve ov∞°φ, ╛e fronta je prßzdnß (3) a pokud ne, Φekß v metod∞ wait() (4). V opaΦnΘm p°φpad∞ zapφ╣e data do souboru (5,6), nastavφ p°φznak (8) a oznßmφ ostatnφm vlßkn∙m (tiskßrn∞) ukonΦenφ operace - metodou notifyAll() (9), kterß probudφ ostatnφ vlßkna Φekajφcφ v metod∞ wait() (4,11) na uvoln∞nφ monitoru.

Metoda cti() (10) funguje analogicky.

16.5.4. Blok synchronized

Tento blok umo╛≥uje oznaΦit jako kritickou sekci men╣φ Φßst ne╛ je metoda a urΦit, objekt podle jeho╛ monitoru se bude provßd∞t synchronizace. SychronizovanΘ metody toti╛ implicitn∞ pou╛φvajφ monitor instance, v jejφ╛ t°φd∞ jsou definovßny, co╛ n∞kdy nemusφ vyhovovat. Syntaxe bloku je:

   synchronized ( referenceNaObjekt ) {
      // kritickß sekce
   }

16.6. DΘmoni

DΘmon je vlßkno, kterΘ "pouze" poskytuje slu╛by ostatnφm vlßkn∙m. V²znam dΘmon∙ je ten, ╛e program, ve kterΘm b∞╛φ pouze vlßkna typu dΘmon, m∙╛e b²t ukonΦen (viz 5.3.).

Z hlediska programu je dΘmon vlßkno, kterΘ mß nastaveno atribut dΘmona metodou:

   public void setDaemon(boolean isDaemon)

s parametrem true. Obdobnß metoda:

   public boolean isDaemon()

vracφ true, je-li vlßkno dΘmon; jinak false.

16.7. Skupiny

Skupina (group) umo╛≥uje manipulaci s vφce vlßkny najednou (spu╣t∞nφ, ukonΦenφ atd.). Vlßkno m∙╛e b²t p°i inicializaci konstruktorem (viz 16.1.) za°azeno do vlastnφ skupiny - jinak se automaticky stßvß Φlenem skupiny implicitnφ (default group).

Skupiny jsou tvo°eny instancemi t°φdy java.lang.ThreadGroup, kterß definuje tyto zßkladnφ metody: (3)

  • Konstruktory skupiny: ThreadGroup(String jmΘno),
    ThreadGroup(String jmΘno, ThreadGroup rodSkupina) -
    parametry:

    • jmΘno - udßvß jmΘno skupiny,

    • rodSkupina - udßvß rodiΦovskou skupinu - skupiny mohou obsahovat dal╣φ (pod)skupiny,

  • public int activeCount() - vracφ poΦet vlßken ve skupin∞,

  • public String getName() - vracφ jmΘno skupiny,

  • public int getMaxPriority() - vracφ maximßlnφ prioritu, kterou m∙╛e mφt nov∞ p°idßvanΘ vlßkno,

  • public resume() - probudφ v╣echna vlßkna ve skupin∞ odstavenß metodou suspend(),

  • public void setName(String jmΘno) - nastavφ jmΘno skupiny,

  • public void setMaxPriority(int priorita) - nastavφ maximßlnφ prioritu, kterou m∙╛e mφt nov∞ p°idßvanΘ vlßkno,

  • public stop() - ukonΦφ v╣echna vlßkna ve skupin∞,

  • public suspend() - "odstavφ" v╣echna vlßkna ve skupin∞.

Vlßkno nelze po vytvo°enφ p°esunout do jinΘ skupiny, stejn∞ tak nelze p°esunout ani podskupinu. Je tomu tak z d∙vodu bezpeΦnosti, nap°φklad vlßkna ka╛dΘho appletu majφ p°id∞lenou svou zvlß╣tnφ skupinu a nem∙╛e se stßt, aby applet zφskal kontrolu nad cizφm vlßknem tφm, ╛e ho p°esune do svΘ skupiny.


  • (1) UvedenΘ deklarace kv∙li p°ehlednosti neobsahujφ modifikßtory final, native a v²jimky (throws).
  • (2) K zablokovßnφ dojde, kdy╛ dv∞ vlßkna na sebe vzßjemn∞ Φekajφ (nap°. a╛ jedno druhΘmu uvolnφ po╛adovan² prost°edek). Tato problematika je bohu╛el znaΦn∞ rozsßhlß a p°ekraΦuje rßmec sbornφku.
  • (3) UvedenΘ deklarace metod nejsou kv∙li p°ehlednosti ·plnΘ, neobsahujφ modifikßtory a nedeklarujφ v²jimky.

Predchozi
Converted by Selathco v0.9 on 25.09.1999 19:46
Dalsi