A Smalltalk programozási nyelv

Egyéb nyelvi elemek

Képernyőpontok

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

Kivételkezelés

Áttekintés

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 szabványos kivételkezelés

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:

FileStream>>named: nameString filename := nameString asFilename. filename exists ifFalse: [...signal exception...]. ^FileStream withDescriptor: filename openDescriptor

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.

[fileStream := FileStream named: nameString] on: ...file not found exception... do: [...handler actions...]. fileStream nextPutAll: ...

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.

Kivétel osztályok és objektumok

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ó:

FileStream>>named: nameString filename := nameString asFilename. filename exists ifFalse: [FileNotFound new fileName: nameString; signal]. ^FileStream withDescriptor: filename openDescriptor

Kezelő:

[fileStream := FileStream named: nameString] on: FileNotFound do: [:ex | "This is how we can get information from an exception." Transcript show: 'File not found: ', ex fileName. ...more handler actions...]. fileStream nextPutAll: ...

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.

Kivételek terjedése

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.

Kivételek lekezelése

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.

MyApplication>>fooMenuAction [self firstAction; self secondAction] on: Error do: [:ex | Dialog warn: 'An error has occurred: ', ex messageText. ...]

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.

MyApplication>>fooMenuAction [self firstAction self secondAction] on: Error do: [:ex | Dialog warn: 'An error has occurred: ', ex messageText. ex return]

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.

| fileContents | fileContents := ['myfile.txt' asFilename readStream contents] on: Error do: [:ex | ex return: String new]

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.

| fileContents | fileContents := ['myfile.txt' asFilename readStream contents] on: Error do: [:ex | | newName | newName := Dialog prompt: 'Problem reading file. Another name?'. ex retryUsing: [newName asFilename readStream contents]]

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.

| fileName fileContents | fileName := 'myfile.txt'. fileContents := [fileName asFilename readStream contents] on: Error do: [:ex | fileName := Dialog prompt: 'Problem reading file. Another name?'. ex retry]

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.

[:ex | (ex receiver == self and: [ex message selector == each]) ifTrue: [ex resignalAs: InvalidAction] ifFalse: [ex pass]]

Folytatható kivételek

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.

MyApplication>>readOptionsFrom: aStream | option | [aStream atEnd] whileFalse: [option := self parseOptionString. "nil if invalid" option isNil ifTrue: [InvalidOption signal] ifFalse: [self addOption: option]]

MyApplication>>readConfiguration [self readOptionsFrom: 'options' asFilename readStream] on: InvalidOption do: [:ex | (Dialog confirm: 'Invalid option line. Continue loading?') ifTrue: [ex resume] ifFalse: [ex return]]

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.

Kivételhalmazok

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.

[...] on: MessageNotUnderstood, FileNotFound, ZeroDivide do: [...]

Párhuzamosság

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.

Process

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.

Processzek állapotai

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.

Fork

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!

[Transcript cr; show: 100 factorial printString] fork

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:

| aProcess | aProcess:=[Transcript cr; show: 100 factorial printString] newProcess

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:

| aProcess | aProcess:=[ :first :second | Transcript cr; show: (first raisedTo: second) printString ] newProcessWith: #(2 20). aProcess resume.

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.

ProcessorScheduler

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.

Folyamat-ütemezés_1
Folyamat-ütemezés_2

Prioritások

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:

[10 timesRepeat: [Transcript show: '1']] forkAt: 50. (Delay forMilliseconds: 1) wait. [10 timesRepeat: [Transcript show: '2']] forkAt: 51.

Az eredmény:

11111111112222222222

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:

[10 timesRepeat: [Transcript show: '1']] forkAt: 49. (Delay forMilliseconds: 1) wait. [10 timesRepeat: [Transcript show: '2']] forkAt: 51. 112222222222111111111

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.

[10 timesRepeat: [Transcript show: '1'. Processor yield]] forkAt: 50. (Delay forMilliseconds: 1) wait. [10 timesRepeat: [Transcript show: '2'. Processor yield]] forkAt: 51. 12222222222111111111

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.

100 (timingPriority) - valós idejű processzek használják (pl. Delay) 98 (highIOPriority) - időérzékeny I/O processzek használják, mint például a hálózatról történő olvasást megvalósítók 90 (lowIOPriority) - a legtöbb I/O processz ezt használja (billentyűzet, egér, stb. esetén) 70 (userInterruptPriority) - azonnali kiszolgálást igénylő felhasználói processzek számára 50 (userSchedulingPriority) - normál felhasználói interakciót megvalósító processzek használják, valamint a felhasználói felület is ezen fut 30 (userBackgroundPriority) - a háttérben futó felhasználói processzek használják 1 (systemRockButtonPriority) - a lehető legalacsonyabb prioritás

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:

MyGlobals class>>appProcessPriority ^Processor userSchedulingPriority [some code] forkAt: MyGlobals appProcessPriority

Hacsak nincs rá nyomós indokunk, alkalmazásaink processzeinek prioritás értékeit ne állítsuk a userBackgroundPriority-userInterruptPriority intervallumon kívülre.

Semaphore

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:

| sem | Transcript clear. sem := Semaphore new. [Transcript show: 'The ' ] fork. [Transcript show: 'quick '. sem wait. Transcript show: 'fox'. sem signal ] fork. [Transcript show: 'brown '. sem signal. sem wait. Transcript show: 'jumps over the lazy dog'; cr ] fork

Az eredmény:

The quick brown fox jumps over the lazy dog.

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.

Semaphore

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.

| n d sem | n := 100000. d := Delay forMilliseconds: 400. [ | i temp | Transcript cr; show: 'P1 running'. i := 1. temp := 0. sem critical: [ [ i <= n ] whileTrue: [ temp := temp + i. (i = 5000) ifTrue: [ d wait ]. i := i + 1 ]. ]. Transcript cr; show: 'P1 sum is = '; show: temp printString ] forkAt: 60. [ Transcript cr; show: 'P2 running'. sem critical: [ n := 10 ]] forkAt: 50.

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.

Szemaforok

Késleltetések

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:

| minuteWait | minuteWait := Delay forSeconds: 60. minuteWait wait.

Processzek közötti kommunikáció

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.

Szintaxis

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.

Processzek kommunikációja

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.

sharedQueue := SharedQueue new. readProcess := [[Transcript show: ' R', sharedQueue next printString] repeat] fork. [1 to: 5 do: [:index | Transcript show: ' W', index printString. sharedQueue nextPut: index. Processor yield]] fork. (Delay forSeconds: 5) wait. readProcess terminate.

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:

sharedQueue nextPut: anObject. Processor yield.

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ó.

PrioritySharedQueue

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.

PrioritySharedQueue>>defaultPriority: aPriority defaultPriority := aPriority PrioritySharedQueue>>defaultPriority ^defaultPriority isNil ifTrue: [defaultPriority := 0] ifFalse: [defaultPriority]

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.

PrioritySharedQueue>>nextPut: anObject ^self nextPut: anObject priority: self defaultPriority

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.

PrioritySharedQueue>>nextPut: anObject priority: aPriority [accessProtect critical: [ |foundElement | foundElement := contents reverseDetect: [:element | aPriority <= element key] ifNone: [nil]. foundElement isNil ifTrue: [contents addFirst: (aPriority -> anObject)] ifFalse: [contents add: (aPriority -> anObject) after: foundElement]] ] valueUninterruptably. readSynch signal. ^anObject
Egy általános SharedQueue olvasó mechanizmus

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.

MyClass>>myDoLoop [self myProcessObject: self myGetObject. Processor yield] repeat MyClass>>myGetObject ^self mySharedQueue next.

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.

MyClass>>myProcessObject: anObject self myAdd: anObject. [ [anObject processYourself] valueNowOrOnUnwindDo: [self myRemove: anObject] ] fork

Processzek leállítása

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!

process := [[Transcript cr; show: 'Hi there'. (Delay forSeconds: 1) wait] repeat] fork. (Delay forSeconds: 5) wait. process terminate. Transcript cr; show: 'All Done'.

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:

TerminationObject>>processYourself Processor terminateActive

Processzek futásának szabályozása

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:

process := [Transcript cr; show: 'Done'] newProcess. (Delay forSeconds: 3) wait. process resume

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.

process := [1 to: 10 do: [:index | Transcript cr; show: Time now printString. index = 5 ifTrue: [Processor activeProcess suspend]]] fork. (Delay forSeconds: 5) wait. process resume.

Processzek megszakítása

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.

^[accessProtect critical: [contents size]] valueUninterruptably

Hogy használjuk-e valaha a megszakítási lehetőséget? Valószínűleg nem. De megéri róla tudni.

Folyamatok nyomkövetése

(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.

Folyamatok követése globális változókkal

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.

MyGlobals class>>addProcess: aProcess ^Processes add: aProcess MyGlobals class>>removeProcess: aProcess ^Processes remove: aProcess ifAbsent: [nil]

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).

MyGlobals class>>addProcess: aProcess name: aName ^Processes add: aProcess -> aName MyGlobals class>>removeProcess: aProcess ^Processes removeAllSuchThat: [:each | each key = aProcess]

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.

Folyamatok követése alosztály képzéssel

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.

Process subclass: #MyProcess instanceVariableNames: 'processName ' classVariableNames: '' poolDictionaries: '' category: 'MyStuff'

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:

MyProcess>>printOn: aStream super printOn: aStream. aStream nextPutAll: ' ('. aStream nextPutAll: self processName. aStream nextPut: $)

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:

[some code] forkWithName: 'some name'.

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.

MyProcess class>>terminateAll self allInstances do: [:each | Transcript cr; show: each printString. each terminate]

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:

Process class>>terminateNamed self allInstances do: [:each | each processName notNil ifTrue: [Transcript cr; show: each printString. each terminate]]

Protokollok

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:

Primitívek

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"

Eseménykezelők

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ó:

button connectSignal: 'clicked' to: self selector: #buttonOnClick userData: nil

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.

Hálózat

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:

| connection message | connection := (Sockets.Socket remote: 'localhost' port: 8000). message displayOn: connection. (String with: (Character value: 10)) displayOn: connection. connection flush. message := (connection nextLine)

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.

| serverSocket client message | serverSocket := (Sockets.ServerSocket port: 8000). serverSocket waitForConnection. client := (serverSocket accept). message := (client nextLine). message displayOn: client. (String with: (Character value: 10)) displayOn: client. client flush

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.

[ | serverSocket | serverSocket := (Sockets.ServerSocket port: 8000). [ true ] whileTrue: [ | client message | serverSocket waitForConnection. client := (serverSocket accept). message := (client nextLine). Message displayOn: client. (String with: (Character value: 10)) displayOn: client. client flush ] ] fork