A Smalltalk nagy újítása volt a grafikus felhasználói felület, ezért a képpontok gyors elérésére is speciális szintaxist kínál. Point osztályból származó objektumokat tehát az alábbi szintaxissal is létrehozhatunk:
100@200
A Smalltalk biztonságos nyelv, azaz például az indexhatár túllépése vagy a nullával való osztás hibaüzenetet eredményez, nem fagyást. A Smalltalk egyébként nem igazán kínál lehetőséget a kivételek kezelésére. Ennek ellenére sok ilyen jellegű probléma megoldható, ha alaposan ismerjük a rendszer logikáját és felépítését. Például az Object osztályban definiálták a doesNotUnderstand metódust, amely akkor hívódik meg, ha az adott objektumnak nem létező metódusát akarjuk meghívni. Ezt a metódust felüldefiniálhatjuk saját céljainknak megfelelően, és néhány helyzetben ez kivételszerű események kezelésére is használható.
Az eredeti Smalltalk-80-ban létezett egy error: nevű metódus, amelyet a programozó meghívhatott, paraméterül megadva a hiba szövegét. Ez a metódus egy ablakban megjelenítette a szöveget, majd lehetőséget adott a program futásának folytatására, vagy a debugger elindítására. A későbbi, kereskedelmi Smalltalk változatok már tartalmaztak kivételkezelést, és ez már része az ANSI Smalltalk szabványnak is. Érdekesség, hogy a kivételkezelés nem a nyelv része, hanem a nyelv reflexív eszközeinek felhasználásával, forrásnyelven megvalósítható.
A kivételkezelés két kódrészlet közötti kommunikáció: a jelző, ami felismeri a kivétel bekövetkeztét, és a kezelő, ami elemzi a helyzetet és eldönti, hogy milyen tevékenységet kell végezni. A következőkben láthatunk egy megoldásvázlatot arra, hogy a hogy ismerhetjük fel és jelezhetjük a kivételt:
Ez a példa jól mutatja, hogy hogyan választhatjuk el a hibakezelést a lényegi tevékenységtől. Az ifFalse után szereplő blokk soha sem fogja visszaadni a vezérlést, így az utasítást használó minden esetben egy érvényes fájlleírót kap visszatérési értékként.
Ezek után foglalkozzunk a kivétel kezelésével. A Smalltalkban minden vezérlési szerkezet a blokkokra épül. A blokkot objektumként továbbíthatjuk, így adódik a lehetőség, hogy a kivételkezelést ennek segítségével valósítsuk meg. Egy blokk végrehajtása alatt hozzárendelhetünk egy kezelőt a kivételhez, ami hiba esetén végrehajtódik. Hogy elkülönítsük a kód többi részétől, ezt is egy blokkban helyezzük el.
A fenti példában láthatjuk, hogy a valódi tevékenységet egy blokkban helyezzük el, amit védett blokknak is neveznek, és a blokknak üzenetként elküldjük a kezelendő kivételt az on után és a kezelő blokkot a do után megadva.
Szükséges, hogy névvel tudjunk hivatkozni az egyes kivételekre, valamint általánosabb vagy speciálisabb kivételeket adhassunk meg. Erre jó eszközt ad az öröklődés. Például a file not found és a permission denied egyaránt a file access error specializációja. A file access errorhoz rendelt kezelőnek reagálnia kell a nála speciálisabb kivételekre is. A kivételosztályok az Exception osztály leszármazottai. Ennek segítségével egy hibára az őt reprezentáló osztály segítségével hivatkozhatunk. Egy kivétel kiváltásához létre kell hoznunk egy kivétel példányt, majd a neki küldött signal üzenettel aktiváljuk a kivételkezelő keretrendszert, ami megkeresi és végrehajtja a megfelelő kezelő blokkot. Az, hogy a kivételeket objektumokkal valósítjuk meg, további lehetőségeket biztosít számunkra. A kivétel osztályát megvalósíthatjuk úgy, hogy további információkat tartalmazzon a kivétellel kapcsolatban.
Kiváltó:
Kezelő:
A példában láthatjuk az előre definiált FileNotFound kivétel használatát, valamint azt, hogy a fileName mező segítségével hogyan adhatunk át információt a kezelő blokk részére.
Minden kivétel osztály rendelkezik a következő szolgáltatásokkal: a messageText attribútum, amit a kiváltó a messageText: üzenettel beállíthat, a kiváltáshoz használhatjuk a signal üzenetet, valamint ha a messageText nincs specifikálva, akkor az információt a description üzenet biztosítja.
Amikor a kivételkezelő keretrendszert egy kivétel kiváltása elindítja, akkor ez megkeresi a megfelelő kezelő blokkot. A keresést a hívási lista alapján végzi. A hívási listában szerepelnek a válaszra váró blokkok. A keretrendszer az első megfelelő kezelő blokkot fogja meghívni, vagyis azt, ami a listában szereplő blokkok közül minél közelebbihez van hozzárendelve, valamint a kivétel megfelel az ott megadott kivételosztálynak (eleme, vagy leszármazottjának példánya). Ha nincs megfelelő kezelő blokk, akkor a keretrendszer a defaultAction üzenetet küldi a kivételpéldánynak. Ez az Exception osztályban úgy van implementálva, hogy elindítja a debuggert. Ez más nyelvekhez képes egy új lehetőséget nyújt a hiba kezelésére.
A
kezelő blokk keresése:
1.
Megkeresi az összes
olyan blokkot, amelyhez van kezelő blokk rendelve. Ezekből egy rendezett
kollekciót készít.
2.
Meghatározza a
blokkokhoz rendelt kivételosztályokat. Ha a blokkhoz rendelt kivételosztálynak
nem eleme az eldobott kivétel, akkor a kezelő blokkot eltávolítja a
kollekcióból.
3.
A maradék kollekció
tartalmazza a megfelelő kezelő blokkokat.
4.
Kiválasztja a kollekció első elemét. A kollekció
maradék részét is megtartja arra az esetre, ha a végrehajtott kezelő blokk
továbbadja (pass) a kivétel kezelésének jogát.
A hiba lekezelésére számos lehetőséget biztosít a nyelv. A következő példában egy vázlatot láthatunk a kivételkezelés szintakszisára. A továbbiakban végignézzük a kivételkezelés lehetőségeit. A Smalltalk más nyelvekhez képest viszonylag gazdag lehetőségeket biztosít a programozóknak.
Van olyan eset, amikor azt szeretnénk, hogy a védett blokk végrehajtása látszólag normálisan véget érjen a hibakezelő blokk befejezése után. Ez a lehetőség a megszokott más programozási nyelvek esetén. Ebben az esetben a kivételpéldánynak a return üzenetet kell küldenünk. A következő példában, ha a firstAction végrehajtása során kibocsátunk egy Error osztályba tartozó kivételt, akkor a kezelő blokk végrehajtása után a return hatására a secondAction soha nem hajtódik végre.
Minden üzenetnek van visszatérési értéke, így van ez a blokk esetén is. A fenti esetben a return hatására a védett blokk létszólag a nil objektummal tér vissza. Ez problémát jelenthet abban az estben, ha a védett blokk visszatérési értékére számítunk. Ennek kiküszöbölésére használhatjuk a return: változatot. Itt Megadhatunk egy tetszőleges objektumot, ami a védett blokk visszatérési értéke lesz.
Egy újabb lehetőséget biztosít a retryUsing: üzenet, aminek paraméterként megadhatunk egy blokkot. Ennek hatására a védett blokk végrehajtása megszakad, és a paraméterként megadott blokk hajtódik végre helyette.
Ebben az esetben felmerül a probléma, hogy esetleg a védett blokkal azonos blokkot kéne végrehajtanunk. Ebben az esetben használhatjuk a retry üzenetet. Ennek hatására a védett blokk végrehajtása félbeszakad, és újra kiértékelődik. A közben megváltozott értékek segítségével befolyásolhatjuk a védett blokk végrehajtását.
Ezekben az esetekben a kivételkezelő blokk természetesen ugyan az marad az újbóli végrehajtás esetén is. A pass üzenet hatására a kivétel továbbterjed. Ha nincs több alkalmas kezelő blokk, akkor a defaultAction eljárás hajtódik végre.
Szükségünk lehet arra, hogy a kezelő blokkban váltsunk ki kivételt. Erre lehetőségünk van a signal üzenettel, de ebben az esetben a kezelő blokktól terjed tovább. Ha azt akarjuk elérni, hogy a kivétel helyett egy másik kivétel váltódjon ki, akkor a kezelő blokkban használhatjuk a resignalAs: üzenetet. Ebben az esetben az elkapott kivétel lecseréljük a paraméterben megadottra, és a terjedése az eredeti kivétel kiváltásától indul.
Ebben a részben egy viszonylag szokatlan lehetőséget mutatunk be. A resume üzenet segítségével visszaadhatjuk a vezérlést a védett blokknak és folytathatjuk a végrehajtását, miután lekezeltük a kivételt. A következő példában két utasítást láthatunk. Az első utasítás hiba esetén egy InvalidOption kivételt vált ki. A második utasítás a védett blokkban “meghívja” az első utasítást. Ha kivétel váltódik ki, akkor a megadott kezelő blokkhoz kerül a vezérlés. A kezelő blokk egy dialógus ablakot jelenít meg. A Dialógus ablak eredményétől függően a vezérlés vagy a kivételt kiváltó utasítás után, vagy a védett blokk után fog folytatódni.
Ez a lehetőség nem használható minden kivétel esetében. Minden kivétel képes
fogadni az isResumable
üzenetet, amire az Error
kivételosztály alapértelmezés szerint hamisat ad vissza, ami azt jelenti, hogy ezen kivételosztály esetén nem lehetséges a folyatás. A resume: üzenetnek adhatunk egy paramétert,
ami a kiváltó üzenet visszatérési értéke lesz.
Végezetül bemutatjuk annak a lehetőségét, hogy hogyan rendelhetünk több kivételhez egy kivételblokkot. Erre szolgál az ExceptionSet osztály, amit az on:do: üzenet első paramétereként kell megadnunk. A kezelő blokk akkor hajtódik végre, ha az eldobott kivétel bármelyik a halmazban levő kivételosztálynak. Könnyen megadhatunk kivételhalmazokat, mivel mind az Exception, mind az ExceptionSet osztály képes fogadni a ’,’ bináris üzenetet, ami paraméterként egy Exceptiont vár és egy ExceptionSetet ad vissza.
Ilyen az 1976-ban kiadott Smalltalkban nincs definiálva. 1990-ben azonban készült a Smalltalk egy kiterjesztése, Parallel Smalltalk néven, ami már támogatja a párhuzamos folyamatok programozását. Az általunk használt Smalltalk változatban a blokkokra (Context) meghívhatjuk a fork metódust, ami egy processz elindítását kezdeményezi. Létezik ezen kívül a Process, a ProcessorScheduler, Semaphore, Mutex osztály is, amelyek szintén a párhuzamossághoz kapcsolódnak.
Kezdetnek csak egy rövid példával illusztrálva nézzük meg milyen lényegi szolgáltatásokat is nyújt a Smalltalk párhuzamossága!
Ha egy blokknak elküldjük a #fork üzenetet, akkor létrejön egy új Process típusú objektum, amely az eddigi programunkkal párhuzamosan kezdi elvégezni a blokkban megadott műveleteket.
stepProcess := [ [Processor sleep: 250. ball step] repeat ] fork
Ezek után a stepProcess változó az új folyamatunkat fogja tartalmazni, amely egy végtelen ciklust ír le: minden iterációban vár egy kicsit, majd a ball objektumnak küldi a step üzenetet.
Megadhatjuk a készítendő processzus prioritását is, ha nem nem a #fork üzenettel hozzuk létre, hanem a #forkAt:-tel, amelynek argumentuma (egy egész szám) a prioritásértéket tartalmazza.
Ezeket és az egyéb ide kapcsolódó elemeket fogjuk a következő pontokban megvizsgálni.
A Process osztály példányai egy-egy futtatási szálat reprezentálnak. Ezek könnyűsúlyú processzek, hiszen közös címtéren (objektummemórián) osztoznak. Minden Process példánya olyan utasítássorozatot reprezentál, melyet a virtuális gép más processzekkel konkurens módon hajthat végre. Egy Process a következő állapotokban lehet:
-
active A Process éppen fut. Általában csak a fő UI process van ilyen állapotban.
-
debug A Process jelenleg debuggolás alatt
van.
-
ready A Process futásra kész, de jelenleg egy processzor
prioritási sorában van.
-
suspended A Process felfüggesztés alatt áll (#suspend),
és nem fog futni, amíg nem küldjük neki a #resume üzenetet.
Jegyezzük meg, hogy az olyan felfüggesztett processzeket, amelyekre nem mutat referencia, a szemétgyűjtő
felszabadít.
- waiting A Process egy <Semaphore>-ra vár.
- dead A Process Terminált vagy befejeződött.
A processzek állapotváltásait befolyásoló 5 lényeges üzenet a következő. suspend, terminate, resume, wait és signal. Az első három címzettje egy Process, az utolsó kettőt pedig egy Semaphore kapja.
A processzek állapotait, a közöttük lehetséges átmeneteket valamint az azokat kiváltó üzeneteket mutatja a következő ábra.
A Smalltalk szálak nem valódi szálak. Valójában a szálakat egy operációs rendszeri szál valósítja meg. A párhuzamos szálakat a virtuális gép szimulálja. Minden szál ugyanazon image felett fut. E miatt nagy veszélyt jelentenek az olyan Processek, amelyek hosszú ideig futnak, mert ezek blokkolják a többi Process végrehajtását.
A blokknak küldhetünk fork vagy forkAt: üzeneteket.
A fork hatására létrejön egy új Process ugyan azzal a prioritással, mint a jelenlegi Process. Az új Process bekerül a várakozási sorba, és az állapota ready lesz. Akkor fog elindulni a futtatása, ha rákerül a sor.
A forkAt: hatására létrejön egy új Process a paraméterként megadott prioritással. Ha a prioritása magasabb a jelenleg futó Processnél, akkor azonnal elindításra kerül. Ha a prioritás alacsonyabb, akkor csak minden magasabb prioritású Process befejezése után indul el. Ha azonos a prioritás, akkor a jelenleg futó Processt nem szakítja félbe.
Nézzük most ezeket a lehetőségeket egy kicsit részletesebben!
Már láttuk, hogy blokkok segítségével valósíthatjuk meg vezérlési szerkezeteink széles skáláját. A Smalltalkban processzeink létrehozásában is alapvető szerepet játszanak a blokkok. A legegyszerűbb módon egy blokknak küldött fork üzenettel hozhatjuk létre a Process osztály példányait. Nézzünk erre egy példát!
A fenti utasítás létrehozza a Process osztály egy példányát, mely megjeleníti a 100 faktoriális kiszámításának eredményét a Transcript ablakban.
Az újonnan létrehozott folyamat ezután a várakozó processzek listájába kerül. A folyamat futáskész állapotban jön létre, és végrehajtása azonnal elkezdődik, amint az aktuális processz (azaz az ablakkezelő irányítója) lemond a processzorról, mint erőforrásról.
Lehetőségünk van egy processz létrehozására annak végrehajtási listába történő beillessztése nélkül is. Ehhez a newProcess üzenetet kell használnunk a következő módon:
Egy Process átmenetileg leállítható a suspend üzenettel. Egy ily módon felfüggesztett folyamat ezután újraindítható a resume üzenettel. Ha egy processzre többé már nincs szükségünk, a terminate üzenettel állíthatjuk le. Ha egy Process megkapta a terminate üzenetet, többé nem indítható újra.
Lehetséges továbbá egy Process létrehozása és kiértékelése tetszőleges számú argumentummal. Erre látunk példát a következőben:
A fenti példa létrehoz egy Process példányt, mely futás közben a 1048576 (2^20) számot jeleníti meg a Transcript ablakban.
A ProcessorScheduler osztály egyetlen példánya a Processor. A Processor vezérli a Processeket, és időzíti az interpretert szigorú prioritási sorrendet és Round-Robin algoritmust használva. A Processor fogadja az összes aszinkron megszakítást.
Amikor a feladatütemező a következő futásra kiválasztandó processzt keresi, először megnézi mely processzek várakoznak végrehajtásra. Amikor új processzt hozunk létre fork-kal, a létrejövő példány örökli az őt fork-kal létrehozó processz prioritását. Azonban amint már láttuk, indíthatunk processzeket általunk definiált prioritással is a blokknak küldött forkAt üzenet segítségével is.
Például ha alkalmazásunk más programokkal kommunikál socket-eken keresztül, elképzelhető, hogy 3 processzünk van: egy bemenetkezelő, egy feldolgozó és egy eredménymegjelenítő folyamat. Lehetséges, hogy a feldolgozó rész valamilyen általunk megadott prioritás mellett szeretnénk futtatni, a bemeneti ciklust valamivel magasabbal, a megjelenítő részt pedig még nagyobb prioritással.
Nézzük hogyan befolyásolja egy magasabb prioritású processz egy alacsonyabb prioritású végrehajtását! A következő VisualWorks implementáció alatti példák szépen megvilágítják a lényeget. Ebben a megvalósításban 1-től 100-ig adhatunk prioritásokat, az alapértelmezés pedig 50.
Első példánkban 50-es prioritással forkolunk egy processzt. Mivel a felhasználói felület szintén 50-es prioritási értékkel fut, a létrehozott folyamat nem szakad meg, így az lefut. A felhasználói felület ezután futtatja a második szálat, mely szintén befejeződik. (A késleltetés annak érdekében szerepel a kódban, hogy az első folyamatnak biztosan legyen esélye elindulni. A késleltetés nélkül a felhasználói felület tovább futna és forkolná a második processzt. Abban a pillanatban a nem lenne már másik feladata, így az ütemező egy másik folyamatot keresne futtatásra, és a második processzt futtatná, mivel annak nagyobb a prioritása.) Nézzük tehát a kódot:
Az eredmény:
Második példánkban az első processz 49-es értékkel kerül indításra, mely alacsonyabb a felhasználói felület prioritásánál. A forkolt processz épphogy csak elindul, megszakítja a felhasználói felület szála, mely elindítja a második processzt. A második processz az elsőnél magasabb prioritással fut, ezért befejeződik. Ezután az első processz ütemeződik, és az is befejeződik. A kód és az eredmény:
Harmadik példánkban az első processz az első számjegy kiírása után átadja az irányítást. Ezután a felhasználói felület forkolja a második processzt, mely - lévén magasabb prioritású - elkezd futni. Bár a második processz átadja a vezérlést első számának kinyomtatása után, azonnal fut tovább, ugyanis az ütemezhető processzek közül neki van a legnagyobb prioritása.
A Smalltalk processzei részlegesen megelőzők. Megszakíthatóak magasabb prioritású processzek által, de alacsonyabb vagy velük megegyező prioritású processzek nem fogják őket megszakítani. Ez különbözik például a Unix multitaszk operációs rendszer megvalósításától, melyben a folyamatok időszeleteket kapnak, és annak lejártakor az operációs rendszer elveszi tőlük a vezérlést. Mivel a Smalltalk processzt nem szakíthatja meg nála alacsonyabb vagy vele megegyező prioritású folyamat, könnyű olyan ciklusokba kerülni, melyekből nem tudunk kilépni.
Példáinkban az egyszerűség kedvéért számozást használtunk, de sokkal jobb gyakorlat a ProcessorScheduler priority names protokolljában definiált nevek használata. A VisualWorks például a következő prioritásokat definiálja.
A nevek használata lehetővé teszi a hozzájuk tartozó értékek megváltoztatását a későbbi kiadásokban a konkrét alkalmazások megváltoztatásának szükségessége nélkül. Ráadásul definiálhatunk saját neveket, melyhez igényeinknek megfelelő prioritási értéket rendelhetünk, s folyamatainkat ezen nevek segítségével indíthatjuk:
Hacsak nincs rá nyomós indokunk, alkalmazásaink processzeinek prioritás értékeit ne állítsuk a userBackgroundPriority-userInterruptPriority intervallumon kívülre.
A Semaphore több processz szinkronizálására használható objektum. Egy Process a Semaphore-nak küldött wait üzenettel vár egy esemény bekövetkeztére. Egy másik processz a szemafornak küldött signal üzenettel jelzi az esemény bekövetkeztét. A szignálra váró folyamat nem fut tovább, míg az be nem következett. Nézzünk egy példát:
Az eredmény:
Egy szemafor csak annyi várakozó processzt enged el, amennyi szignált kapott. Mikor a szemafor egy olyan wait üzenetet kap, melyhez nem érkezett signal üzenet, a wait-et küldő processz felfüggesztődik. Minden szemafor kezel egy láncolt listát, mely a felfüggesztett processzeket tartalmazza, és FIFO mechanizmus alapján engedi el őket. A lista szerkezetét mutatja a következő ábra.
Mikor a szemafor egynél több processztól kap wait üzenetet, minden egyes figyelt processztől kapott signal üzenetre csak egyetlen processzt enged el.
A ProcessorScheduler-rel ellentétben a Semaphor nem foglalkozik a processzek prioritásával: abban a sorrendben tárolja őket a sorában, amelyben azok várakozásra érkeztek.
A szemaforok a kód kritikus részének védelmére is használhatók. Gyakran használják őket ilyen kódrészekre vonatkozó kölcsönös kizárás megvalósítására. Ezt a #critical: példánymetódus is támogatja: az argumentumként kapott blokk csak akkor hajtódik végre, ha nincs más ugyanehhez a szemaforhoz tartozó kritikus blokk végrehajtás alatt.
A kölcsönös kizárásra használt szemafornak egy extra signal üzenettel kell kezdődnie, különben sosem lehet belépni a kritikus szakaszba. Egy speciális példánykonstrukciós metódus áll a rendelkezésre: Semaphore forMutualExclusion.
A Delay osztály példányai processzek késleltetésére használhatók. A Delay osztály példánya a wait üzenetre az aktív processz egy adott ideig tartó felfüggesztésével fog reagálni. A visszatérési időt a Delay példány létrehozásánál kell megadni, pl. az aktuális időponthoz képest relatíve a #forMilliseconds: és #forSeconds: üzenetekkel. Ez a visszatérési idő azonban megadható abszolút módon is a rendszer miliszekundumos órájának segítségével az #untilMilliseconds: üzenet használatával. Az ezzel a módszerrel létrehozott Delay-eknek legfeljebb egyszer küldhetünk wait üzenetet.
Példa:
Igen gyakori, hogy az alkalmazásokban tipikusan kétféle processzel találkozunk. Az egyik fajta elindul, végrehajtja feladatát, majd automatikusan terminál. A másik típus ciklusban várakozik egy objektum felbukkanására, majd üzenetet küld neki. Ez szükségessé teszi a processzek közötti kommunikációt, mely megvalósítására a Smalltalk által nyújtott eszköz a SharedQueue.
Két folyamat közötti kommunikáció felépítéséhez létrehozunk egy SharedQueue-t, majd mindkét processzt értesítjük erről. Az egyik folyamat objektumokat helyez az osztott sorba nextPut: üzenettel, a másik pedig a next segítségével objektumot vesz ki belőle. Mikor egy objektum elküldi a next üzenetet, blokkolódik míg üres a sor (a blokkolódást igazából egy szemaforra való várakozás eredményezi). A következő ábra két osztott sort használó kommunikáló processzt mutat.
A következő példában a második processz olvas egy számot, majd az osztott sorra teszi, míg az első folyamat először olvas a sorból, és ez után írja ki a számot. Ha kipróbáljuk az alábbi kódot, a Transcript ablakban a következőket kapjuk: W1, R1, W2, R2, W3, R3, W4, R5, W5, R5.
Vegyük észre, hogy az olvasó folyamat ciklusban fut, míg nem termináljuk explicit módon a terminate üzenettel. Ha újra kipróbáljuk a példát, ezúttal kihagyva a második folyamat kódjából a Processor yield metódushívást, a következőt kapjuk: W1, W2, W3, W4, W5, R1, R2, R3, R4, R5. Ez a Smalltalk parciális preempivitásának következménye: az első folyamatnak esélye sincs futni, míg a második be nem fejeződött. Általában beleteszünk a kódba egy Processor yield hívást, miután objektumot helyeztünk a sorba - ez biztosítja, hogy a sorra váró processz kap esélyt a végrehajtásra. Például:
Amennyiben az olvasó folyamat nem akar blokkolódni, használhatja az isEmpty üzentet, mellyel megállapíthatja, van-e már valami a soron, ill. a peek-et aminek segítségével a sor következő eleme annak sorból való kivétele nélkül kiolvasható.
A SharedQueue-k FIFO sorok: a sorba helyezett objektumokat a behelyezésüknek megfelelő sorrendben olvashatjuk ki. Azonban vannak esetek, mikor egy egyszerű FIFO sor használata nem elégséges, mivel valamiféle prioritási sémát szeretnénk a rendszerünkbe építeni. A SharedQueue nem nyújt szolgáltatásokat prioritásos tárolások megvalósítására, azonban származtatással létrehozhatunk belőle egy PrioritySharedQueue-t. A következő példa egy egyszerű PrioritySharedQueue-t mutat egy defaultPriority példányváltozóval.
A SharedQueue-k FIFO sorok: a sorba helyezett objektumokat a behelyezésüknek Egy egyszerű implementáció helyezi a sorba az objektumokat a prioritások és az objektumok Association-jeiként. Felül kell definiálnunk a next, peek és nextPut: priority: metódusokat az Association kezelése érdekében. Ezenkívül a nextPut: egyszerűen a nextPut: priority:-t hívja a következőképpen.
A SharedQueue-k FIFO sorok: a sorba helyezett objektumokat A PrioritySharedQueue szíve a nextPut: priority: metódus, mely objektumokat helyez a sorba elsődlegesen a prioritást, másodlagosan az időbeliséget figyelembe véve. A nagyobb számok nagyobb prioritásokat jelentenek.
Objektumok osztott sorból való kivétele és feldolgozása tipikusan egy ciklus segítségével történik - erre láthatunk példát ebben a pontban. A bemutatott kód nagyon általános, könnyen örököltethető. Ezen kívül két jellemző erénnyel rendelkezik. Először is nem tesz semmiféle feltételezést arra vonatkozóan, hogy az objektumok honnan származnak ill. feldolgozásuk hogyan történik. Így a myProcessObject: és myGetObject: metódusok a leszármazott osztályokban felüldefiniálhatók, amennyiben más jellegű megvalósításra van szükség. Másodszor pedig lehetővé teszi töréspontok beszúrását az említett metódusokba hibakeresés céljából. Mivel myDoLoop repeat ciklusban van, nem adhatunk töréspontokat hozzá végrehajtás közben, hogy a töréspont kifejthesse hatását.
Megoldásunkat kiterjeszthetjük olyan esetekre, melyben az objektumokat saját fork-olt process-ében dolgozzuk fel. Vannak olyan szituációk - mint például időigényes operációk, vagy olyan műveletek melyek végrehajtásakor egy szükséges erőforrás nem áll rendelkezésre -, melyeknél szükségessé válhat a feldolgozás megszakítása. Ha meg kell szakítanunk a feldolgozást, beállíthatunk az objektumban egy kölcsönös kizárási védelmet biztosító szemaforral védett példányváltozót. A feldolgozás egy megfelelő pillanatában az objektum ellenőrizheti, hogy meg lett-e szakítva, és elvégezheti a megfelelő intézkedéseket. Az alábbi kódban eltároljuk az objektumot, majd miután az objektum végrehajtotta a processYourself metódust, kivesszük az objektumot a tárolónkból. A velueNowOrOnUnwindDo: üzenet biztosítja az objektum eltávolítását függetlenül attó, hogy a processYourself metódus végrehajtódik vagy megszakítódik egy kiváltott kivétel miatt.
Processzeink általában akkor érnek véget, mikor a végrehajtás eléri a létrehozásukkor forkolt blokk végét. Vannak azonban olyan folyamatok, melyek végtelen ciklusban hajtódnak végre, így blokkjuk jobb oldali szögletes zárójeléig sosem jut el a vezérlés. Emiatt szükségünk van egy másik eszközre, ez pedig a már korábban is említett termiante hívás. Nézzünk egy példát!
Minden következmény nélkül küldhetünk terminate üzenetet egy már leállított folyamatunknak is, nem kell "káros" mellékhatásoktól tartanunk. Amennyiben egy processzünket végrehajtásának közepén szeretnénk leállítani, jogosan merül fel bennünk az igény egy olyan eszköz iránt, mely lehetővé teszi a folyamat "gyengéd" terminálását, megengedve, hogy az még befejezhesse feladatát. Egy megoldás lehet, hogy a folyamatra bízzuk saját maga leállítását a Processor terminateActive vagy Processor activeProcess terminate üzenetek segítségével. Ha például folyamatunk egy osztott sorból olvas, megtehetjük, hogy egy speciális termináló objektumot helyezünk a sorra. Ha a sor olvasója mondjuk a sharedQueue next processYourself üzenetet küldi, a termináló objektumunk a következő kóddal rendelkezhet:
A legtöbb alkalmazásban nem használunk terminate vagy suspend hívásokat, de a lehetőségünk megvan rá arra az esetre, ha netalán mégis szükségünk lenne rájuk. Egy processz felfüggesztéséhez küldjünk el neki egy suspend, a folyatásához pedig egy resume üzenetet. Egy folyamatot elindíthatunk felfüggesztett állapotban is egy BlockClosure-nek küldött newProcess üzenettel - valójában a fork-ot is így implementálták: a fork nem más, mint egy newProcess üzenet, amit egy resume követ. Itt láthatunk egy példát egy olyan processz létrehozására, mely nem kezdi meg azonnal futását:
Ha úgy döntünk, hogy processzünk futását a suspend és resume üzenetekkel akarjuk befolyásolni, legyünk nagyon óvatosak, mikor használjuk őket! Lehet, hogy éppen egy olyan tevékenység közepén van, melynek a felfüggesztés előtt be kell fejeződnie. Az egyik megoldás, hogy a folyamatra hagyjuk saját magának felfüggesztését, abban az esetben, mikor ez biztonságos. A következő példa kipróbálásakor azt láthatjuk, hogy processzünk ötször jeleníti meg az időpontot, felfüggeszti saját magát, majd 5 másodperc elteltével újraindítjuk.
A Smalltalk lehetőséget biztosít rá, hogy folyamatainkat megszakíthassuk, és egy másik - a korábbitól eltérő - kód végrehajtását bízzuk rájuk. Erre a funkcióra valószínűleg sosem lesz szükségünk, a teljesség kedvéért azonban röviden bemutatjuk ezt a funkciót. Egy processz megszakításához és egy másik kód benne történő futtatásához küldjük el folyamatunknak az interruptWith: aBlockOfCode üzenetet. A folyamat ekkor elmenti környezetét, végrehajtja a neki átadott blokkot, visszaállítja környezetét, majd visszatér korábbi feladatának végrehajtásához.
Lehetőségünk van megakadályozni a megszakításokat arra az esetre, ha folyamataink olyan fontos dolgot végeznek, melyeket biztosan nem akarunk félbeszakíttatni. A megszakításmentes kódunkat védetté tehetjük, ha egy blokkba helyezzük, majd elküldjük neki a valueUninterruptably üzenetet. Az osztott sorok például kihasználják ezt a lehetőséget. Az ilyen sorok minden hozzáférése egy kölcsönös kizárást biztosító szemaforral védett. A SharedQueue>>size metódus például a következőképpen van implementálva.
Hogy használjuk-e valaha a megszakítási lehetőséget? Valószínűleg nem. De megéri róla tudni.
(Megjegyzés: Ebben a pontban a Smalltalk VisualWorks implementációjához tartozó példák kerülnek tárgyalásra, így a közölt forráskódokban előfordulhatnak megvalósítás-specifikus részletek is.)
Gyakorlatlan felhasználóként könnyen hozhatunk létre olyan folyamatokat, melyek végrehajtása végtelen ciklusban való végrehajtódásuk révén nem terminálnak, s megszakításuk a felhasználó számára nem lehetséges. Abban az esetben, ha userSchedulingPriority vagy magasabb prioritási érték mellett futnak (azaz legalább a felhasználói felület prioritási szintjén), még a Ctrl-C sem segít. Ez okból kifolyólag jó szokás aktuális munkánk elmentése minden új processz kipróbálása előtt, valamint processzeink userSchedulingPriority alatti prioritási érték melletti futtatása.
Ha mégis beragadnánk, kilőhetjük a virtuális gép folyamatát, vagy használhatjuk az Emergrency Evaluatort. Hacsak nem definiáltuk át a megszakító billentyűkombinációt, Shift-Ctrl-C segítségével hozhatjuk fel az Emergency Evaluatort, ahol az ObjectMemory quit parancsot beírva, majd Escape-et ütve kiléphetünk a Smalltalkból.
Egy másik dolog, ami történhet a "forkolt" processzekkel végzett munka során, hogy elveszítjük őket. Tegyük fel, hogy van egy végtelen ciklusban futó folyamatunk, ami esetleg még megszakítható is lenne, viszont a rá vonatkozó referencia elvesztésével esélyünk sincs terminálására.
Processzek nyomkövetésének egyik módja azok globális változókban történő tárolása. Tegyük fel például, hogy folyamatainkat a MyGlobals osztály globális változóiban szeretnénk nyilvántartani. Amennyiben van egy Processes nevű osztályváltozónk, melyet (osztály- vagy lusta-inicializációval) OrderedCollection-ként inicializáltunk, elkészíthetjük a következő két metódust.
A MyGlobals osztály segítségével bármikor megnézhetjük mely folyamatokat indítottuk el, s terminálhatjuk őket akár egyenként, akár - egy alkalmas metódust készítve - az összeset egyszerre. Hogy könnyebben láthassuk melyik processzről is van szó, készíthetünk egy processzből és névből álló asszociációt (Association).
Ez a séma feltételezi, hogy az elindított és a gyűjteményhez adott folyamatainkat el is tudjuk távolítani. Sok folyamat azonban rövid életű, s miután egyszerűen véget értek, nem könnyű őket eltávolítani (hacsak nem távolítják el magukat a kollekcióból lejártuk előtt). Az itt bemutatott elgondolás ezért legjobban hosszú életű folyamatok esetében használható, melyeket az alkalmazás képes mind elindítani, mind pedig terminálni.
Egy másik megközelítésként elkészíthetjük saját folyamat-osztályunkat, mondjuk MyProcess néven a Process osztály leszármazottjaként. Egy példányváltozójában tárolhatjuk a processz nevét.
Készítsünk a példányváltozóhoz kiolvasó és módosító metódusokat (processName és processName:), valamint írjunk egy printOn: metódust:
Anélkül, hogy a részletekbe belemennénk, a BlockClosure három új metódusát kell elkészítenünk. Az első newProcessWithName:, mely létrehozza a MyProcess osztály egy példányát, és beállítja a folyamat nevét. A másik két metódus pedig: forkWithName: és forkAt: withName: melyek a newProcess helyett a newProcessWithName: metódust hívják majd. Ezután létre kell hoznunk a Behavior egy új metódusát: allInstancesAndSubInstances az allInstances helyett. Ezután már el is indíthatjuk faját folyamatainkat a következőképpen:
Ezután megnézhetjük futó (illetve már befejeződött, de a szemétgyűjtő által még nem eltakarított) folyamatainkat a MyProcess allInstances segítségével. Egy elvesztett folyamat befejezéséhez nyissunk egy inspector ablakot a tömb megfelelő elemén, majd értékeljük ki: self terminate.
Az image fájlunkat fogvatartó folyamatok problémájának megoldására írhatunk egy osztálymetódust, mely a MyProcess osztály összes példányát terminálja. Az Emergency Evaluator-ből (Ctrl-Shift-C) ekkor a MyProcess terminateAll parancs kiadásával (egy Escape-pel követve) visszakaphatjuk a kurzort.
Egyszerűbb lehet megváltoztatni magát a Process osztályt, mint örököltetni belőle. Ha ezt a megközelítést választjuk, hozzá kell adnunk egy processName példányváltozót. Ezen felül a BlockClosure osztályhoz is szükséges lesz fork-kezelő metódusok hozzáadása a folyamat nevének beállításához, mielőtt a processz a resume üzenetet megkapná. Végül a MyProcess class>>terminateAll metódus helyett valami ilyesmire lenne szükségünk:
A Smalltalk nyelv csak az egyszeres öröklődést támogatja. Hogyan tudjuk akkor garantálni egy osztályban több különböző funkcionalitás meglétét? Erre valók a protokollok. Egy protokoll üzenetek egy halmaza. Egy osztály megfelel egy protokollnak, ha a protokoll összes üzenetére képes válaszolni. Az objektumtallózó ablak feltünteti az általa ismert protokollok közül azokat, amelyeknek az adott osztály megfelel. Így a programozó láthatja, hogy képes-e az adott osztály ellátni a szükséges feladatot. Néhány általánosan használt protokoll:
Amit eddig láttunk a nyelvből, az csak üzenetek küldözgetése volt objektumok között. Valahol pedig tényleges műveleteknek is kell történni. Erre valók a primitívek. Viszonylag kevés (~100) elemi műveletet definiáltak a nyelv tervezői, és sorszámmal látták el őket. Ezeket a primitív műveleteket a Smalltalk rendszert futtató virtuális gépnek kell tudni végrehajtani. Ezenkívül az operációs rendszer alapszintű szolgáltatásai is elérhetők a standard-call utasítás segítségével. Például:
<primitive: 81> | "blokk végrehajtása" |
<primitive: 15> | "egész számok összeadása" |
<stdcall: handle GetCurrentThread> | "aktuális szál lekérdezése az oprendszertől" |
Minden olyan osztálynak, amely képes események kiváltására, van egy connectSignal nevű metódusa. Egy ilyen üzenetet kell küldenünk az eseménykezelők felcsatolásához. Az eseménykezelők neve elé #-ot kell tenni.
Példa egy gomb (button) click eseményének feldolgozására egy buttonOnClick nevű metódussal, amely ugyanabban az osztályban található:
Mivel esetünkben nem küldünk további információt, a userData üres lesz.
Az eseménykezelők lecsatolása a disconnect üzenettel történik.
Smalltalkban hálózati kapcsolat a más nyelvekből jól ismert Socket-es módszerrel hozható létre. A szükséges osztályok a Sockets csomagban találhatóak. Kapcsolat kéftéleképpen hozható létre. Vagy mi kezdeményezünk kapcsolatot egy hálózati cím és egy port megadásával, vagy egy ServerSocket adja vissza. A Socket-ek felett stream-ek vannak definiálva, így a legalacsonyabb szinten nem kell menedzselnünk a kapcsolatot. Ha a Socket éppen üres, a hívás blokkolódik.
Példa kapcsolat létrehozására, majd egy üzenet küldésére és fogadására:
A következő példában indítunk egy listener-t, várunk egy csatlakozó kliensre, majd visszaküldjük neki az üzenetet, amit kaptunk. A fenti példaprogrammal együtt így összeáll egy egyszerű echo kliens-szerver pár.
Természetesen több klienst szeretnénk kiszolgálni, így praktikus ezt betenni egy végtelen ciklusba. Ez azonban lefagyasztaná a programot, ezért célszerű külön szálon elindítani. Ezt a fork üzenettel tehetjük meg.