Gyakorlati alapok III.
A többszálú programozás alapjai (multithread)
A szál (thread) és alapszintű működése
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.
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:
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:
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.