A Modula3 programozási nyelv

Párhuzamosság

Szintaxis

általános leírás
A nyelv megalkotásakor a készítőknek fontos volt, hogy a Modula-2 meglehetősen szegényes párhuzamos eszköztárát kicseréljék egy korszerűbbre. Az itt használt modell "pehelysúlyú;" folyamatok (lightweight processes, threads) használatát teszi lehetővé, tehát áj folyamat indításakor nem az operációs rendszer indít egy új programot, hanem a futó programon belül jön létre egy logikai folyamat. Az így létrehozott szálak (thread-ek) közösen használják a program memóriaterületét, tehát osztott memórián keresztüli kommunikációra van lehetőség. Ehhez természetesen adott a megfelelő apparátus: kölcsönös kizárás kezelésére adott egy MUTEX alaptípus, valamint a Hoare-féle monitorok megvalósításához könyvtári támogatás van szinkronizációs feltételek kezelésére.
A Thread modul
A Thread modulban gyűjtötték össze a párhuzamos programozáshoz használt könyvtári eszközöket. A legalapvetőbb ezek közül a Closure típus:
TYPE Closure = OBJECT METHODS apply(): REFANY; END;
Ennek a típusnak a leszármazottai segítségével készíthetünk új folyamatokat. A folyamatot a Thread.Fork eljárás indítja el:
PROCEDURE Fork(cl: Closure): T;

Az apply függvény a folyamat főprogramja, a Fork ezt futtatja le. Amit az apply visszaad (egy REFANY típusú érték), az lesz a szál végeredménye. A Fork hívás egy Thread.T típusú értéket ad vissza, ez az elindított szál azonosítója. Ennek, valamint a Join eljárásnak a segítségével várhatjuk meg egy folyamat befejeződését:

PROCEDURE Join(thread: T): REFANY);

Ez az eljárás megvárja, amíg a szál főprogramja lefut, majd az általa visszaadott értékkel tér vissza.

Kommunikáció

A folyamatok közötti kommunikáció osztott változókon keresztül történhet. Ennek a biztonságos lebonyolításához kölcsönös kizárásra van szükség. Erre több eszközt is kitaláltak már, ebben a nyelvben mutexeket használnak, aminek a segítségével más módszerek (pl. szemaforok) is leprogramozhatóak. Ehhez egy MUTEX nevű típus áll a rendelkezésünkre, amely egy speciális objektum típus. Két állapota van: vagy szabad, vagy pontosan egy folyamat birtokolja. Ha épp szabad, akkor bármely folyamat "magáévá teheti"; ha már valaki birtokolja, akkor annak előbb el kell engednie, és ezután lehet újra birtokba venni. Erre a lefoglalás-elengedés műveletpárra vezették be a LOCK utasítást:

VAR mut: MUTEX := NEW(MUTEX); LOCK mut DO   <kritikus szekció> END;

Ebben a kódrészletben a kritikus szekcióval megjelölt helyen egyszerre csak egy folyamat futhat, és mivel a LOCK utasítás be van ágyazva a blokkstruktúrába, nem fordulhat elő az a más nyelvekben gyakori hiba, hogy egy lezárt mutexet valahol elfelejtünk felszabadítani, holtpontot idézve ezzel elő.

Szinkronizáció

A folyamatok futásának szinkronizálását az un. szinkronizációs feltétel ek segítségével lehet megtenni. Ez a fogalom először Hoare monitor-koncepciójában jelent meg, majd kissé módosítva több eszköztár is átvette, mivel nagyon hasznos (azaz kényelmesen és biztonságosan használható) eszköznek bizonyult. Ebben a nyelvben is lehet önállóan is használni, valamint a mutexekkel együtt monitort is könnyen lehet vele implementálni. Egy szinkronizációs feltétel lényegében várakozó szálak egy halmaza. Ehhez a halmazhoz bármelyik szál hozzáadhatja magát, vagy jelezheti, hogy a feltétel teljesült; ez utóbbi esetben a halmaz valamelyik eleme (azaz valamelyik, a feltételre váró szál) elkezd futni.

Egy ilyen feltételt a Thread.Condition típus egy példánya reprezentál. A feltételre a Thread.Wait(m: MUTEX, c: Condition eljárással lehet várakozni. Ehhez a híváshoz előbb le kell foglalni az m mutexet; a Wait hívás törli a lefoglalást, vár a feltételre, majd amikor tovább futhat, újra lefoglalja m-et. Ez a szemantika a monitorokban definiált wait műveletnek felel meg.

Egy feltétel teljesülését két módon is jelezhetjük. A Thread.Signal(c: Condition) eljárás felébreszt egy vagy több szálat a feltételre várakozók közül (ha egyáltalán van ilyen, mert különben nem csinál semmit). A Thread.Broadcast(c: Condition) hívás minden várakozó szálat felébreszt. Ezeknek a szemantikáját a könnyű implementáció érdekében definiálták így; a használatukat általában egy őrfeltételhez szokás kötni, ezzel együtt a nemdeterminisztikus működés ellenére jól használható eszközök.

Monitorok
A monitor fogalmát Hoare alkotta meg. Az eredeti koncepció szerint a monitor olyan eljárások gyűjteménye, amelyek közül egyszerre csak egy futhat; azt pedig, hogy milyen sorrendben fussanak, ill. várjanak egymásra, szinkronizációs feltételek segítségével határozták meg.

Itt annyiban finomították ezt, hogy egy mutex segítségével pontosan meg kell adni, hogy az eljárásokon belül melyik részek nem futhatnak egyszerre. Ez a kizárás finomításán keresztül a hatékonyabb kód készítését teszi lehetővé. Az egész kódot becsomagolhatjuk egy modulba, így kívülről nem lehet elrontani a működését; ha pedig a modul által kezelt adatokat egy objektumba rakjuk össze, akkor egy nagyon szép egységet kapunk:

TYPE Monitor = MUTEX OBJECT ... END;
Egy ilyen konstrukcióban a metódusok magát az objektumpéldányt használhatják a monitoron belüli kizárásra.

A szinkronizációs feltételek szemantikájának változása miatt ezeket általában egy őrfeltételhez kötik. Ennek az őrfeltételnek a teljesülését jelzi egy szál a Signal hívással; ha ekkor azonban több szál is elindul, akkor lehet, hogy nem mindnél teljesül a feltétel, ezért a Wait hívást egy ciklusba célszerű tenni, amely vizsgálja az őrfeltételt.