Az Objective-C programozási nyelv

Párhuzamosság

Párhuzamosság

Szálak szinkronizálása

Az Objective-C nyelv hagyományosan szekvenciális nyelv. Ez azt jelenti, hogy a programokon belül nincs lehetőség a párhuzamosság kezelésére, szálak létrehozására.

A nyelv azonban támogatja a többszálúságot abban az értelemben, hogy a különböző taszkokban vagy szálakban futó objektumok tudnak egymással kommunikálni és egymás metódusait meg tudják hívni.

Ebben az esetben lehet szükség egy adott kritikus szekció megfelelő biztosítására, és erre ad támogatást az Objective-C a

@synchronized() { //kritikus kod }
direktívával.

Ez a direktíva azt biztosítja, hogy a benne szereplő kritikus kód biztosan csak egyetlen szálban futhasson, a többi szálnak várnia kell, amíg az éppen futtató szálban a vezérlés a blokk vége utáni utasításra ér. A direktíva egyetlen paramétere tetszőleges objektum lehet, a saját objektumot jelölő self is. Ez a paraméter tulajdonképpen egy mutex, egy kölcsönös kizárásért felelős szemafor.

Érdemes minden kritikus kódnak külön mutex objektumot készíteni még a többszálúvá válás előtt, így a versengés is megelőzhető.

A nyelv megengedi a rekurzív kritikus kódokat, ugyanazzal a mutexszel, ekkor más processzek addig nem használhatják a kódot, amíg az utolsó lockot fel nem oldja a használó szál, vagy kivétel keletkezik. Kivétel esetén a runtime rendszer elkapja a kivételt, feloldja a lockokat, és továbbdobja a kivételt.

A nyelvhez tartozó osztálykönyvtárak már többféle eszközzel is támogatják a párhuzamosság kezelését.

Eseménykezelés, időzítők

Az események kezeléséért az ún. RunLoop-ok felelősek. Arra használhatjuk őket, hogy időzítsünk velük bizonyos munkákat, illetve hogy bejövő események hatására ( pl. hálózatról adat érkezett és annak feldolgozására ) futtassunk bizonyos kódokat. Összefoglalva: a RunLoop felelős azért, hogy egy szál elfoglalt maradjon, illetve ha nincs semmi dolga, akkor alvó állapotba helyeződjön, ezáltal nem terhelve a rendszert.

A RunLoop-ok kezelése nem teljesen automatikus. Minden szál létrehozásakor automatikusan létrejön egy NSRunLoop objektum, de a fő szál kivételével ezek nem indulnak el maguktól, nekünk kell kézzel elindítani őket, és a kódunk megtervezésekor figyelembe kell vennünk ezeket a szempontokat.

Ha a program rendelkezik valamilyen felhasználói felülettel, vagy hálózati kapcsolatot használ, akkor ezek kezelését az NSRunLoop osztállyal tudjuk megoldani. Ez általában automatikusan létrejön, így minden futási szálhoz tartozik egy eseménykezelő ciklus.

Egy RunLoop két forrásból kaphat eseményeket: az első a bemeneti források, amelyek aszinkron jellegűek (például hálózati események, vagy kérések másik szálakból), a második pedig a timer események, amelyek szinkron jellegűek ( egy kód lefuttatása bizonyos idő múlva/bizonyos időközönként ).

Az aktuális szál RunLoop-ját a +currentRunLoop metódussal tudjuk lekérni, és a run üzenettel indíthatjuk el a futtatását. Lehetőség van úgy elindítani a RunLoop-ot, hogy egy bizonyos ideig fusson, ezt a runUntilDate üzenet küldésével érhetjük el.

Egy RunLoop különböző módokban képes futni. Ezzel elérhetjük, hogy bizonyos jellegű események ne kerüljenek feldolgozásra (például figyelmen kívül hagyja a user interface eseményeket, de figyeljen a timer eseményekre). Ha egy olyan eseményt talál a RunLoop, amire az adott módban nem reagálhat, akkor azt eltárolja, és addig megtartja, amíg olyan módba nem kerül, amikor meghívhatja a kezelőjét (kivéve, ha törlik az adott eseményt.) Saját magunk is definiálhatunk ilyen futási módokat.

Ezen kívül lehetőség van ún. RunLoop Observert telepíteni egy RunLoop-ra, amivel nyomon tudjuk követni, hogy milyen eseményeket kapott és miket dolgozott fel. Ezekkel az eszközökkel az üzenetek kezelése nagyon jól kézbentartható.

A timer-ek működése szorosan kapcsolódik a RunLoop-okhoz. Az időzítőket megvalósító osztály az NSTimer (Cocoa osztálykönyvtárban legalábbis). Timer kétféleképpen hozható létre:

scheduledTimerWithTimeInterval: target: selector: userInfo: repeats

vagy

timerWithTimeInterval: target: selector: userInfo: repeats:

Mindkét esetben meg kell adnunk target-két azt az objektumot, aminek üzenetet akarunk küldeni, a selector fogja megadni, hogy milyen üzenet legyen az. A repeats-szel megadható, hogy az üzenetküldés periodikusan ismétlődjön, vagy csak egyszeri legyen. Utóbbi esetben az első hívás után a timer kiveszi magát az őt tartalmazó RunLoop-ból.

A két hívás között a különbség pedig, hogy az első esetben a timer automatikusan bekerül annak a szálnak a RunLoop-jába, amelyben létrehozták, míg másik esetben nekünk kell kiválasztani a RunLoop-ot. Ezzel lehetőségünk van más szálakban kód futtatására.

Ide kapcsolódik az NSObject ősosztályban lévő performSelector metóduscsalád. Ezzel az adott objektumon lehet meghívni bizonyos metódust valamennyi idő eltelte után. Ez létrehoz egy timert, ami nem ismétlődő, és végrehajtja a kívánt metódust. Két fajtája van: a sima performSelector az éppen aktuális szálban hajtja végre a kívánt metódust, míg a performSelectorInMainThread az alkalmazás főszálában teszi ugyanezt. (Más szál meghatározására ezen a módon nincs lehetőség, ha erre van szükség, használjunk NSTimer-t).

Részletesebben lásd:
RunLoop Management
NSTimer dokumentáció
performSelector (NSObject dokumetáció, Adopted protocols rész )

Példa kód

Többszálúság, szálak

Az NSThread osztállyal lehet megvalósítani a szálak futtatását. Két szál ugyanazon a folyamaton belül fut, így ügyelni kell a szinkronizálásra, mivel elérhetnek közös adatokat is.

A szálak közötti kommunikáció megvalósítására több módszer is létezik. Egyrészt a szálak küldhetnek egymásnak üzeneteket (NSNotification), vagy felépíthetnek egy lokális kapcsolatot (NSConnection).

Az elküldött üzenet NsNotification használata esetén megjelenik egy üzenetsorban, amire feliratkozhat tetszőleges számú szál. Az üzeneteket az eseménykezelő ciklus automatikusan feldolgozza, azaz meghívja az üzenetkezelő osztályhoz regisztrált metódust.

NsConnection-nel tetszőleges adatok küldhetőek szálak között, viszont itt egy hálózati kapcsolathoz hasonlóan, manuálisan tudjuk lekérni a megérkezett információt.

Példa kód

Zárolás

Az NSLock osztály segítséget nyújt a kritikus kódrészek vagy objektumok elérésének szabályozásában. Egy lock-ot csak egyszer lehet zárolni, így egyszerre csak egy szál éri el a kritikus részeket.

Az NSLock Több zárolási módot is támogat. Az azonnali zárolás rögtön visszatér attól függő BOOL értékkel, hogy sikerült-e a zárolás. A határozatlan idejű zárolás addig vár, amíg fel nem szabadul az adott zár. Beállítható olyan zárolás is, ami adott dátum és idő értékig próbál zárolni.

A rekurzív zárolás az NSRecursiveLock segítségével valósítható meg, így egy szál többször is zárolhatja ugyanazt a zárat, viszont ugyanannyiszor el is kell engednie, mielőtt újra használható lenne.

Példa kód

Taszkok

A párhuzamosság megvalósításának másik módja a Taszkok használata. Egy Taszk egy teljesen különálló folyamat, ami a gazda folyamattól függetlenül működik. Létrehozása az NSTask osztály segítségével történik.

A gyerek taszkok az bezáródásáról a létrehozó taszk értesül, de ezen kívül teljesen különálló memória területet foglalnak el és használnak. A két folyamat között pl. pipe-okkal vagy hálózati socketekkel lehet kommunikálni, közös adatstruktúrák nem használhatóak.

A gyerek taszk környezeti változóinak nagy részét örökli a szülőtől. Ha változtatni akarunk valamit (pl aktuális könyvtár, stdout cseréje pipe-ra, stdin...), akkor azt a gyerek taszk indítása előtt meg kell tennünk.

Példa kód

Művelet objektumok

Az NSOperation osztály egy absztrakt osztály, ami azt a célt szolgálja, hogy egy egyszerű feladat elvégzéséhez szükséges adatot és kódot egységbe zárja. A programozónak nem kell foglalkozni azzal, hogy hogyan hajtódik végre, azt a rendszer elintézi. Egy ilyen objektum egyszer használhátó, ha egyszer végrehajtja a feladatát utána nem lehet újra végrehajtani.

Mivel ez egy absztrakt osztály, a konkrét feladatot elvégző osztályt származtatással kell előállítani, vagy pedig használhatunk néhány előre elkészített alosztályt ( pl.: az NSInvocationOperation osztály egy objektum egy metódusát tudja meghívni, vagy az NSBlockOperation, amivel C-s blokkok sorozatát tudjuk egymás után végrehajtani ).

Az elkészült művelet objektumokat betehetjük egy NSOperationQueue-ba, ami végrehajtja azokat. Lehetőség van függőségek kialakítására is: megadhatjuk, hogy az egyes művelet objektumok végrehajtása előtt milyen más műveleteknek kell befejeződniük. A maxConcurrentOperationCount property-vel megadható a párhuzamosan végrehajtható művelet objektumok száma.(Lehetőség van arra, hogy nem használunk művelet sort, ekkor a start metódust kell meghívnunk a művelet elindítására.)

Függőségek definiálása:

Az NSOperation által támogatott függőségek nem tesznek különbséget, hogy a függő művelet sikeresen vagy sikertelenül hajtódott végre. Tehát ha meghívjuk egy művelet objektum cancel metódusát, akkor hasonlóan kezeli mint a befejezettet. A függőségeket az előtt kell definiálnunk, mielőtt hozzáadnánk őket egy NSOperationQueue-nak. Ha nincsenek függőségek megadva akkor a művelet objektumokat nem determinisztikus sorrendben hajtja végre.
Továbbá lehetőség van priorizálni a művelet objektumokat, a prioritást a setQueuePriority: metódussal adhatjuk meg. Prioritásokat egymástól független operációknál érdemes használni, nem szabad függőségek kezelésére használni. A prioritási értékeket előre meghatározott konstansok tárolják:
enum { NSOperationQueuePriorityVeryLow = -8, NSOperationQueuePriorityLow = -4, NSOperationQueuePriorityNormal = 0, NSOperationQueuePriorityHigh = 4, NSOperationQueuePriorityVeryHigh = 8 };
Amennyiben egy olyan prioritást adunk meg, amely nem egyezik a fent megadott konstansokkal, akkor az setQueuePriority: metódus a NSOperationQueuePriorityNormal prioritás felé haladva az első egyező konstans értékére állítja be azt.

A műveletek alapvetően külön szálakban hajtódnak végre. Az NSOperation osztály metódusai szálbiztosak, és nagyon fontos, hogy az általunk a származtatott osztályban írt metódusok (adatlekérdező metódusok, a művelet sikerességét lekérdező metódusok) is szálbiztosak legyenek.

Elosztott objektumok

Bár eredetileg az Objective-C nyelvet egy processzorra korlátozott, és egy memóriaterületet használó programokra fejlesztették ki, az objektum-orientált modell sugallja azt a lehetőséget, hogy a kommunikáció az objektumok között akár szálakon, vagy folyamatokon is átíveljen. Az elosztott objektumok megvalósítása mögött az Objective-C hálózati üzenetküldő szolgáltatásai állnak.

Így például egy szerver-kliens alkalmazásban a szerver intézheti a közösen használt objektumok elosztását, vagy a háttérben történő erőforrás-igényes számításokat. A két folyamat között Objective-C üzenetekkel történhetne a kommunikáció.

A GNUStep és Cocoa objektumkönyvtáraknan megtalálhatóak az elosztott objektumok készítéséhez szükséges eszközök. A kliens az üzeneteket egy úgynevezett Proxy objektumnak küldi, ami egy könnyűsúlyú helyettesítőként működik a kliens oldalon. Ez a Proxy objektum ezután kommunikál a szerverrel, ahol a tényleges helyettesített objektum helyezkedik el.

Ahhoz, hogy a kliens és szerver közötti kommunikáció jól-definiált legyen, azaz ne küldjünk nem létező üzenetet a szervernek, két lehetőségünk van. Egyrészt használhatjuk a futásidejű metódus ellenőrzést, azaz a kliens megkérdezheti a szervert, hogy tud-e kezelni egy adott típusú üzenetet. Másrészt, a kommunikáció gyorsítása érdekében implementálhatunk protokollokat, ekkor a szerver és a kliens oldali proxy objektum is azonos protokollnak felel meg, így biztosak lehetünk benne, hogy a szerver oldali objektum képes lekezelni a neki küldött üzeneteket.

A protokollok használata még több lehetőséget is nyújt: módosítókkal megadhatjuk, hogy az adott paraméter bemenő, kimenő paraméter, vagy esetleg mindkettő (in/out/inout), ezzel csökkentve a hálózati kommunikáció adatmennyiségét, illetve használhatunk aszinkron metódusokat, amelyek futását nem szükséges megvárni a kliens oldalon (Ezek a (oneway void) típusú metódusok lesznek).

Lehetőség van azt is megadni, hogy ha a távoli objektum visszatérési értékként jelenik meg, akkor másolódjon le a kliens oldalra (bycopy), vagy maradjon proxy objektum (byref). Ezekkel együtt használható a többi módosító is, így lehetséges akár egy (bycopy out id *) típusú metódust is létrehozni, ami a szervernek nem küld semmilyen objektumot, visszafelé viszont érték szerint visszakap egyet. Érdemes az érték szerinti átadást használni, ha például egy listát kérünk le a szerverről, amit szeretnénk egyesével bejárni, és így túl nagy lenne a hálózati terhelés és válaszidő, ha minden egyes lekérdezésnél hálózati kommunikáción keresztük kellene dolgoznunk. A másoláskor csak az objektum példányváltozói kerülnek át a klienshez, ahol ezekből újrakonstruálódik az objektum. Fontos, hogy nem minden osztály támogatja az érték szerinti másolást, így például ha a szerver oldalon memóriában nincs jelen az adott objektum.