Gyakorlati alapok III.

A többszálú programozás alapjai (multithread)

 

A szál (thread) és alapszintű működése

 

www.informatika-programozas.hu - Ezt most meg kell tanulni!


A szál (thread) voltaképpen hasonló a programhoz: szintén önálló (de rögtön tegyük hozzá: a program által előzetesen lefoglalt) erőforrásokkal, külön végrehajtó veremmel (stack) és lépéssszámlálóval rendelkező programkomponens. Közöttük talán a legfontosabb különbség, hogy a szál önállóan nem futtatható. Ezért is nevezik sokan könnyűsúlyú folyamatnak (lightweight process).


Nyilvánvalóan 1 programon belüli szál magát a programot alkotja, ennek így tehát önmagában nincs sok értelme, bár később látni fogjuk, hogy valójában megvalósítható. A szálprogramozás nagy előnye, hogy képesek vagyunk belőle egyszerre többet is futtatni egyetlen programon belül. Ez a multithread, azaz többszálú programozás alapja. Ám mindenekelőtt azért mégis kezdjük egyetlenegy szállal, hogy lássuk azt a maga pőre, lecsupaszított működésében.


Szálak létrehozásához 2 módszer áll rendelkezésünkre: a Thread osztály vagy a Runnable interfész segítségével. Kezdjük a “klasszikussal”. A java.lang csomagban van 1 releváns osztály, neve: Thread. Ez lesz ősosztályunk, működtető osztályunkat belőle kell származtatnunk (extend Thread), hiszen ezen öröklődés segítségével tudunk hozzáférni a szál menedzseléséhez szükséges rutinokhoz. A Thread osztályt nem kell importálnunk, mert a java.lang csomag a Java-rendszeren belül natívan elérhető.


Az öröklődés biztosít nekünk egy run() metódust is. Ebbe a metódusba kell megfogalmaznunk az összes olyan kódot, amelyet a szál kódjának szánunk, másképpen: a szál konkrét programkódja az, ami ebbe a metódusban van megfogalmazva.


A szálnak -éppúgy mint a teljes programnak-, teljes és bejárt életciklusa van: keletkezik, él, dolgozik, működése megszakítható, végül elhal. Ezen tevékenységeket metódusokkal vagy külső, függőségi kapcsolatokkal tudjuk befolyásolni (időzítés, I/O-műveletek).


A szál a származtatott osztály példányosításának pillanatában, a start() metódus meghívásakor kezd el futni, mintegy “élni”.


Bevezetésképpen nézzünk meg 1 egyszerű szálat működés közben. Amint az fentebb már említésre került, az 1 szálas futtatásnak nincs sok értelme, de jó oktatási kiindulópont. A szál run() metódusában egy for ciklus hosszában írunk ki adatokat a konzolra, a metódus legutolsó rutinja pedig a “Szal 1 kesz!” üzenet kiírása.

 

www.informatika-programozas.hu - Futtatható Java-kód!

 

 

 

 

 

 

 


public class Main extends Thread {

public Main(String nev) {
    super(nev);
}

public void run() {
    for(int i = 1; i <= 10; i++) {
        System.out.println(i + ". ciklus - " + getName());
    }
    System.out.println(getName() + " kesz!");
}

public static void main (String[] args) {
    new Main("Szal 1").start();
    }
}

 

Végeredmény:
1. ciklus - Szal 1
2. ciklus - Szal 1
3. ciklus - Szal 1
4. ciklus - Szal 1
5. ciklus - Szal 1
6. ciklus - Szal 1
7. ciklus - Szal 1
8. ciklus - Szal 1
9. ciklus - Szal 1
10. ciklus - Szal 1
Szal 1 kesz!


A száltechnológia akkor kezd izgalmassá válni, amikor több szálat kezdünk futtatni. Ehhez nem kell sokmindent átszabnunk a kódban, csupán egy másik névvel még 1 szálat példányosítani (Szal 2). Ekkor a 2 szál nyilvánvalóan ugyanazt fogja csinálni:

 

www.informatika-programozas.hu - Futtatható Java-kód!

 

 

 

 

 

 

 


public class Main extends Thread {

public Main(String nev) {
    super(nev);
}

public void run() {
    for(int i = 1; i <= 10; i++) {
        System.out.println(i + ". ciklus - " + getName());
    }
    System.out.println(getName() + " kesz!");
}

public static void main (String[] args) {
    new Main("Szal 1").start();
    new Main("Szal 2").start();
    }
}

 

Végeredmény:
1. ciklus - Szal 1
1. ciklus - Szal 2
2. ciklus - Szal 1
2. ciklus - Szal 2
3. ciklus - Szal 1
3. ciklus - Szal 2
4. ciklus - Szal 1
4. ciklus - Szal 2
5. ciklus - Szal 1
5. ciklus - Szal 2
6. ciklus - Szal 1
6. ciklus - Szal 2
7. ciklus - Szal 1
8. ciklus - Szal 1
7. ciklus - Szal 2
9. ciklus - Szal 1
10. ciklus - Szal 1
Szal 1 kesz!
8. ciklus - Szal 2
9. ciklus - Szal 2
10. ciklus - Szal 2
Szal 2 kesz!


A kiírás sorrendjéből sajnos nem tudunk tágabb következtetést levonni, miszerint a 2 szál milyen valós ütemezés alapján futott le. Ennek anomáliájáról, nevezetesen a JVM és operációs rendszer eddig felderítetlen kapcsolatáról már írtunk a bevezető fejezetben. Talán ezt erősíti azon tény, miszerint további futtatások esetén mindig más, kaotikusnak tűnő kiírások történnek. A kaotikus kiírásnak nyilvánvalóan nincs köze a matematikai kvázivéletlenhez (amely például a Java-rendszeren belül néhány random-függvényben implementálva van), valószínűsítem a konzol kiírató rutinjainak lassú válaszidejeit (ezt már más esetben is tapasztaltam):


1. ciklus - Szal 1
1. ciklus - Szal 2
2. ciklus - Szal 1
2. ciklus - Szal 2
3. ciklus - Szal 1
3. ciklus - Szal 2
4. ciklus - Szal 1
4. ciklus - Szal 2
5. ciklus - Szal 1
5. ciklus - Szal 2
6. ciklus - Szal 1
6. ciklus - Szal 2
7. ciklus - Szal 1
7. ciklus - Szal 2
8. ciklus - Szal 1
8. ciklus - Szal 2
9. ciklus - Szal 1
9. ciklus - Szal 2
10. ciklus - Szal 1
10. ciklus - Szal 2
Szal 1 kesz!
Szal 2 kesz!


1. ciklus - Szal 1
1. ciklus - Szal 2
2. ciklus - Szal 1
2. ciklus - Szal 2
3. ciklus - Szal 1
3. ciklus - Szal 2
4. ciklus - Szal 1
4. ciklus - Szal 2
5. ciklus - Szal 2
5. ciklus - Szal 1
6. ciklus - Szal 2
6. ciklus - Szal 1
7. ciklus - Szal 2
7. ciklus - Szal 1
8. ciklus - Szal 2
8. ciklus - Szal 1
9. ciklus - Szal 2
9. ciklus - Szal 1
10. ciklus - Szal 2
10. ciklus - Szal 1
Szal 2 kesz!
Szal 1 kesz!


A többszálas technológia már beindult, ám ennek még mindig nincs sok értelme, mert a 2 szál ugyanazt csinálja. Nyilvánvaló, hogy a többszálas programozás előnye akkor válik behozhatatlanná, ha a (különböző) feladatok okosan kerülnek szétosztásra a szálak közt. Nézzük meg ennek elvi sémáját az alábbi kódban. Azért csak elvileg, mert a szálak még mindig ugyanazt a műveletet végzik el, de már külön osztályokban:

 

www.informatika-programozas.hu - Futtatható Java-kód!

 

 

 

 

 

 

 

 

Main.java


public class Main {
public static void main (String[] args) {
    new Szal1("Szal 1").start();
    new Szal2("Szal 2").start();
    }
}

 

Szal1.java


public class Szal1 extends Thread {

public Szal1(String nev) {
    super(nev);
}

public void run() {
    for(int i = 1; i <= 10; i++) {
        System.out.println(i + ". ciklus - " + getName());
    }
    System.out.println(getName() + " kesz!");
    }
}

 

Szal2.java


public class Szal2 extends Thread {

public Szal2(String nev) {
    super(nev);
}

public void run() {
    for(int i = 1; i <= 10; i++) {
        System.out.println(i + ". ciklus - " + getName());
    }
    System.out.println(getName() + " kesz!");
    }
}

 

Végeredmény:
1. ciklus - Szal 1
2. ciklus - Szal 1
3. ciklus - Szal 1
4. ciklus - Szal 1
5. ciklus - Szal 1
6. ciklus - Szal 1
7. ciklus - Szal 1
8. ciklus - Szal 1
9. ciklus - Szal 1
10. ciklus - Szal 1
Szal 1 kesz!
1. ciklus - Szal 2
2. ciklus - Szal 2
3. ciklus - Szal 2
4. ciklus - Szal 2
5. ciklus - Szal 2
6. ciklus - Szal 2
7. ciklus - Szal 2
8. ciklus - Szal 2
9. ciklus - Szal 2
10. ciklus - Szal 2
Szal 2 kesz!


A többszálas futtatás kódkörnyezete már készen áll, ennek ellenére néhány gondolat erejéig kanyarodjunk vissza egy egyszerű szál menedzseléséhez és nézzük meg néhány alapvető, beépített rutinját. Ezt a következő fejezetben fogjuk megtenni.