A Tcl programozási nyelv

Párhuzamosság



Folyamatok

exec ?kapcsolók? arg ?arg ...?
Az exec parancs egy parancs pipeline elemeinek alfolyamatként történő végrehajtását kezdeményezi. Az exec argumentumai a pipeline részét képező unix parancsokat és ezek kapcsolatát írják le. A pipeline megadásának szintaxisa a unix-shellekben megszokotthoz hasonló.

A pipeline parancsai szavakból állnak, minden argumentum pontosan egy szót ad meg. A parancs első szava a parancs neve. Ha a parancs neve nem ad meg könyvtárat, vagyis a tilda-helyettesítés elvégzése után nem tartalmaz '/' karaktert, az exec a PATH környezeti változóban felsorolt könyvtárakban elsőként megtalált megfelelő nevű fájlt hajtja végre.

A pipeline parancs argumentumai pontosan a parancs nevét követő szavak lesznek. Az exec parancs ezeken semmilyen helyettesítést nem végez. Ha mégis a shellben megszokott mintákkal szeretnénk fájlneveket paraméterként átadni, a glob parancsot kell alkalmaznunk. Ekkor azonban nem elég a glob parancsot parancshelyettesítésbe bújtatni, mert eredménye az exec és ezzel egyben a végrehajtandó parancs egyetlen argumentumává válna. Egyik lehetséges kiút az eval parancs használata.

A vezérlőargumentumok a pipeline parancsainak elválasztására és inputjuk és outputjuk irányítására szolgálnak, ezek nem kerülnek a végrehajtott programoknak átadásra. A paraméteres vezérlőargumentumok paramétere képezheti a vezérlőargumentum részét (például '>fájlnév'), de állhat a következő argumentumban is (például '> fájlnév').

| A pipeline két parancsát választja el. A megelőző parancs standard outputját a következő parancs standard inputjába irányítja.
|& A pipeline két parancsát választja el. A megelőző parancs standard és error outputját a következő parancs standard inputjába irányítja. Felülbírálja a '2>' és '>&' jellegű átirányításokat.
<fájlnév A megnevezett fájlt a pipeline első parancsának standard inputjába irányítja.
<@azonosító Az azonosítóval adott, olvasásra is megnyitott fájlt a pipeline első parancsának standard inputjába irányítja.
<<érték A megadott értéket a pipeline első parancsának standard inputjába irányítja.
>fájlnév A pipeline utolsó parancsának standard outputját a megadott fájlba irányítja, felülírva annak eredeti tartalmát.
>@azonosító A pipeline utolsó parancsának standard outputját az azonosítóval adott, írásra is megnyitott fájlba irányítja.
>>fájlnév A pipeline utolsó parancsának standard outputját a megadott fájl végéhez fűzi.
2>fájlnév A pipeline összes parancsának error outputját a megadott fájlba irányítja, felülírva annak eredeti tartalmát.
2>@azonosító A pipeline összes parancsának error outputját az azonosítóval adott, írásra is megnyitott fájlba irányítja.
2>>fájlnév A pipeline összes parancsának error outputját a megadott fájl végéhez fűzi.
>&fájlnév A pipeline utolsó parancsának standard outputját és összes parancsának error outputját a megadott fájlba irányítja, felülírva annak eredeti tartalmát.
>&@azonosító A pipeline utolsó parancsának standard outputját és összes parancsának error outputját az azonosítóval adott, írásra is megnyitott fájlba irányítja.
>>&fájlnév A pipeline utolsó parancsának standard outputját és összes parancsának error outputját a megadott fájl végéhez fűzi.
& Utolsó argumentumként adható meg, a pipeline aszinkron végrehajtását eredményezi.

Ha a pipeline első parancsának standard inputját nem adtuk meg külön, akkor pipeline a Tcl interpretert végrehajtó folyamat standard inputjáról fog olvasni.

Szinkron végrehajtás esetén (nem '&' argumentummal zártuk le a parancsot) az exec parancs megvárja a pipeline parancsainak befejeződését. Ha nem irányítottuk át, a pipeline utolsó parancsának standard outputja lesz a parancs visszatérési értéke.

Az exec parancs hibát jelez, ha a pipeline bármely eleme abnormálisan fejeződik be. A hibaüzenet a pipeline outputjából és az abnormális befejeződést leíró diagnózisból áll, az errorCode változó az utolsóként abnormálisan befejeződött paranccsal kapcsolatos információkat tartalmaz. Az exec parancs akkor is hibát ad vissza, ha a pipeline bármely eleme error outputjára ír, és az nincsen átirányítva. A hibaüzenet ekkor a standard outputot és a diagnózist követően az error outputot is megadja.

Akár normálisan, akár hibával fejeződik be a szinkron exec parancs, törli az eredmény utolsó karakterét, ha az újsor karakter.

Aszinkron végrehajtáskor az exec nem várja meg a pipeline végrehajtásának befejeződését, visszatérési értéke a pipeline elemeinek folyamat-azonosító számait tartalmazó lista. Az aszinkron futó pipeline parancsainak standard illetve error outputja - ha nem irányítottuk át - a Tcl interpretert futtató folyamat standard illetve error outputjára kerül.

Az exec parancs két kapcsolót támogat:

-keepnewline
Szinkron végrehajtás mellett nem törli az eredmény vagy hibaüzenet végéről az újsor karaktert.
--
A kapcsolók sorának végét jelzi. A következő argumentum még akkor sem számít kapcsolónak, ha kötőjellel kezdődik.

pid ?azonosító?
A pid parancsot meghívhatjuk egy argumentummal vagy argumentum nélkül. Az argumentum egy fájlazonosító lehet. Ha az azonosító az open paranccsal megnyitott pipeline azonosítója, a visszatérési érték a pipeline elemeinek folyamat-azonosító számát tartalmazó lista. Ha az azonosító nem pipeline azonosítója, a visszaadott lista üres.

Argumentum nélkül kiadva a pid parancs a Tcl interpretert végrehajtó folyamat-azonosító számát adja vissza.

exit ?kód?
Az exit parancs a Tcl interpretert futtató folyamatot a megadott visszatérési kóddal terminálja. A visszatérési kód alapértelmezése 0.

Szálak

Ez a fejezet a Tcl egyik fontos kiegészítéséről, a thread csomagról szól, amely lehetővé teszi szálak használatát egy Tcl szkriptben.

A szálkezelés rendkívül lényeges része egy programozási nyelvnek, hiszen enélkül bizonyos feladatokat csak nagyon körülményesen, vagy nem kielégítően lehet megoldani. Legegyszerűbb példa erre a grafikus kezelőfelület: egy-egy bonyolultabb folyamat elindításakor természetes elvárás, hogy amíg a gép dolgozik, a felhasználó tudjon dolgozni a programmal, vagy akár leállítsa az éppen futó folyamatot. Ehhez a programot párhuzamos folyamatokra kell bontani, amit szálakkal érhetünk el. Mivel a Tcl programozási nyelvet a Tk kiegészítés révén általában grafikus prototípusok készítésére használják, az ilyen problémák mindennaposak. Természetesen a helyzet megoldható az update paranccsal, illetve külső programok futtatásával és azok eredményeinek felhasználásával is, de egy ilyen gyakorta felmerülő kérdésre elegánsabb választ igényel a programozó világ - a szálakat.

A szálak használatára nincs felkészítve minden Tcl interpreter; fordításkor ezt általában egy parancssori argumentummal (vagy egy #define-al a Makefile-ban) lehet megkövetelni. Ha az interpreter vagy egy betöltött csomag nem szálbiztos, az könnyen a program hibás futásához illetve szegmentációs hibához vezethet.

A csomag letölthető a Tcl/Tk honlapjáról, betöltéséhez a forrásban a következő sornak kell szerepelnie:

package require Thread 2.1

ahol a "2.1" természetesen a megkövetelt verziószámnak megfelelően változtatandó.

A Tcl szálmodellje

A Tcl szálmodellje szerint minden szálhoz tartozik legalább egy Tcl interpreter, de minden egyes interpretert csak egy-egy szál használhat. Ennek az osztott adatkezelés megvalósításánál van jelentősége - a szálak biztonságos használata érdekében az adatokat vagy a szálakban lokálisan kell definiálni, vagy mutexekkel kell megvédeni (bővebben erről később lesz szó, a szálbiztosság kapcsán). Következésképp a Tcl inkább az olyan egyszerűbb szálas feladatokra alkalmas, amelyek munkáltató - dolgozó alapúak, azaz van egy fő szál, ami több mellékfolyamatot elindít és kommunikál velük, illetve vár, hogy azok válaszoljanak. A szálak kommunikációja az interpreterek eseménysorán (event queue) keresztül történik, ez lehet szimmetrikus vagy aszimmetrikus. Szimmetrikus esetben a hívó fél megvárja, hogy válasz érkezzen, aszimmetrikus esetben rögtön továbbmegy. A 8.4-es verziótól kezdve már I/O csatornákat is át lehet adni szálak közt.

A Thread csomag utasításai

Egyéb, szálakkal kapcsolatos utasítások

Szálbiztosság (thread-safe)

Ha egy Tcl szkriptben szálakat használunk, akkor minden, a szálakban használt utasításnak szálbiztosnak kell lennie, ellenkező esetben a program futása nem lesz biztonságos. A Tcl szkriptek prototípus-jellegük miatt legtöbbször C-ben írt saját könyvtárakat is betöltenek - ahhoz, hogy ezek szálbiztosak legyenek, használni kell a Thread kiegészítés C API-jait. Ehhez használhatóak mutexek, szinkronizálást elősegítő feltételváltozók (condition variables), valamint a szálakra nézve lokális adattárolás. Ezek a struktúrák mind automatikusan törlődnek, amikor a szál terminál, így nem kell velük külön törődni. Ha a lokális adatterületben kell felszabadítani memóriát, akkor ez egy szálspecifikus kilépéskezelővel (exit handler) tehető meg.

Egy régi forráskód szálbiztossá tételénél általában elég a globális adatstruktúrákra figyelni, és vagy mutexekkel korlátozni az ezekhez való hozzáférést, vagy a szálakra nézve lokális adatterületre átvinni őket.

Mutex változók
A TCL_DECLARE_MUTEX makró deklarál egy mutex változót, ha a szálhasználat engedélyezett, egyébként nem csinál semmit. Hasonlóan, a Tcl_MutexLock és Tcl_MutexUnlock makróknak is csak akkor van hatása, ha a szálhasználat engedélyezett (--enable-threads). Pl.:
TCL_DECLARE_MUTEX(mutexValtozo)

(...)

Tcl_MutexLock(&mutexValtozo);
(műveletek a közös adatokkal...)
Tcl_MutexUnlock(&mutexValtozo);
Lokális adattárolás
A lokális adattárolás a typedef struct ThreadSpecificData struktúra segítségével történik. Egy ilyen struktúra definiálásával és a TCL_TSD_INIT makró segítségével az adatok szálakra nézve lokálissá tehetők - ennek a módszernek a részletes tárgyalása (egyelőre) meghaladja e leírás kereteit, érdemes a Tcl/Tk forráskódban a ThreadSpecificData sztringre keresve példákat nézni hozzá.
Feltételváltozók (condition variables)
A feltételváltozók a mutexekkel vannak kapcsolatban: egy feltételváltozóra váráskor a mutex feloldódik, és ismét életbe lép, amikor a várásnak vége. Tipikusan két fajta használata van:
Tcl_MutexLock(&mutexValtozo);

while (a_feltétel_hamis) {
     Tcl_ConditionWait(&feltetelValtozo, &mutexValtozo, NULL /* nincs időkorlát */);
}

Tcl_MutexUnlock(&mutexValtozo);
illetve:
Tcl_MutexLock(&mutexValtozo);
/* közös/osztott állapot beállítása */
Tcl_ConditionNotify(&feltetelValtozo);
Tcl_MutexUnlock(&mutexValtozo);

A Tcl szálbiztos kiegészítései

Néhány szálbiztos könyvtár a Tcl-hez:

És végül egy rendszeresen frissített lista a szálbiztos C könyvtárakról.

Példák szálak használatára

Egy egyszerű példa párhuzamosításra:

 package require Thread 2.1

 #
 # Új szál indítása. A visszatérési értéket (a szál azonosítóját)
 # berakjuk a threadID változóba, hogy később hivatkozni tudjunk rá.
 #
 set threadID [thread::create {
    #
    # Innentől kezdve a thread::wait utasításig (amivel a szál belép az
    # eseményciklusba) definiáljuk azokat az eljárásokat, amiket majd a
    # főprogram meg fog hívni.
    #
    # Ebben a példában egy egyszerű kis eljárás van csak, ami lefuttatja
    # a parancsot amit átadtak neki. Ez a parancs lehet akár egy hosszan
    # futó Tcl utasítás, vagy éppen egy külső program is.
    #
    # Az eredményt visszaküldi az ID azonosítóval meghatározott szálnak,
    # a globális printResult eljárás segítségével.
    #
    # A thread::wait utasításra azért van szükség, hogy a szál ne fejezze
    # be a működését és lépjen ki, hanem maradjon benn az eseményciklusban.
    #
    proc runCommand { ID command } {
        set result [eval $command]
        eval [subst {thread::send -async $ID \
                {::printResult [list $result]}}]
    }
    thread::wait
 }]

 #
 # Ez az eljárás hívódik meg, amikor a szál befejezte a parancs futtatását.
 #
 proc printResult { result } {
    puts $result
    exit
 }

 #
 # Kiírja az aktuális időt és beütemezi önmagát egy másodperc múlvára.
 #
 proc passTheTime { } {
    puts [clock format [clock seconds]]
    after 1000 passTheTime
 }

 #
 # Ez itt egy általános utasítás külső program futtatására, csak az 
 # exec utáni részt kell lecserélni valami másra.
 #
 set commandString "exec du -sk /usr/local"
 eval [subst {thread::send -async $threadID \
        {runCommand 1 [list $commandString ]}}]

 #
 # Maga a program csak kiírogatja az időt és vár az örökkévalóságig.
 # Ezzel megmutatjuk, hogy miközben a szál dolgozik, a főprogram él.
 #
 passTheTime
 vwait forever