Multithreading
Az eddigi leírásokban ismétlôdôen találkozhattunk az OREXX azon tulajdonságaival, amelyek lehetôvé teszik azt, hogy olyan programokat készítsünk, amelyek egyszerûen szólva ,egyszerre több mindent csinálnak". Természetesen egy darab gépen egy darab processzorral ez elméletileg is lehetetlen (itt most ne említsük az OS/2 for SMP-t [Symmetric Multiprocessing], amely pont a többprocesszoros gépek ezen elônyét aknázza ki), de aki idáig eljutott, valószínûleg tapasztalhatta, hogy a látszólagosan egyszerre futó programok teljesen hétköznapiak lettek manapság, különösen olyan operációs rendszerek esetén, mint amilyen az OS/2. Aki mégsem tudná, annak röviden pár szót a multitaskingról (többfeladatosság) és a multithreadingrôl (többszálúság). A multitasking az, amikor több különbözô program fut (látszólag) egyszerre, és ezek vagy kommunikálnak egymással vagy nem. Igazából a dolog úgy mûködik, hogy az elsô programból lefut egy ,pici", majd a másodikból, majd az összes többibôl, majd ismét az elsô jön ott, ahol abbahagyta, és így tovább. (Erre példa lehet egy szövegszerkesztô és egy zenelejátszó program egymás mellett futása.) A multithreading (a ,thread" szó szálat jelent) ehhez nagyon hasonló módszer arra, hogy egy adott program egyszerre képes legyen többféle dolgot végrehajtani, mint például egy szövegszerkesztô szöveget formázni, nyomtatni és eközben kezelni azt is ha a felhasználó a program menüjében akar mászkálni. Az egymás mellett futó ,szálak" pici programok a nagy programon belül, melyek többnyire közös adatokon dolgoznak. Természetesen itt is vannak buktatók, amelyekkel szintén érdemes megismerkedni. Az egyszerre futó feladatok szinkronizálása nem egyszerû feladat, hiszen ezek többnyire aszinkron módon (vagyis egymástól nagyrészt függetlenül) futnak, és nem jósolható meg hogy amikor az egyiknek szüksége van a másikra az épp mit fog csinálni. Minden rendszer _ így az OS/2 is _ rendelkezik lehetôségekkel a szinkronizálásra, ilyenek a queue-k (sorok és vermek), semaphore-ok (magyarul szemafor, ami nem sokkal magyarabb, véleményem szerint) és néhány egyéb lehetôség. A lényeg, vagy inkább a fô veszély neve deadlock, amit én ,végzetes patthelyzetként" fordítanék (az irodalom idonként ,holtpont" néven emlegeti), és amelynek lényege, hogy valami nem kellôen átgondolt ok miatt az X program vár az Y eredményére, míg az Y eredményéhez szükséges az X kimenete. Igy mindegyik a másikra vár, nagyjából a világ vége plusz mérési hiba ideig... Ilyen összeakadásokat bizony néha nagyon nehéz kivédeni, hiszen nem mindig egyértelmû a dolog: lehet, hogy egy ,belsô mechanizmus" (mint a késôbbiekben említendô guarding) akad össze egy programrészünkkel. Nem kell elszontyolodni, ha kiderül egy ilyen hiba: magában az OS/2-ben is található néhány ilyen probléma, és valószínûleg minden többfeladatos rendszer rendelkezik ilyenekkel. Az OREXX is képes többszálú végrehajtásra, méghozzá _ valószínûleg ezt is említettem némi belülrôl fûtött szenvedély miatt is _ igencsak egyszerûen. Példákat láthattunk eddig is, de nézzük kissé közelebbrôl a lehetôségeinket!
A legegyszerûbb _ és így a legkönnyebben alkalmazható _ módszer neve early reply, amely ,korai válaszadást" jelent. Emlékezzünk a return utasításra, amely egy objektum visszaadott értékét adja meg, és többnyire az objektum programjának végét is jelzi, hiszen az érték visszaadásával a programrész befejezte feladatait. Vagy mégsem? Az early reply lényege pont ez: az objektum ugyan visszaad egy értéket az ôt hívónak, de ezután a futás két részre szakad: az egyik ágon a ,szokásos" módon a végrehajtás visszakerül a hívóhoz az érték visszaadásával, és a program fut tovább. Azonban a másik ágon az objektumunk renegát módon folytatja futását a return után, és még számos érdekes dolgot vihet véghez befejezôdéséig. Egyet nem tehet meg: többé nem adhat vissza értéket, legalábbis hagyományos módon nem. Ezen kívül azonban szinte bármit megtehet: hangokat adhat ki, szövegeket vagy grafikát szülhet a képernyôre, számolgathat a program változóival és hasonló nyalánkságok. Ezt egyszerûen úgy érhetjük el, hogy az elágazásba beteszünk egy reply utasítást, pont úgy, mintha oda egy return került volna, amely az objektum által visszaadott értéket adná meg. Rövid példa:
/* threadek */
ah = .foo~new /* egy peldany */
bee= .foo~new /* es egy masik */
say ah~hush("Bim!") /* fusson az elso */
say bee~hush("Bam!") /* a masodik IS */
do 5 /* ket obj + foprogram */
say "hmm..."
end /* ha vege, minden megall */
::class foo
::method hush
use arg blah
reply "Started" blah /* itt ketfele agazik */
do 5
say blah
end
A program igen egyszerû, mégis azonnal három szálon futnak az események (jobb mint egy kalandfilm! :-)), hisz fut a két objektum és a fôprogram. Egyszerû, nem?
Kissé más megközelítéssel dolgozik a start metódus. Ez az Object Class metódusa, tehát mindenki rendelkezhet vele. Mûködése kissé összetettebb, de ettôl egy egyszerûbb felhasználás esetén el is tekinthetünk. Mindenesetre amikor egy objektum a start üzenetet megkapja, akkor létrehoz egy névtelen Message objektumot, amelynek paraméterként átadja saját magát, mint célállomást, valamint a startnak megadott paramétereket, mint az üzenet paramétereit. A Message objektumokat már ismerjük, velük lehet ,aszinkron módon" (ami a többszálúság és párhuzamos végrehajtás alapja!) üzeneteket küldeni objektumoknak. Itt tehát az történik, hogy a Message objektum létrejön és ,elindul útjára" mialatt a programunk fut tovább, függetlenül attól hogy az üzenet mikor él célba, vagy épp hogy az objektum akinek szólt az üzenet mikor óhajt befejezôdni. De nézzük inkább az elôzô példát ,startosítva":
/* threadek starttal */
ah = .foo~new /* egy peldany */
bee= .foo~new /* es egy masik */
ah~start("hush","Bim!") /* fusson az elso, majd... */
bee~start("hush","Bam!") /* a masodik is valamikor */
do 5 /* ket obj + foprogram */
say "...hmm.."
end /* ha vege, minden megall */
::class foo
::method hush
use arg blah
say "Started" blah
do 5
say blah
end
Az eredmény nagyon hasonló, az eltérés itt abban van hogy itt maga az indítás aszinkron (tehát független szálon futó), míg a replyt használó példában pontosan látható a pont ahol elágazik az objektum futása. Látható, ez utóbbi módszer (a startos) közelebb áll a ,hagyományos" multitasking gondolkodásmódhoz. Tehát ilyen egyszerû lenne a dolog? Fusson a program százfelé, semmi gond se lesz vele? Azért a dolog némileg több gondolkodást igényel hosszabb távon. Vegyük például az alábbi példát:
/* guard 1 */ ah = .brave~new ah~start(cheeky) /* indul az egyik */ ah~start(spooky) /* es a masik */ do 6 /* es hogy lassuk hogy megy tovabb */ say "hmm..." end ::class brave ::method init /* obj. valtozo kezdoerteke */ expose foo foo = 1 ::method cheeky /* szamolgat */ expose foo do 4 foo=foo+1 say "Foo="foo end ::method spooky /* csak szol egyet */ expose foo foo=foo+668 say "Foo is" foo "!!!"
Amikor lefuttatjuk a fenti példát, valami furcsa történik! Miért nem indul el spooky? Látható, hogy szépen megvárja, míg cheeky befejezi a számolgatást, és csak utána indul el. De miért?
Az OREXX igyekszik minket megóvni a legtipikusabb hibáktól, amelyek többszálú programok esetén elôfordulhatnak, és ez a változók nemkívánatos egymástól függése. Vizsgáljuk csak meg, mi történne, ha spooky elindulna! Tegyük fel elôször is hogy a program nem csak zagyvaságokat írogat, hanem komoly számítási feladatokat végez változóin. Tegyük fel, hogy a program egyik szála éppen cheeky-t hajtja végre. Megnöveli foo értékét eggyel. Ekkor elindul spooky (mert odafönn nem szeret minket valaki) és megnöveli a foo-t 668-al! Cheeky mit sem sejtve továbblép, feltételezve hogy a foo eggyel nagyobb... ehelyett az valami egészen mást tartalmaz amit a program ott elvár, és ettôl a ponttól megjósolhatatlan hogy mik történhetnek...
Ettôl óv meg minket az OREXX úgynevezett guarding (ôrzés, ôrködés) módszere. Lényege, hogy egy thread futása blokkolt (vagyis nem indulhat el) addig, míg az általa használt nem lokális (helyi) változók közül egy is olyan, amit más thread használ, vagyis ne legyen olyan változó amin egyszerre többen is dolgoznak egymástól függetlenül. Ez a mechanizmus sok érdekes dologra használható, hiszen helyettünk ügyel arra, hogy az azonos változókat használó részek várják meg egymást, így egy jól megtervezett programban leveheti vállunkról a szinkronizálás nehéz problémáját (és mellesleg kizárja a változók ilyen használatából eredô deadlock-okat). Azonban sok esetben a programozók zokon veszik ha a gép túlságosan okos akar lenni. ,Héé, gép, bízd rám a dolgot, én majd gondoskodom róla hogy ne akadjon össze semmi!" _ mondhatja a programozó, és igaza van; végül is ô a fônök. Igy tehát _ mint az várható volt _ a guarding kikapcsolható. De az OREXX fejlesztôi látszólag nem szegényes fantáziájukról híresek, ugyanis ezt ismét nagyszerû csavarral oldották meg.
Elôször is az alapértelmezés szerint az ôrzôszolgálat aktív, gondatlan programozók önkínzásának megelôzése céljából. Ha valaki azonban teljesen ura a helyzetnek, az unguarded módosítót megadva egy metódus definíciójában az a metódus nem fog többet törôdni azzal, hogy ki használja az általa is felhasznált objektumokat. (Ugyanezt dinamikus módon megtehetjük a Method class setunguarded metódusával, de ritkán hozunk létre metódusokat dinamikusan, így ebbe most ne menjünk bele.) Van egy érdekes lehetôségünk a védelem rugalmas szabályozására is, és ez a GUARD ON utasítás. Számos formája létezik, ezekbôl pár példát nézzünk meg:
guard on /* innentôl él a védelem */ guard off /* ezután pedig szabad a vásár */ guard on when z<>0 /* ha z nem nulla akkor legyen védelem. Értelme lehet pl. ha tudjuk, hogy z=0 esetén nem fut le más objektumban sem olyan kód, amely a mi változóinkat zavarhatja */
Látható, hogy nagyszerûen megállapíthatja magáról egy programrész, hogy épp olyan-e az objektumban a politikai helyzet, hogy futni kell, vagy jobb megvárni, mi történik. Ha a feltétel miatt a futás megáll, akkor a kifejezés minden esetben újra kiértékelésre kerül, amikor a benne szereplô változók értéke megváltozik. Amint a kifejezés hamissá (illetve guard off when xxx esetén igazzá) válik a futás azonnal folytatódik.
Nagyjából ezen a ponton értük el a tudásnak azt a szintjét, ahol látható a Message class értéke, hiszen ez nagyszerû eszköz az egymástól némileg függetleníthetô részek indítgatásához, illetve ezek egymás közötti üzengetéséhez. Jelenlegi tudásunk alapján már nagyon sokféle feladatot meg tudunk oldani, legalábbis kétlem hogy a többszálú programok gyártásától bárkinek is kimelegedne az agya: tényleg nevetségesen egyszerû ezek létrehozása.
A következô részben olvashatunk a ,hagyományos" multitaskingot (több program fut egyszerre) segítô mechanizmusokról.
| Gervai Péter 1997. 07. 29. | [ Elôzô lecke | Következô lecke | Tartalom ] |