Párhuzamos algoritmusok

Dr. Olajos, Péter

Új Széchenyi Terv logó.

Miskolci Egyetem

Kelet-Magyarországi Informatika Tananyag Tárház

Kivonat

Nemzeti Fejlesztési Ügynökség http://ujszechenyiterv.gov.hu/ 06 40 638-638

Lektor

Póta József

Dobó István Gimnázium, Eger, középiskolai tanár

A tananyagfejlesztés az Európai Unió támogatásával és az Európai Szociális Alap társfinanszírozásával a TÁMOP-4.1.2-08/1/A-2009-0046 számú Kelet-Magyarországi Informatika Tananyag Tárház projekt keretében valósult meg.


Tartalom

1. Alapfogalmak
1.1. Bevezetés
1.2. Párhuzamossági alapfogalmak
1.3. Multiprocessors (megosztott memóriájú) architektúra
1.4. Multicomputers (üzenet átadó) architektúra
1.5. Párhuzamos programozás
2. Multipascal
2.1. Bevezetés
2.2. A multi-Pascal nyelv
2.2.1. Bevezetés a Multi-Pascal használatába
2.2.2. A FORALL utasítás
2.2.3. A processz granularitása (felbontási finomsága)
2.2.4. Egymásba ágyazott ciklusok (Nested loops)
2.2.5. Mátrixok szorzása (Matrix multiplication)
2.2.6. Megosztott és lokális változók
2.2.7. Utasításblokkok deklarációval
2.2.8. A FORALL indexek hatásköre (scope)
2.2.9. A FORK utasítás
2.2.10. Processz terminálás
2.2.11. A JOIN utasítás
2.2.12. Amdahl törvénye
2.3. Mintafeladatok
2.4. A multiprocessors architektúra
2.4.1. Busz orientált rendszerek
2.4.2. Cache memória
2.4.3. Többszörös memória modulok
2.4.4. Processzor-memória kapcsolóhálózatok
2.5. Processz kommunikáció
2.5.1. Processz kommunikációs csatornák
2.5.2. Csatorna változók
2.5.3. Pipeline párhuzamosítás
2.5.4. Fibonacci-sorozat elemeinek meghatározása
2.5.5. Lineáris egyenletrendszerek megoldása
2.5.6. Csatornák és struktúrált típusok
2.6. Adatmegosztás
2.6.1. A spinlock
2.6.2. A contention jelenség osztott adatoknál
2.6.3. Spinlockok és Channelek összehasonlítása
2.7. Szinkronizált párhuzamosság
2.7.1. Broadcasting és aggregálás
3. PVM
3.1. Elosztott rendszerek
3.2. A PVM bevezetése
3.2.1. A PVM rendszer szerkezete
3.2.2. A PVM konfigurációja
3.2.3. A PVM elindítása
3.3. PVM szolgáltatások C nyelvű interfésze
3.3.1. Alapvető PVM parancsok
3.3.2. Kommunikáció
3.3.3. Hibaüzenetek
3.3.4. Buffer(de)allokálás
3.3.5. Adatátvitel (küldés, fogadás)
3.3.6. Processz-csoportok
3.4. Példaprogramok
3.4.1. Programok futtatása
3.4.2. Hello
3.4.3. Forkjoin
3.4.4. Belső szorzat (Fortran)
3.4.5. Meghibásodás (failure)
3.4.6. Mátrixok szorzása (mmult)
3.4.7. Maximum keresése (maxker)
3.4.8. Egy dimenziós hővezetési egyenlet
4. MPI
4.1. Bevezetés
4.2. Történet és Fejlődés
4.2.1. Miért használjunk MPI-t
4.3. Az MPI használata
4.4. Környezet menedzsment rutinok
4.4.1. Parancsok
4.4.2. Példa: környezet menedzsment rutinok
4.5. Pont-pont kommunikációs rutinok
4.5.1. Pont-pont műveletek típusai
4.5.2. Pufferelés
4.5.3. Blokkoló kontra nem-blokkoló
4.5.4. Sorrend és kiegyenlítettség
4.5.5. MPI üzenetátadó rutinok argumentumai
4.5.6. Blokkoló üzenetátadó függvények
4.5.7. Példa: blokkoló üzenet küldő rutinok
4.5.8. Nem-blokkoló üzenetátadó rutinok
4.5.9. Példa: nem-blokkoló üzenet átadó rutinokra
4.6. Kollektív kommunikációs rutinok
4.6.1. Minden vagy semmi
4.6.2. Rutinok
4.6.3. Példa: kollektív kommunikációs függvények
4.7. Származtatott adattípusok
4.7.1. Származtatott adattípusok függvényei
4.7.2. Példák: származtatott adattípusok
4.8. Csoport és kommunikátor kezelő rutinok
4.8.1. Csoportok kontra kommunikátorok
4.9. Virtuális topológiák
4.10. Példaprogramok
4.10.1. MPI array (mpi_array.c)
4.10.2. Hővezetés egyenlete (mpi_heat2D.c)
4.10.3. Hullámegyenlet (mpi_wave.c)
4.10.4. Prímgenerálás (mpi_prime.c)
5. JCluster
5.1. Bevezetés
5.1.1. Telepítés, futtatás
5.1.2. Alkalmazások indítása a Jclusterben
5.2. Alkalmazások készítése
5.2.1. PVM típusú alkalmazások
5.2.2. MPI típusú alkalmazások
5.2.3. „Hello World” program
5.3. Példaprogramok
5.3.1. RankSort (Multi-Pascal - Jcluster)
5.3.2. Mmult (Multi-Pascal - Jcluster)
5.3.3. Mmult2 (PVM - Jcluster)
Irodalomjegyzék

Az ábrák listája

1.1. Multiprocessors architektúra közös busszal.
1.2. Megosztott memória.
1.3. Multicomputers (üzenet átadó) architektúra.
2.1. Párhuzamos Ranksort algoritmus.
2.2. Processz létrehozása FORALL utasítással.
2.3. Egymásba ágyazott FORALL ciklusok.
2.4. Mátrixszorzás sémája.
2.5. A javított Ranksort program által adott helyes eredmény.
2.6. FORK utasítás hatása
2.7. Busz orientált rendszer.
2.8. Cache memória 1 processzoros gépen.
2.9. Cache memória multiprocessors-os gépen.
2.10. Szeparált modulok.
2.11. Lebegőpontos szorzás egyszerűsített pipeline algoritmusa.
2.12. A termelő-fogyasztó program.

1. fejezet - Alapfogalmak

1.1. Bevezetés

A jegyzet célja, hogy ismertesse a párhuzamos algoritmusok és eljárások implementálási lehetőségeit ismert problémákon keresztül. Így valós képet kaphatunk több párhuzamos programozási eszköz/nyelv esetén annak futásáról és függvényeiről, melyek kiemelt szerepet kap(hat)nak egy-egy példaprogram során. Mivel a felhasznált programnyelvek és a hozzá kapcsolódó dokumentációk alapvetően angol nyelven érhetőek el, ezért sok esetben megadásra kerülnek a felhasznált kifejezések angol megfelelői is (zárójelben).

Amennyiben a kedves olvasó „elméleti” (azaz pszeudókóddal megadott) algoritmusokról szeretne olvasni, akkor érdemes elolvasni az Iványi Antal által elkészített Párhuzamos algoritmusok című könyvet (lásd az [3]-t).

Az algoritmusok esetén figyelni kell azok erőforrás használatára (pl. memória) és a lépésszámra, adott esetben össze is kell ezek alapján hasonlítani különböző technikák vagy algoritmusok alapján készült programokat. Ezeket az összehasonlításokat így megfelelő módon definiált függvények segítségével adhatjuk meg.

Ha és két, természetes számokon értelmezett valós (esetleg komplex) értékű függvény, akkor azt mondjuk, hogy

  • (azaz egyenlő nagy ordó -vel), ha és , hogy esetén ,

  • (azaz f egyenlő kis ordó -vel), ha csak véges sok helyen nulla, és , ha ,

  • , ha (az inverze),

  • , ha és , azaz és , hogy .

A fenti bonyolultsági jelölések felhasználásával tekintsük a következő két példát:

1.1. Példa. Mutassuk meg, hogy !

Megoldás: Tekintsük a következőket:

ahol . Teljesült a feltétele, azaz az állítás bizonyítva van.

További észrevételként megjegyezhető, hogy mivel (legyen pl. ), ezért az állítás az alakban is felírható.

1.2. Példa. Igaz-e az az állítás, hogy ?

Lehet-e tovább élesíteni ezt az állítást?

Megoldás: Mivel

ezért megfelelő valós szám esetén a fenti kifejezésre teljesül, hogy -nal egyenlő. Tovább lehet élesíteni ezt, hiszen a lináris tagot a négyzetes taggal lehet becsülni, azaz teljesül az

1.1. Megjegyzés. Észrevehető, hogy , ahol valós szám, de fordítva nem igaz, hiszen pl. (azaz nem szimmetrikus).

1.2. Párhuzamossági alapfogalmak

A szekvenciális algoritmusok az emberi természet és viselkedés következményei. Hiszen pl. minden nap felkel a nap, majd lenyugszik egymást követően. Ennek köszönhetően az első számítógépek, programok, algoritmusok is a szekvenciális jelleget hordozták magukban. Viszont az utóbbi évtized(ek)ben megfigyelhető volt az, hogy a szekvenciális jelleg a történetnek csak egy része. Ugyanis az emberi tevékenységek és természeti törvények nemcsak szekvenciálisan (sorosan), hanem párhuzamosan is megjelenhetnek (pl. egyidőben mindenhol). Alapvetően tehát a következő kérdéseket tehetjük fel magunknak.

Kérdések:

  • Miért kell a párhuzamosság, a párhuzamos algoritmus?

  • Szekvenciális gépek a Neumann-elv szerint működnek. Mi a helyzet a párhuzamos gépekkel?

A párhuzamos gépek alapelve: sok processzor legyen ugyanabban a számítógépben, amelyek (egyidejűleg) szimultán tudnak dolgozni.

Követelmények: A processzorok képesek legyenek adatokat megosztani és kommunikálni egymással.

Jelenleg két fő architektúra típus/közelítés ismert:

  • Megosztott memória (shared memory),

  • Üzenet átadás (message passing).

A megosztott memóriájú számítógépekben, melyeket rendszerint multiprocessors-nak is neveznek az individuális processzorok

  • hozzáférnek egy közös megosztott memóriához,

  • a memóriában elhelyezett adatokat megosztva közösen használják.

Az üzenet átadó számítógépekben, melyeket általánosan multicomputers-nek neveznek

  • minden processzornak saját memóriája van,

  • a processzorok az adatokat egy kommunikációs hálózaton keresztül osztják meg egymással.

1.3. Multiprocessors (megosztott memóriájú) architektúra

A multiprocessors-nak megfelelő gép szervezését az alábbi ábra mutatja:

1.1. ábra - Multiprocessors architektúra közös busszal.

Multiprocessors architektúra közös busszal.


A processzorok mindegyike párhuzamosan számolhat és elérheti a központi megosztott memóriát a buszon keresztül. Mindegyik processzor olvashat a memórából, számolhat és visszaírhatja a megosztott memóriába. A közös busznak biztosítania kell, hogy az összes processzor ki legyen szolgálva egy minimum időn belül.

A párhuzamosan dolgozó individuális processzorok tevékenysége hasonló az egyprocesszoros szekvenciális gépekéhez. Ha tehát processzorunk van, akkor a rendszer maximális számítási kapacitása az egyprocesszoros gép -szerese.

Tipikus processzor szám: .

A megosztott memórájú processzorok egyik fő problémája a processzorok un. „memory contention”-ja (azaz memória küzdelme), amely a processzorok számának növelésével együtt fokozódik.

Jelentése: A közös memória nem képes sok igényt egyszerre kielégíteni és a processzoroknak várni kell.

Számos ötlet adható megoldásként:

  • Minden egyes processzor rendelkezzen helyi cache memóriával, melyek a közös memória megfelelő adatainak egy-egy másolatát tartalmazzák. Ez viszont cache koherencia problémához (cache coherence problem) vezethet, hiszen így számos másolatot találhatunk egy-egy helyi memóriában, melyek már elévültek. Ehhez szolgáltat megoldást a snooping cache, amely figyeli a változásokat és ha szükséges érvényteleníti azokat az adatokat, melyeket más processzorok már módosítottak.

  • A közös memória megosztása szeparált modulokra, amelyeket az egyes processzorok párhuzamosan érhetnek el. Ezzel az egyidejű igény fellépésének a valószínűségét próbálják csökkenteni. A megoldás sémája a következő:

    1.2. ábra - Megosztott memória.

    Megosztott memória.


    Az processzor mindegyike elérheti az memória modul mindegyikét a processzor-memória kapcsoló hálózaton keresztül. Ez a megoldás növeli a multiprocessors hatékonyságát.

Minthogy minden processzor minden memóriamodulhoz hozzáférhet, programozási szempontból a két modell azonos.

1.4. Multicomputers (üzenet átadó) architektúra

A számítógép szerkezetét a következő ábra mutatja:

1.3. ábra - Multicomputers (üzenet átadó) architektúra.

Multicomputers (üzenet átadó) architektúra.


Minden egyes processzornak saját memóriája van és minden egyes processzor kommunikálhat az üzenetátadó (közvetítő) kommunikációs hálózaton keresztül.

Minden egyes processzor úgy viselkedik ment egy egyprocesszoros gép. A processzor csak a saját memóriáját érheti el közvetlenül. Más processzoroknak csak üzeneteket küldhet. Emiatt más programozási koncepció/modell szükséges mint az előbbi architektúránál. Alapvető cél a kommunikációs hálózat topológiájával szemben, hogy a kis költséggel (pl. időben) valósítsa meg a processzorok egymás közötti üzenetváltásait.

Kommunikációs hálózatok topológiája sokféle lehet (hiperkocka, háló, tórusz, gyűrű, stb.). A probléma az, hogy a teljes gráf (minden processzor össze van kötve minden processzorral) sokba kerülne, hiszen ez kommunikációs utat jelentene processzor esetén.

A két architektúrát összehasonlítva a multiprocessors esetben könnyebb a programozás, mivel közelebb áll az egyprocesszoros gép modelljéhez. Viszont a multicomputers esetben a programozást nagyban megnehezítheti az előre kiválaszott kommunikációs hálózat topológiája.

1.5. Párhuzamos programozás

Az új architektúrák miatt fellépő párhuzamos processzorok változásokat igényeltek a programozásban is. Ennek érdekében a következő elemeknek kell megjelennie a párhuzamos programozás során:

Szükséges elemek:

  • korábban algoritmus, most párhuzamos algoritmus

  • korábban program nyelv, most párhuzamos programnyelv

Fogalmi eszközök:

Legfontosabb eszköz a processz.

Processz „definíciói”:

1.2. Definíció. A processzt tekinthetjük

  • utasítások sorozatának, amely végrehajtható, vagy

  • informálisan felfogható mint egy szubrutin, vagy eljárás, amely végrehajtható bármely processzoron vagy

  • röviden számítási aktivitás.

Párhuzamos nyelvek követelményei:

  1. Processz definiálása és létrehozása.

  2. Adatok megosztása párhuzamos processzek között.

  3. A processzek szinkronizálása.

A párhuzamos programozás főbb technikái:

  1. Adatpárhuzamosítás (Data parallelism): Hasonló vagy azonos eljárások alkalmazása/futtatása párhuzamosan nagy mennyiségű, de különböző adatokon. Leggyakrabban a numerikus számolások esetén alkalmazható. A legjobb eredményt (futási időben) ebben az esetben érhetjük el, ugyanis nincs szükség a processzorok közötti szinkronizációra (,de kommunikáció lehetséges).

  2. Adat felosztás (Data partitioning): Az adatpárhuzamosítás egy olyan fajtája, melynél az adatterület szomszédos részekre van felosztva (azaz partícionálva), és mindegyik részen párhuzamosan működik egy-egy processzor. Természetesen szükség lehet adatcserére a szomszédos részek között. Ez tipikusan a multicomputers architektúra egyik megvalósítási lehetősége, hiszen ebben az architektúrában minden egyes processzor eleve saját adatterülettel rendelkezik.

  3. Relaxált algoritmus (Relaxed algorithm): Minden egyes processz önálló módon számol, nincs szinkronizáció vagy kommunikáció a processzek között. Mindkét architektúrán jól működik és kis futási időket produkál a szekvenciális esethez képest.

  4. Szinkronizált iteráció (Synchronous iteration): Minden egyes processzor ugyanazt az iterációt/számítást végzi el az adatok egy részén, azonban az iterácó végén szinkronizációra van szükség. Azaz egy processzor nem kezdheti el a következő iterációt mindaddig, amíg az összes többi be nem fejezi az előzőt. Ez a multiprocessors architektúrán működik hatékonyan, hiszen ekkor a processzorok közötti szinkronizáció rövid, míg a másik architektúra esetén ez jelentősen befolyásolhatja a futási időt.

  5. Lemásolt dolgozók (Replicated workers): Hasonló számítási feladatok központi tárolóban való kezelése. Ekkor a nagy számban levő „dolgozó” processzek először kiveszik a feladatot a tárolóból, majd végrehajtják azt. Adott esetben újabb feladatot helyezhetnek el a tárolóban. A teljes számolás akkor ér véget, ha a tároló üres. Ezt a technikát gyakran alkalmazzák kombinatorikai problémáknál vagy gráfkeresésnél. Ezeknél a problémáknál nagy számú, dinamikusan generált kis számításokkal írják le a teljes számítási folyamatot.

  6. Pipeline számítás (Pipeline computation): A processzek ekkor gyűrűbe vagy kétdimenziós hálóba vannak rendezve. Az adat végigmegy a teljes struktúrán, miközben minden egyes processz újabb módosítást végezhet rajta.

A párhuzamos programozás problémái, melyek befolyásolják a párhuzamos program futási idejét:

  1. Memória küzdelem (Memory contention): A processzor várakozik egy memória cellára, mivel azt egy másik processzor használja.

  2. Kimerítő szekvenciális kód (Excessive sequential code): Minden párhuzamos algoritmusban lehet tisztán szekvenciális kód pl. inicializáció. Ennek nagy mennyisége jelentős mértékben befolyásolja a futási időt.

  3. Processz létrehozási ideje (Process creation time): Minden valós rendszerben a processz(ek) létrehozása időt igényel. Ha számítási idő kisebb vagy összemérhető a létrehozási idővel, akkor ez jelentősen megnövelheti a párhuzamos program végrehajtási idejét.

  4. Kommunikációs késedelem (Communication delay): Ez a probléma alapvetően a multicomputers architektúrában fordul elő, mivel a processzoroknak kommunikálni kell egymással a hálózaton keresztül.

  5. Szinkronizációs késedelem (Synchronization delay): A processzek szinkronizációja azt jelenti, hogy egy adott ponton a processznek várakoznia kell a többire, amely jelentős növekedést eredményez a futási időben.

  6. Terhelési kiegyensúlyozatlanság (Load imbalance): Vannak olyan párhuzamos programok, melyeknél az egyes generált részfeladatok jelentős különbséget mutatnak, azaz könnyen előfordulhat, hogy egy-egy processzor miatt kell várakozni, miközben a többi már befejezte a számolást.

Feladatok:

1. Bizonyítsa be, hogy a négyzetszámok összegére teljesül az alábbi összefüggés:

2. Bizonyítsa be, hogy a köbszámok összegére teljesül az alábbi összefüggés:

3. Bizonyítsa be, hogy a természetes számok -dik hatványösszegére is hasonló összefüggés teljesül, mint az előző két példában, azaz

2. fejezet - Multipascal

2.1. Bevezetés

2.1. Példa. Ranksort: Adott db természetes szám. Definiáljuk egy szám rangját: minden szám rangja egyenlő a nála kisebb, vagy egyenlő elemek számával. Egy szám rangját úgy határozhatjuk meg, hogy összehasonlítjuk az összes elemmel és megszámoljuk a tőle elemek számát. Ezután a számot egy új listába, a rangjának megfelelő helyre tesszük.

A párhuzamosítás alapja az, hogy minden szám rangja egymástól függetlenül határozható meg, azaz tisztán valósul meg az adatpárhuzamosítás és a relaxált algoritmus technika. Ugyanis az i-edik processz veszi a LIST[i] elemet, meghatározza a RANK[i] rangját és beírja a rangnak megfelelő helyre a SORTED tömbbe.

Az eljárás vázlatát a következő ábrán figyelhetjük meg a multiprocessors architektúra esetén:

2.1. ábra - Párhuzamos Ranksort algoritmus.

Párhuzamos Ranksort algoritmus.


Itt minden processzor egyedül dolgozik és kommunikál a megosztott közös memóriával. Az eljárás végén a rendezett SORTED tömböt kapjuk.

Ekkor a párhuzamos programok hatékonyságának mérése két adattal valósítható meg:

  1. (párhuzamos) futási idő,

  2. gyorsítási faktor (speed up).

A speed up definíciója a következő:

2.1. Definíció. Egy számítás/feladat elvégzésekor annak gyorsítási faktora a következőképpen határozható meg:

2.2. A multi-Pascal nyelv

2.2.1. Bevezetés a Multi-Pascal használatába

A párhuzamos programozás alapelveinek és lehetőségeinek megismerésében egy a Bruce P. Lester (lásd még a [1]-ben) által elkészített programnyelv segít bennünket. Ennek a neve: Multi-Pascal nyelv. Már a nevéből is kiderül, hogy a népszerű Pascal programnyelv „kibővítése”, hogy lehetőségünk nyíljon párhuzamos processzek létrehozására és iterációjára. Ennek segítségével mindkét architektúrában reprezentálhatunk párhuzamos algoritmusokat. Ez a program/programnyelv kiválóan alkalmas arra, hogy a legfontosabb párhuzamos technikákat implementált (kipróbálható, futtatható) formában ismerjük meg. Ezért a további fejezetekben az adott környezet vagy rendszer esetén nem térünk ki az eljárások/függvények teljes részletességgel való bemutatására, hiszen azok elméleti háttere az első két fejezetben megtalálható.

A programnyelv természetesen a megszokott módon működik, azaz változókkal (variables) és eljárásokkal (procedures) készíthetjük el a programjainkat.

A programnyelv mellett készült egy MS-DOS alatt futó gépfüggetlen Multi-Pascal szimulátor, amellyel a párhuzamos architektúrákat modellezhetjük ill. futtathatjuk az elkészült párhuzamos programjainkat..

A Multi-Pascal programnyelvben a párhuzamos architektúrák és nyelvi megfelelőik a következők:

A Multi-Pascalban rendelkezésre álló további lehetőségek:

  • Processz szinkronizálás.

  • Debugger.

  • Performance mérés.

A program indítása DOS ablakban (min 450 KB szabad terület kell):

multi, majd a Multi-Pascal rendszer elindul. A következő üzenetet kapjuk az ablakban:

Program File Name:

Itt kell megadnunk pl. a programunk nevét is. Például:

Program File Name: file.prg/list=file.lis/input=file.inp/output=file.out,

ahol a file.prg a programunk neve és a / karakter segítségével 3 további opciót is megadhatunk. A /list=file.list rész eredményeképpen a program a file.list állományba lesz kilistázva. Ha ez elmarad, akor a képernyőn láthatjuk ugyanezt. Alaphelyzetben a standard be/kiviteli (input-output) eszközökkel rendelkezünk, azaz a billentyűzettel (in) és a képernyővel (out). Viszont a mi esetünkben ezt külön állomány segítségével is megvalósíthatjuk. Erre szolgálnak az /input=file.inp és az /output=file.out opciók. Ekkor minden olvasás és írás a megadott állományokban zajlik.

A program és az opciók megadása után a program lefordításra kerül és átesik egy szintaktikai elllenőrzésen is. Ha valamilyen probléma merül fel, arról a képernyőn kapunk információt. Például a következő hibát jelenti a TOO BIG hiba kód: a szám túl sok jeggyel rendelkezik vagy a kitevő túl nagy (lásd a továbbiakhoz még [1]). Ha minden rendben van, akkor visszakapjuk a * promptot és jöhet a program elindítása, tesztelése.

Számos paranccsal rendelkezünk, hogy teszteljük a programunkat és adott esetben izoláljuk a felmerülő hibákat:

  • * RUN: A program elindítása.

  • * EXIT: A Multi-Pascal interaktív rendszer terminálása.

  • *LIST n:m: Listázza a program forrását az n-diktől m-dik sorokig a képernyőn.

  • *BREAK n: Egy töréspont (breakpoint) elhelyezése az n. sorba. Ekkor a program futása felfüggesztésre kerül, amint az összes processz végrehajtja ezt a sort.

  • *CLEAR BREAK n: Törli a töréspontot az n. sorból.

  • *CONT: Folytatja a program futását egy töréspont esetén.

  • * WRITE p name: A parancs kiírja a „name” változó értékét a „p” processz esetén a program futása közben.

  • *TRACE p name: Ha a program futása során bármely processz olvassa vagy írja a „name” változót, akkor a program futása felfüggesztésre került. Azaz ezen parancs hatására a „name” változó un. trace változó lesz.

  • *DISPLAY: Megjelennek a képernyőn a töréspontok és a trace változók.

  • *STATUS p: A „p” processz aktuális információit tudhatjuk meg vele, pl. fut-e még.

  • *TIME: Megadja az eltelt időt.

A rendszer érzékeny a kis és nagy betűkre és bizonyos hivatkozási Pascal utasítások nincsenek meg benne (pl. véletlenszám generálás). A sikeres program futás után minden esetben megkapjuk a szekvenciális, párhuzamos futási időket és a hányadosukat, azaz a speed-up-ot is.

2.2.2. A FORALL utasítás

Multi-Pascal egyik adatpárhuzamosítási lehetősége a FORALL utasítás. Ennek segítségével hozhatjuk létre a párhuzamos processzeinket. Mivel a program maga is egy processz, így őt hívjuk első processznek, míg a többi a kreált processz nevet is viselheti, vagy használhatjuk a szülő-gyerek processz elnevezéseket is. A FORALL parancs a következő szintaxissal rendelkezik:

FORALL <induló változó>:=<kezdeti érték> TO <végső érték> [GROUPING <méret>] DO<utasítás>;

Mielőtt még értelmeznénk a FORALL működését vizsgáljuk meg a következő Multi-Pascal programhoz mellékelt párhuzamos Ranksort eljárást (lásd még a 2.1. ábrát):

  PROGRAM Ranksort; 
  CONST n=10; 
  VAR values, final: ARRAY [1..n] OF INTEGER; 
  i: integer; 
  PROCEDURE PutinPlace( src: INTEGER); 
  VAR testval, j, rank: INTEGER; 
  BEGIN 
    testval :=values[src]; 
    j :=src; (*j megy végig a teljes tömbön*) 
    rank :=0; 
    REPEAT 
       j :=j MOD n+1; 
       IF testval >= values[j] THEN rank:=rank+1;  
    UNTIL j=src; 
    final[rank] :=testval; (*az érték a rangnak megfelelő helyre kerül*) 
  END; 
  BEGIN 
  FOR i :=1 TO n DO 
    Readln( values[i]); (*a rendezendő értékek beolvasása*) 
  FORALL i :=1 TO n DO 
    PutinPlace(i); (*a values[i] rangjának meghatározása és elhelyezése*) 
  FOR i:=1 TO n DO 
    Writeln(final[i]); 
  END. 

A program működését a következő ábra mutatja:

A programot az input adatokkal futtatva a következő eredményt kapjuk (listában, lásd még ranksort1.mp.swf):

Az eredmény hibás, mert a kétszer előforduló 2 szám rendezésénél hiba van (0-át ír be az egyik 2 helyett). Ha pl. az első 2-est 12-re változtatjuk, akkor az eredmény a következő (lásd még ranksort2.mp.swf):

A program jól működik, ha minden szám különbözik. Hogyan tudnánk javítani ezen (lásd a 2.5. ábrát)?

Ehhez vizsgáljuk meg a Multi-Pascal FORALL utasítását! A FORALL a ciklusmagjában szereplő utasítást (processzt) sokszorozza meg és azok szimultán (párhuzamos végrehajtását) okozza. Nézzünk erre egy egyszerű példát!

2.2. Példa. Példa processz létrehozására (Sqroot program).

Tekintsük a következő szekvenciális programot, amely egy tömb minden elemének négyzetgyökét számolja ki:

  PROGRAM Sqroot; 
  VAR A: ARRAY [1..100] OF REAL; 
    i: INTEGER; 
  BEGIN  
  FOR i :=1 TO 100 DO 
    Readln(A[i]); 
  FOR i:=1 TO 100 DO 
    A[i]:=SQRT(A[i]); 
  FOR i:=1 TO 100 DO 
    Writeln(A[i]); 
  END. 

A FORALL utasítással a 100 tömb elemen párhuzamosan dolgozó 100 processzt definiálhatunk:

  PROGRAM ParallelSqroot; 
  VAR A: ARRAY [1..100] OF REAL; 
    i: INTEGER; 
  BEGIN 
  FOR i :=1 TO 100 DO 
    Readln(A[i]); 
  FORALL i:=1 TO 100 DO 
    A[i]:=SQRT(A[i]); 
  FOR i:=1 TO 100 DO 
  Writeln(A[i]); 
  END. 

A FORALL utasítás létrehozza a ciklusmag 100 másolatát az ő egyedi i indexváltozó értékükkel:

2.2. ábra - Processz létrehozása FORALL utasítással.

Processz létrehozása FORALL utasítással.


Standard Pascal ciklusban az i értéke a ciklusmag minden egyes végrehajtásánál 1-el növekszik.

Multi-Pascalban minden egyes processzor megkapja az i index lokális másolatát a megfelelő i értékekkel.

2.2.3. A processz granularitása (felbontási finomsága)

Egy processz végrehajtási idejét a processz granularitásának nevezik.

Alapprobléma: A processz létrehozása és egy processzorra történő allokációja időbe telik. A processz végrehajtási ideje (granularitása) nagyobb kell, hogy legyen mint ez az idő.

Tekintsük az előző példát! A fő program a 0. processzoron létrehozza és allokálja a processzt. Tegyük fel, hogy ez 10 időegység processzenként. Azt is tegyük fel, hogy egy processz 10 időegység alatt hajtódik végre.

Ekkor a idő egység.

Ha a szekvenciális programot nézzük, akkor ugyanez: .

Ez jobb mint a párhuzamos verzió.

Ha most olyan processzt párhuzamosítunk a FORALL utasítással, amelynek végrehajtási ideje, mondjuk 10000 időegység, akkor a időegység. Ezzel szemben a szekvenciális program futási ideje: időegység. Ekkor az elérhető , ami elég jelentős.

A most vázolt probléma megoldása az eredeti feladat szétvágása kisebb (de nem atomi) részekre. Ez is tulajdonképpen egyfajta granuláció.

Ekkor Multi-Pascal eszközként ismét a FORALL utasítás használható:

FORALL utasítás GROUPING m

Ez a csoportosítás lehetősége.

A GROUPING m utasításrész azt okozza, hogy a FORALL index n elemű tartományát hézagmentesen m elemszámú csoportokra bontja:

Ha az utolsó csoportba kevesebb elem jut (), akkor a ciklusmagot ezzel a kevesebb indexszel hajtja végre.

Ha nincs GROUPING rész, akkor 1 elemű csoportokat képez a FORALL.

Tekintsük az előző négyzetgyök számítás példát különböző G értékekre:

  PROGRAM ParallelSqrootGrouping; 
  VAR A: ARRAY [1..100] OF REAL; 
    i: INTEGER; 
  BEGIN 
  FOR i :=1 TO 100 DO 
    Readln(A[i]); 
  FORALL i:=1 TO 100 GROUPING G DO 
    A[i]:=SQRT(A[i]); 
  FOR i:=1 TO 100 DO 
    Writeln(A[i]); 
  END. 

A következő táblázat tartalmazza az eredményeket:

A kapott eredményeket grafikusan is ábrázoljuk:

A táblázat, ill. grafikonok alapján úgy tűnik, hogy az optimális csoportméret .

Az előző példa alapján magyarázzuk meg, hogy miért ez az érték adódik! Legyen

  1. a FORALL indexek száma,

  2. a csoportméret,

  3. az egyes processzek létrehozási ideje,

  4. a FORALL ciklusmag végrehajtási ideje.

A létrehozott gyermek processzek száma: .

Ennek időigénye: .

Minden egyes létrehozott processz futásideje: .

Teljes futási idő:

Vegyük észre (használva a számtani-mértani közép közötti összefüggést: , egyenlőség akkor és csak akkor ha ), hogy

ahol az egyenlőség csak a

esetben érhető el. Tehát ez az optimális csoportérték.

2.2.4. Egymásba ágyazott ciklusok (Nested loops)

A FORALL utasítások egymásba ágyazhatók és ezáltal a párhuzamosítás hatékonysága növelhető.

Tekintsük a következő példát, amelyben két mátrixot adunk össze:

  PROGRAM SumArrays1; 
  VAR i,j : integer; 
    A, B, C: ARRAY [1..20,1..30] OF REAL; 
  BEGIN 
  ... 
  FORALL i:=1 TO 20 DO 
    FORALL j:=1 TO 30 DO 
       C[i,j]:=A[i,j]+B[i,j]; 
  ... 
  END. 

A példában 600 párhuzamos processz kerül generálásra. A külső FORALL ciklus 20 (gyermek) processzt generál, minden i értékre egyet. Minden ilyen i processz további 30 párhuzamos processzt generál, tehát összesen 600 (unoka) processzünk van.

Tehát a processzek generálása két generációban (szinten) történik:

  1. A főprogram létrehozza a külső FORALL gyermek processzeket (első szint).

  2. A külső FORALL gyermek processzek (processzorok) létrehozzák a saját gyermek processzeiket (a főprogrambeli processz unokáit) (második szint).

Ez a kétszintű létrehozás nagymértékben csökkenti a processzek létrehozásának teljes idejét.

2.3. ábra - Egymásba ágyazott FORALL ciklusok.

Egymásba ágyazott FORALL ciklusok.


Tegyük fel, hogy a külső FORALL ciklusnak index eleme van, míg a belsőnek eleme.

Ha egy processz létrehozási ideje , akkor az első szint processzeinek létrehozási ideje .

Az első szint processze (processzora) mindegyike létrehoz unokát idő alatt. Így a párhuzamos processzek létrehozási ideje:

Ha ezt direktben csinálnánk, akkor a létrehozás költsége (ideje) , ami sokkal nagyobb mint , ha .

A most látott példában a processzek létrehozási ideje a processzek végrehajtási idejéhez (1 művelet) képest nagy. Tehát a granularitási probléma itt is felmerül.

Ezen a belső FORALL csoportosításával lehet segíteni:

  PROGRAM SumArrays2; 
  VAR i,j : integer; 
    A, B, C: ARRAY [1..20,1..30] OF REAL; 
  BEGIN  
  ... 
  FORALL i:=1 TO 20 DO 
    FORALL j:=1 TO 30 GROUPING G DO 
       C[i,j]:=A[i,j]+B[i,j]; 
  ... 
  END. 

Ha például , akkor 600 processz helyett csak 100 unoka processz kerül generálásra, ami a granularitást jelentősen javítja.

2.2.5. Mátrixok szorzása (Matrix multiplication)

A mátrixok szorzásának párhuzamosítása egy fontos alkalmazási probléma/lehetőség.

A szokásos alapötlet két dimenziós vektor (, ) skaláris szorzata. A lehetséges programrészlet a következő:

  sum:=0; 
  FOR k:=1 TO n DO 
    sum:=sum+X[k]*Y[k]; 

Két -es mátrix és szorzata vektorok skalárszorzására vezethető vissza:

Ha , akkor a mátrix eleme -edik sorának és -edik oszlopának skalárszorzata:

2.4. ábra - Mátrixszorzás sémája.

Mátrixszorzás sémája.


Egy szokványos szekvenciális program megoldás a következő:

Mátrixszorzás (Matrix multiplication):

  C=A*B: 
  FOR i:=1 TO n DO 
    FOR j:=1 TO n DO 
       számítsuk ki a C[i,j]-t, mint az A i-dik sorának és a B j-dik oszlopának skaláris szorzataként; 

Ha legalább processzorunk van, akkor a fenti mátrix szorzási problémát egymásba ágyazott FORALL ciklusokkal a következőképpen oldhatjuk meg:

Párhuzamos mátrixszorzás (Parallel matrix multiplication):

  C=A*B:  
  FORALL i:=1 TO n DO 
    FORALL j:=1 TO n DO 
       számítsuk ki a C[i,j]-t, mint az A i-dik sorának és a B j-dik oszlopának skaláris szorzataként;

Az előzőek alapján a szekvenciális mátrix szorzás programja a következő lehet:

  PROGRAM MatrixMultiply; 
  CONST n=10; 
  VAR A,B,C: ARRAY [1..n,1..n] OF REAL; 
  i, j, k: INTEGER; 
  sum: REAL; 
  BEGIN 
  ...  
  FOR i:=1 TO n DO 
    FOR j:=1 TO n DO 
    BEGIN 
       sum:=0; 
       FOR k:=1 TO n DO 
          sum:=sum+A[i,k]*B[k,j]; 
       C[i,j]:=sum; 
    END;
  ... 
  END. 

A párhuzamos verzióban a skalár szorzatra egy eljárást kell definiálnunk (okok később). A megfelelő program:

  PROGRAM ParallelMatrixMultiply; 
  CONST n=10; 
  VAR A,B,C: ARRAY [1..n,1..n] OF REAL; 
  i, j, k: INTEGER; 
  PROCEDURE DotProduct(i,j: INTEGER); 
  VAR sum: REAL; 
  k: INTEGER; 
  BEGIN 
    sum:=0; 
    FOR k:=1 TO n DO 
       sum:=sum+A[i,k]*B[k,j]; 
    C[i,j]:=sum; 
  END; 
  BEGIN (*a főprogram*) 
  ... 
  FORALL i:=1 TO n DO 
    FORALL j:=1 TO n DO 
       DotProduct(i,j); 
  ... 
  END. 

A szekvenciális szorzás esetén a műveletszám . A párhuzamos algoritmus esetén az processzor egyszerre dolgozik és igy a program végrehajtási ideje (a processzek létrehozását nem számítva). Tehát a párhuzamos program speed-up-ja jelentős.

Ha processzorunk van, akkor megmutatható, hogy a műveletidő -re leszorítható.

2.2.6. Megosztott és lokális változók

A párhuzamos mátrix szorzó programban definiáltunk egy DotProduct eljárást. Ennek oka a következőkben foglalható össze.

Tekintsük először az alábbi hibás programot, amelyben ezt nem tesszük:

  PROGRAM ErroneousMatrixMultiply; 
  CONST n=10; 
  VAR A,B,C: ARRAY [1..n,1..n] OF REAL; 
  i, j, k: INTEGER; 
  sum: REAL; 
  BEGIN 
  ... 
  FORALL i:=1 TO n DO 
    FORALL j:=1 TO n DO 
    BEGIN 
       sum:=0; 
       FOR k:=1 TO n DO 
          sum:=sum+A[i,k]*B[k,j]; 
       C[i,j]:=sum;  
    END; 
  ... 
  END. 

A létrejövő 100 processz mindegyikének van saját másolata az i, j változók (megfelelő) értékeiről. Azonban a sum változót a program elején deklaráltuk, ebből csak egy van a közös memóriában, amelyhez minden processz hozzáférhet. Minthogy mindegyik processz hozzáférhet és ténylegesen is hozzányúl interferencia lép fel és azt se tudjuk, hogy mi lesz a végén.

A korrekt működéshez minden processznek a sum változóból egy saját lokális másolat kell. Ezt oldja meg a DotProduct eljárás, amelynek használata esetén az előző interferencia probléma nem léphet fel.

A Multi-Pascalban a főprogramban deklarált változók mind megosztott változók (shared variables). Minden eljárásban definiált lokális változót csak az a processz érhet el, amelyik az eljárást meghívja.

A Multi-Pascal processz létrehozását bárhol a programban megengedi, így egy eljárás belsejében is. Ekkor az eljárás lokális változói az itt létrehozott processzek számára megosztott (shared) változóként viselkednek.

2.3. Példa. A Ranksort program javított változata.

A megosztott változókkal kapcsolatos ismereteink alapján már javíthatjuk a korábbi Ranksort program „hibás” működését (ha az inputban vannak azonos elemek, akkor is jó eredményt adjon). A javított program a Ranksort2:

  PROGRAM Ranksort2;  
  CONST n=10; 
  VAR values, final: ARRAY [1..n] OF INTEGER; 
  i: integer; 
  PROCEDURE PutinPlace( src: INTEGER); 
  VAR testval, j, rank, count: INTEGER; 
  BEGIN 
    testval :=values[src]; 
    rank :=1; 
    count :=0; 
    FOR j :=1 TO n DO 
    BEGIN 
       IF testval > values[j] THEN rank :=rank+1; 
       IF (testval = values[j]) AND (j < src) THEN count :=count+1; 
    END;  
    rank := rank+count; 
    final[rank] :=testval; (*az érték a rangnak megfelelő helyre kerül*) 
  END; 
  BEGIN 
  FOR i :=1 TO n DO 
    Readln( values[i]); (*a rendezendő elemek betöltése*) 
  FORALL i :=1 TO n DO 
    PutinPlace(i); (*a values[i] rangjának meghatározása és elhelyezése*) 
  FOR i :=1 TO n DO 
    Writeln( final[i]); 
  END. 

A fenti programban megfigyelhető, hogy megjelent egy új változó a korrábiakhoz képes, amely megoldja az azonos elemek rangszámítását: ez a count.

Ha a korábbi input alapján futtatjuk a programunkat, akkor a következőt kapjuk (lásd még ranksort3.mp.swf):

2.5. ábra - A javított Ranksort program által adott helyes eredmény.

A javított Ranksort program által adott helyes eredmény.


2.2.7. Utasításblokkok deklarációval

Az előző probléma egy másik lehetséges Multi-Pascal megoldása az un. „utasításblokk” (statement block), amelynek általános alakja a következő:

  VAR <declaration 1>; 
  <declaration 2>; 
  ... 
  <declaration m>; 
  ... 
  BEGIN <statement 1>; 
  <statement 2>; 
  ... 
  <statement n>;  
  END; 

A statement block utasítás a BEGIN ... END pár közötti utasításokat egy összetett utasításnak tekinti, amelyben a VAR részben megnevezett változóknak új lokális környezetet definiál. A blokk terminálása után ezek a lokális változók törlődnek.

Szabályok:

  1. Az utasítás blokkok egymásba ágyazhatók.

  2. Utasítás blokkok szerepelhetnek eljárások, vagy függvények testében.

  3. Utasítás blokkban nem definiálhatók eljárások és függvények.

  4. Az utasítás blokk végrehajtása úgy történik mintha egy eljárás testet hívtunk volna meg. A deklarálások létrehoznak egy lokális környezetet, amely törlésre kerül a blokkba tartozó utasítások végrehajtása után.

  5. A lokális deklarációk csak az utasítás blokkon belül érvényesek.

  6. Az utasítás blokkon belüli eljárás, vagy függvény meghívás a végrehajtást ideiglenesen az eljárás testbe viszi, amely a lokális utasítás blokk deklarációk érvényességén (hatáskörén) kívül van.

Tekintsük a következő példaprogramot:

  PROGRAM Sample; 
  VAR A: ARRAY [1..10,1..10] OF REAL; 
  x,p: REAL; 
  ... 
  BEGIN(*a főprogram*) 
  ... 
    x:=A[i,4]*3; 
    p:=Sqrt(x); 
    VAR x,y: INTEGER (*az utasításblokk kezdete*) 
    value: REAL; 
  BEGIN 
  value:=1; 
  FOR x:=1 TO 10 DO 
    FOR y:=1 TO 10 DO 
    BEGIN 
       A[x,y]:=value; 
       value:=value*2; 
    END;  
  END; (*az utasításblokk vége*) 
  Writeln(x); (*„x”-re vonatkozóan, amely a főprogram elején volt deklarálva*) 
  Writeln(A[1,1]); 
  ... 
  END. 

Ha egy processz meghív egy eljárást, akkor a processzhez hozzárendelődik az eljárásban deklarált lokális változók saját, egyedi másolata.

Lásd pl. PutinPlace eljárást a Ranksort programban.

Ugyanez a helyzet a processzben definiált utasításblokk változóival kapcsolatban is. Ennek megfelelően a hibás mátrixszorzó program javítása az utasításblokk segítségével a következő lehet:

  PROGRAM ParallelMatrixMultiply2; 
  CONST n=10; 
  VAR A,B,C: ARRAY [1..n,1..n] OF REAL; 
  i, j, k: INTEGER; 
  BEGIN 
  ... 
  FORALL i:=1 TO n DO 
    FORALL j:=1 TO n DO 
       VAR sum: REAL; 
       k: INTEGER; 
    BEGIN 
       sum:=0; 
       FOR k:=1 TO n DO 
          sum:=sum+A[i,k]*B[k,j]; 
       C[i,j]:=sum; 
    END; 
  ... 
  END. 

2.2. Megjegyzés. A programban minden processznek 4 lokális változója lesz: i, j, k, sum. A beágyazott FORALL utasítások minden processzhez hozzárendelik az i, j lokális másolatát és azok kezdőértékeit.

Ha megváltoztatnák bármely skalár változót egy FORALL utasítás belsejében, akkor ez az összes létrehozott processz megváltoztatja ugyanazt a változót. Az utasítás blokk belsejében tett változások azonban csak a lokális másolatra hatnak.

2.2.8. A FORALL indexek hatásköre (scope)

A FORALL által létrehozott összes processz megkapja a ciklus index egyedi lokális másolatát. A ciklus indexet a FORALL-on kívül kell deklarálni, ugyanakkor úgy viselkedik mintha minden egyes processz belsejében definiáltuk volna.

Valójában a FORALL ciklusindex úgy viselkedik mintha egy lokális utasításblokkban lenne definiálva, azzal a különbséggel, hogy az index egy egyedi kezdőértéket kap minden processzben.

Tekintsük a következő programrészletet:

  PROGRAM Sample2; 
  VAR i: INTEGER; 
  PROCEDURE Tree; 
  BEGIN 
  ... 
  END; 
  BEGIN 
  ... 
  FORALL i:=1 TO 20 DO 
  BEGIN 
  ... 
  Tree;  
  END; 
  ... 
  END. 

Az i változót a főprogramban definiáljuk. A FORALL-on kívül közönséges változóként viselkedik. FORALL-on belül egy lokálisan definiált változóként. Ezért a lokális i index hatásköre nem érvényes a Tree eljáráson belül. A Tree meghívásakor temporálisan elveszti a lokális FORALL indexet. Ha tehát az i indexet használni akarjuk a meghívott eljáráson belül, akkor paraméterként kell átadni.

Pl. PutinPlace(i), DotProduct(i,j).

A FORALL indexek speciális tulajdonságai:

  1. Ha processz létrehozásra került az egyedi indexértékével, akkor ez nem változtatható meg a processzen belül.

  2. Bármely kisérlet ennek megváltoztatására fordítási hibát eredményez.

  3. FORALL index nem szerepelhet READ utasításban.

  4. FORALL indexet nem lehet átadni eljárások, vagy függvények változó paramétereként, csak érték paraméterként.

  5. Minden olyan környezetben használható, amely nem változtatja meg az index értékét.

2.2.9. A FORK utasítás

Eddig csak a FORALL paranccsal ismerkedtünk meg, amely általában több párhuzamos processz indítására alkalmas. Egyedi párhuzamos processz létrehozására szolgál viszont a

FORK <utasítás>;

szintaxissal megadott parancs.

Az utasítás bármely érvényes Multi-Pascal utasítás lehet. Az utasítás egy „gyermek processzé” válik, amely egy másik processzoron kerül végrehajtásra. A „szülő processz” ezután a „gyermek processzre” való várakozás nélkül folytatódik.

2.4. Példa. Tekintsük a következő programrészletet:

  ... 
  x:=y*3; 
  FORK FOR i:=1 TO 10 DO 
    A[i]:=i; 
    z:=SQRT(x); 
  ... 

Itt a következők történnek:

  1. x értéket kap.

  2. A szülő processz létrehozza a gyermek processzt.

  3. Mialatt gyermek processz még fut, a szülő végrehajtja a z értékadását és folytatja tovább a (fő)programot.

2.5. Példa. Példa a FORK parancs használatához:

FORK Normalize(A);

FORK Normalize(B);

FORK Normalize(C);

Itt 3 párhuzamos processz van, ugyanazon eljárás meghívásával. Mialatt ezek a hívások futnak, a szülő eljárás (főprogram) folytatódik. A következő 2.6. ábrán jól látható ezek szemléltetése:

2.6. ábra - FORK utasítás hatása

FORK utasítás hatása


2.2.10. Processz terminálás

Vizsgáljuk meg, hogy milyen különbségek adódnak a most megismert FORK és a FORALL esetén!

FORALL esetében: megvárja, amíg az összes processz befejeződik. Tekintsük a következő lehetséges alakot:

FORALL i:=1 TO n DO <utasítás>;

A fenti parancs esetén a következők történnek:

  1. Létrehoz n „gyermek” processzt.

  2. Várakozik, amíg mindenki befejezi.

  3. Folytatja a főprogram FORALL utáni végrehajtását (tulajdonképppen szinkronizál).

A FORK esetében nem ez a helyzet:

  1. Létrehozza a „gyermek” processzt, amely elkezd futni.

  2. Folytatja a „szülő” processzt várakozás nélkül (aszinkron működés).

Tekintsük a következő utasítást:

FOR i:=1 TO n DO

FORK <utasítás>;

Itt minden iterációban létrehozunk egy „gyermek” processzt. Miután a FOR ciklus befejeződőtt, a főprogram (szülő processz) folyatódik, azaz párhuzamosan a létrehozott gyermek processzekkel.

Ez merőben eltér a FORALL viselkedésétől.

Ha a „szülő” processz előbb fejeződne be mint a létrehozott gyermek processzek, akkor felfüggesztésre kerül, amíg a „gyermekek” befejezik (túl korai terminálás védelme).

2.2.11. A JOIN utasítás

A JOIN utasítás hatására a „szülő” processz várakozik a „gyermek” processz befejezésére (szinkronizációs eszköz). Ha a gyermek még nem terminált, akkor várakozik. Ha a gyermek terminált közben, akkor nincs hatása. A JOIN lényegében a FORK ellentéte.

2.6. Példa. Tekintsük a következő programrészletet:

  ... 
  FORK Normalize (A); 
    FOR i:=1 TO 10 DO 
       B[i]:=0; 
  JOIN; 
  ... 

Elemzés:

  1. A szülő létrehozza a FORK gyermeket.

  2. Mialatt a gyermek processz végrehajtásra kerül, a szülő végrehajtja a FOR ciklust.

  3. A ciklus befejzése után a szülő végrehajtja a JOIN utasítást.

  4. A gyermek processz befejezése után a szülő program folytatódik.

Tulajdonságok:

  1. Ha több FORK „gyermek” processz van, akkor a JOIN utasítást bármelyik terminálása kielégíti.

  2. A JOIN utasítások száma nem lehet több mint a létrehozott FORK processzek száma. Ellenkező esetben holtpont (deadlock) lép fel.

2.7. Példa. Példa több join utasításra:

  FOR i:=1 TO 10 DO 
    FORK Compute(A[i]); 
  FOR i:=1 TO 10 DO 
    JOIN; 

2.2.12. Amdahl törvénye

Láttuk, hogy a párhuzamos programok speed-up-ját a szekvenciális kódrészek jelentősen befolyásolják.

Tegyük fel, hogy db 1 időegységű „műveletet” kell párhuzamosan végrehajtanunk. Tegyük fel, hogy része a „műveleteknek” szekvenciális. Tehát művelet szekvenciális, művelet párhuzamos.

Ha processzorunk van, akkor a minimális végrehajtási idő:

Tekintve, hogy a szekvenciális végrehajtási idő , a maximális speed-up (az Amdahl-féle törvény értelmében):

Minthogy esetén , az Amdahl-féle törvény azt mondja, hogy nagyobb speed-up-ot csak a szekvenciális kódrészek csökkentésével érhetünk el. Ez egy nagyon egyszerű közelítő becslése a speed-up maximumának, miközben a valós programozási környezetben fellépő pl. szinkronizációs vagy kommunikációs feladatokból adódó időnövekedést nem veszi figyelembe.

2.3. Mintafeladatok

1. Feladat: Legyen és és tegyük fel, hogy oszloponként partícionált (), a mátrix pedig soronként

Ekkor a mátrixszorzat előáll a

diadikus szorzat formában. Tegyük fel, hogy rendelkezésre áll egy rutin, amely az szorzást elvégzi, ahol , .

Írjunk Multi-Pascal programot, amely a mátrix szorzást ennek segítségével elvégzi!

Mi az elérhető speed-up, ha a rendelkezésre álló processzorok száma és a következő esetek állnak fenn: a) ; b) , c) ?

Ha van elég processzorunk, akkor hol lehet még probléma (összeadásnál)?

2. Feladat: Tegyük fel, hogy az és tömbök elemei növekvő nagyságrendben rendezettek. Tegyük fel azt is, hogy az egyes tömbökben az elemek páronként különbözőek. Fésüljük össze a két tömböt egyetlen, növekvő sorrendben rendezett tömbbe párhuzamos algoritmussal!

Segítség:

Tegyük fel, hogy az tömb elemet az tömb -edik tagja helyére tudnánk betenni (). Ekkor helye az új tömbben: .

A „Segítség” bizonyítása:

-t elem, -t elem előzi meg, amely -nél kisebb. Tehát az új pozíciója: .

Kérdés:

Mi van, ha megengedjük a tömbökön belüli elemek esetleges azonosságát is?

Megoldás: A program egyik lehetséges változata a következő lehet (, melyben 10 és 5 elemből álló tömböket/vektorokat fésülünk össze):

  PROGRAM Merge;  
  CONST n=10; m=5; 
  VAR x: ARRAY [1..n] OF INTEGER; 
    y: ARRAY [1..m] OF INTEGER; 
    z: ARRAY [1..n+m] OF INTEGER; 
  i,j: INTEGER; 
  PROCEDURE PutX( src: INTEGER); 
  VAR testval, k: INTEGER; 
  BEGIN 
    testval:= x[src]; 
    k:= 1; 
    WHILE (y[k]<testval) AND (k<m) DO 
       k:= k+1;  
    IF (k=m) AND (y[k]<testval) THEN k:= k+1; 
    z[src+k-1]:= testval; (*az érték a rangjának megfelelő helyre kerül*)  
  END; 
  PROCEDURE PutY( src: INTEGER); 
  VAR testval, k: INTEGER; 
  BEGIN 
    testval:= y[src]; 
    k:= 1; 
    WHILE (x[k]<=testval) AND (k<n) DO 
       k:= k+1; 
    IF (k=n) AND (x[k]<=testval) THEN k:= k+1; 
    z[src+k-1]:= testval; (*az érték a rangjának megfelelő helyre kerül*) 
  END; 
  BEGIN 
  FOR i:= 1 TO n DO 
    Readln( x[i]); 
  FOR i:= 1 TO m DO 
    Readln( y[i]);  
  FOR i:= 1 TO n DO 
    FORK PutX(i); 
  FOR j:= 1 TO m DO 
    FORK PutY(j); 
  FOR i:= 1 TO n+m DO 
  JOIN; 
  FOR i:= 1 TO n+m DO 
  Writeln( z[i]); 
  END. 

2.4. A multiprocessors architektúra

Programok hatékonyságát multiprocessors architekturában a következő architekturális tényezők befolyásolják döntően:

  1. A processzorok száma és sebessége.

  2. A közös memória elérése és ennek szervezése.

Következmény: A programozónak ismernie kell a processzor-memória kommunikáció működését.

A lehetséges megoldások többfélék lehetnek:

  1. Busz orientált rendszer, esetleg cache memóriákkal.

  2. Memória modulok, amelyek kapcsoló hálózaton keresztül kötődnek a processzorokhoz.

Bármely esetben felléphet: memory contention, amely jelentősen csökkentheti a program hatékonyságát.

A következőkben azokat a fontos kérdéseket (különösképpen a memory contentiont) vizsgáljuk, amelyek a programozást befolyásolják.

2.4.1. Busz orientált rendszerek

A memory contention erősen függ a memória architektúrától. A busz orientált rendszerek sémája a következő:

2.7. ábra - Busz orientált rendszer.

Busz orientált rendszer.


A processzorokat és a közös memóriát egy közös busz köti össze. A buszon keresztül folyik az adat oda és vissza. A busz és a memória egyszerre csak egy üzenetet/kérést (request) tud kezelni.

A memória bármely cellájának eléréséhez a processzor a busz kizárólagos használatát igényli a rövid ideig. Ezért a busz sebessége (bus bandwith) korlátozza az osztott memóriát hatékonyan kihasználó processzorok számát.

Pl. Ha a busz sebessége 50 MHz, a memória elérési rátája 5MHz, akkor a busz 10 processzort tud kezelni.

A busz sebessége fizikailag korlátozott. 1994-ben a kommerciális busz orientált multiprocessors-ok 20-30 processzor tudtak kezelni. Akkori előrejelzés: 50-100 processzoros busz.

A napjainkban végbemenő, szinte robbanásszerű hardverfejlesztések eredményeképpen mind a busz sebessége, mind a memória elérési rátája többszörösen megnövekedett, miközben az egy számítógépben megtalálható processzorok száma alig növekedett. Érdemes tehát több processzort elhelyezni egy számítógépben, azaz egyedi konfigurációkat létrehozni.

2.4.2. Cache memória

A cache memória egy processzoros rendszerekben fontos szerepet játszik a memóra elérési ráta csökkentésében. A cache memória egy kisméretű (xKb), nagysebességű memória, amelyet közvetlenül a processzorhoz kapcsolnak és amelynek elérési sebessége nagyobb mint a központi memóriáé (xMb).

Az alapelvet mutatja a következő ábra:

2.8. ábra - Cache memória 1 processzoros gépen.

Cache memória 1 processzoros gépen.


A cache memória minden egyes rekesze egy cím-adat párt tartalmaz. A „cím” a központi memória valamelyik valódi címének, az „adat” a megfelelő központi memória cella tartalmának felel meg. Pl. az 1000 központi memória című cella a 75 adatot tartalmazza.

A cache memória rekeszei bizonyos központi memória cellák másolatai.

A processzor memória művelet esetén a következőképpen jár el:

  1. Ellenőrzi, hogy az adat a cache memóriában van-e (Ha igen, akkor „hit”, ha nem akkor „miss”).

  2. Ha nem, akkor a központi memóriában keres (lényegesen lassabban).

A cache memória elérése nagyon gyors (lényegében asszociatív memóriaként viselkedik). Mivel a cache memória mérete kicsi, a „miss” állapotok kikerülhetelenek.

Elv: a cache memóriában a valószínűleg leggyakrabban igényelt adatok kellenek.

Tipikus rendszeren a „hit” arány 90%. A cache memória olvasáskor előnyös. Iráskor a cache memóriába és a központi memóriába is be kell irni. Ha beírunk egy adatot a cache memóriába, akkor a központi memória eredeti adata elévül. A kezelésre két megközelítés van:

  1. Az új adatot egyszerre írjuk a cache és központi memóriába (write-through technika).

  2. Csak a cache memóriába írjuk be az új adatot és csak amikor ez már nem változik, akkor írjuk vissza a központi memóriába (write-back technika).

Statisztika: tipikus programok esetén: 20% írás (write), 80% olvasás (read).

A cache memória koncepció kiterjesztése multiprocessors-ra a következő:

2.9. ábra - Cache memória multiprocessors-os gépen.

Cache memória multiprocessors-os gépen.


Működési elv:

  1. Minden processzor először a saját cache memóriájában keres.

  2. Ha nem talál, akkor a buszon keresztül a központi memóriához fordul.

A egyprocesszoros rendszerek 80%-20% siker-tévedés aránya miatt a memory contention nagymértékben csökkenthető.

Problémák:

Minden lokális cache memória rendelkezik a központi memória bizonyos adatainak saját másolatával. Amíg csak olvasás van, addig nincs is probléma. Íráskor azonban probléma van, ui. minden másolat azonos kell, hogy legyen (cache koherencia). Tehát meg kell oldani a sok másolat kezelését.

Több megoldás van.

Egy lehetséges megoldás: Minden write művelet üzenetet küld a közös buszra, minden lokális cache figyeli a lokális buszt, hogy van-e írás utasítás valahol és azonnal felülírja a saját adatát (write-through technika).

2.4.3. Többszörös memória modulok

Ennél a megoldásnál a közös memóriát osztjuk meg szeparált modulokra, amelyeket az egyes processzorok párhuzamosan érhetnek el:

2.10. ábra - Szeparált modulok.

Szeparált modulok.


Az n processzor mindegyike elérheti az m memória modul mindegyikét a processzor-memória kapcsoló hálózaton keresztül. A memória rendszer összességében azonban egy összefüggő memóriaként viselkedik. A memóriamodulok hozzáférése szekvenciális módon történik.

A processzorok minden memória hozzáférési utasítása a kapcsoló hálózaton keresztül történik, amely automatikusan a korrekt fizikai címre küldi az igényt.

Memory contention kétféleképpen léphet fel:

  1. Forgalmi dugó a kapcsoló hálózatban.

  2. Két, vagy több processzor ugyanazt a memóriamodult akarja egyszerre, vagy kis időkülönbséggel elérni.

Megoldások a memória modulok használatára:

alapelv: az adatok alkalmas szétosztása a memóriamodulok között a hozzáférések figyelembevételével.

Pl. számos alkalmazásban a processzorok egymásutáni memóriacímeket használnak (pl. nagy tömbök esetén). Itt a memory contention azzal csökkenthető, hogy az egymásutáni címeket szétosztjuk a memóriamodulok között. Pl.

Tekintsünk most egy párhuzamos programot, amely 4 fizikai processzoron dolgozik és amelyben a processzorok a 0, 1, 2, 3, ..., 100 memóriacímeket érik el a következő minta szerint:

Látszólag memory contention lép fel. Azonban a memóriacímek előbbi szétosztása miatt nem ez a helyzet a számítások zömében. A tényleges (fizikai) memória elérési mintát a következő táblázat mutatja (*=várakozás):

A táblázatból láthatjuk, hogy a 4-ik memória hozzáférési ciklus után a processzorok fizikailag különböző memória modulokhoz fordulnak. Ez így is marad az utolsó 3 hozzáférésig.

A séma a következő:

A hasonló megoldások széles körben elterjedtek.

2.4.4. Processzor-memória kapcsolóhálózatok

A többszörös memória modulok jelentős mértékben csökkentik a memory contention veszélyét. Azonban a kommunikációs kapcsoló hálózat maga is okozhat memória harcot (memory contentiont). Minden memóriamodul egyidejűleg csak egy igényt tud kielégíteni. Ha tehát két, vagy több processzor egyszerre ugyanahhoz a memóriamodulhoz fordul, akkor memória harc (memory contention) lép fel.

Számos elméleti és gyakorlati megoldás van. Három esetet nézünk meg.

1. Crossbar hálózat

Minden processzort szimultán összeköthet minden memóriamodullal.

A kapcsolók bármilyen memória-processzor kapcsolatot megengednek mert minden pontban megvannak a megfelelő kapcsolók. Ez processzor és memóriamodul esetén kapcsolót és költséget jelent. A memóri harc (memory contention) soha nem léphet fel a crossbar hálózatban.

Nagy esetén költséges.

Helyette költségű megoldások: butterfly, shuffle-exchange.

2. Butterfly hálózat

processzor esetén kapcsolósor, soronként kapcsolóval. Összesen kapcsoló.

A nevét azért kapta mert minden egyes kapcsolósoron a kapcsolóvonalak a pillangószárnyakra emlékeztetnek.A pillangószárnyak minden sorban megkettőződnek.

3. Shuffle-exchange hálózat

Hasonló az előzőhöz. Valamilyen minta szerint elirányítjuk a memória hozzáférést. Jelen esetben ez a kártyák keverésére emlékeztet. Innen a név is.

Mindkét esetben a költség . Mindkét esetben felléphet contention, ha két, vagy több processzor ugyanazt a memóriát akarja.

2.5. Processz kommunikáció

Az eddigi példák a relaxált algoritmusok körébe tartoztak:

  1. minden processz egymástól függetlenül dolgozott,

  2. a közös adatázist csak olvasásra használták,

  3. a processzek soha nem írtak olyan adatokat, amelyeket más párhuzamos processz használna.

A processzek kommunikációját és egymásra hatását (interaction) vizsgáljuk.

Eszköz: channel változó (Multi-Pascal).

Cél: processz kommunikáció és szinkronizáció.

A channel változók lehetővé teszik adatok cseréjét és megosztását párhuzamos processzek között.

Fontos koncepció: pipeline szervezésű algoritmus.

A pipeline koncepció igen régi (). Először az aritmetikai műveletek gyorsítására találták ki:

2.11. ábra - Lebegőpontos szorzás egyszerűsített pipeline algoritmusa.

Lebegőpontos szorzás egyszerűsített pipeline algoritmusa.


Nagy mennyiségű szorzás esetén az egy műveletre eső átlagos idő csökken, mert nincs várakozás a szorzási művelet befejezéséig.

Tegyük fel, hogy

  1. Az algoritmust fel tudjuk bontani önálló részek egy elemű sorozatára (szekvencia).

  2. Az algoritmus adatfolyama olyan, hogy az önálló részek csak az előző rész végeredményét használják fel és csak a következő részhez adnak adatokat át.

  3. Az algoritmust igen sokszor ismételjük.

  4. Létre tudunk hozni párhuzamos processzt.

Az algoritmus pipeline végrehajtása a következők szerint történik:

A pipeline algoritmusban a párhuzamos processzeket csatornák kötik össze úgy, hogy az adatfolyam szempontjából egy cső (pipeline, tkp. összeszerelő sor) alakját öltik.

Minden egyes processz az adat átalakításának egy-egy önálló részét végzi. A bemenő (input) adat a cső egyik végén jön be, a kimenő (output) adat pedig a cső másik végén. Ha a cső tele van adatokkal, akkor minden processz párhuzamosan működik (adatokat vesz át az egyik szomszédtól és adatokat küld át a másik szomszédnak).

2.5.1. Processz kommunikációs csatornák

Olyan programokhoz szükségesek, amelyekben a párhuzamos processzek közös megosztott változókkal rendelkeznek és a futásuk alatt egymással kommunikálnak (egymást befolyásolják) a megosztott változókon keresztül.

Tekintsük két párhuzamos processz P1 és P2 esetét! P1 a futása alatt kiszámít egy értéket és azt a C változóba írja, amelyet P2 olvas és tovább alakít:

Minthogy P1 és P2 párhuzamos processzek, belső végrehajtásuk relatív sorrendje nem ismert előre. Vagyis nem lehet előre garantálni, hogy P1 beírja C-be az értéket mielőtt P2 olvassa azt. Vagyis a párhuzamosság elveszik. Hasonló szituáció számos helyen fordul elő.

A probléma megoldása: a CHANNEL változó rendelkezik az „üres” tulajdonsággal.

Ez azt jelenti, hogy

  1. Induláskor a CHANNEL változó üres.

  2. Ha a P2 processz üres CHANNEL változót akar olvasni, akkor a P2 processzt addig felfüggesztjük, amíg P1 nem írja ki a változó értékét.

Ezzel a megoldással a processz kommunikáció korrekt lesz.

2.5.2. Csatorna változók

Ha a processz nem csak egy értéket, hanem adatok egész sorát küldi egy másik processznek, akkor ezt az együttműködést (interakciót) termelő-fogyasztó (producer-consumer) típusúnak nevezzük.

A Multi-Pascal channel változói, amelyek ezt valósítják meg, úgy viselkednek mint egy FIFO sor.

Az adatokat írásuk sorrendjében egy sorba mentik ki, ahonnan azokat valamely processz kiolvassa.

Az alábbi ábra ezt a szituációt jelképezi:

Itt a C channel változót egy „cső” jelképezi. Az adatok balról jönnek és tovább „folynak” jobbfelé, ahol P2 olvashatja őket.

A channel változókat a program deklarációs részében kell definiálni. A channel változóknak típusa van és az értékadó utasításokban a baloldalon a Pascal szabályoknak meg kell felelni.

Az általános szintaxis:

<channel név>: CHANNEL of <komponens típus>

A channel változók lehetséges típusai: Integer, Real, Char, Boolean.

A channel változó értékadása:

Az értékadásban bármely érvényes Pascal kifejezés lehet.

Például:

  C:=10; 

Az utasítás hatására a C csatorna (sor) végére 10 kerül beírásra.

Például:

  C:=x+i*2; (* x és i skalár változók*) 

Szabály: A jobboldal típusa meg kell, hogy egyezzen a baloldal (csatorna) típusával.

A channel változó olvasása:

változó:=Channel name

A változó típusnak meg kell egyeznie a csatorna változó típusával.

Például:

  x:=C; 

A csatornába való minden írás hozzátesz a belső sorhoz, minden olvasás elvesz a belső sorból.

Ha egy processz olvasni próbál egy csatorna változót, amely éppen üres, akkor a processz végrehajtása automatikusan felfüggesztődik addíg amíg egy más processz nem ír a csatornába. Ezután a processz automatikusan újraindul (folytatódik).

Ez a tulajdonság a csatorna koncepció egyik fő értéke. Képes az olvasó processzt addig késlelteni, amíg a szükséges értéket valamelyík író processz nem produkálja. Minden csatorna tárolási kapacitása korlátlan. Tehát az író processzek soha nem lassulnak le emiatt.

Annak eldöntése, hogy egy csatorna tartalmaz-e legalább egy értéket, a következőppen lehetséges:

  IF C? 
  THEN x:=C (*lehet olvasni a csatornából*) 
  ELSE x:=0; (*nem lehet olvasni a csatornából*) 

A C? kifejezés TRUE lesz, ha a C csatorna nem üres. Egyébként pedig FALSE.

A példában a processz nem kerül felfüggesztésre, ha a csatorna üres.

Tekintsük a következő „tipikus” termelő-fogyasztó (producer-consumer) programot!

  PROGRAM Producer-Consumer; 
  CONST endmarker=-1; 
  VAR commchan: CHANNEL OF INTEGER; 
  PROCEDURE Producer; 
  VAR inval: INTEGER; 
  BEGIN 
    REPEAT 
    ... (*számítsuk ki az „inval” elem értékét a csatorna számára*) 
    commchan:=inval; (*írjunk a csatornába*) 
    UNTIL inval=endmarker; 
  END; 
  PROCEDURE Consumer; 
  VAR outval: INTEGER; 
  BEGIN 
  outval:=commchan; (*olvasunk a csatornából*) 
    WHILE outval<>endmarker DO 
       ... (*felhasználjuk az „outval” értékét valamilyen számításban*) 
       outval:=commchan; (*olvassuk a következő értéket a csatornából*) 
  END; 
  BEGIN (*főprogram*) 
    FORK Producer; (*A termelő (producer) processz*) 
    FORK Consumer; (*A fogyasztó (consumer) processz*) 
  END. 

A termelő (producer) processz egy értéket ír a csatorna változóba. A fogyasztó (consumer) processz ezt olvassa. Ha éppen nincs érték, akkor várakozik, amíg lesz. Ez mindaddig megy, amíg az inval változó az endmarker értéket el nem éri.

A program működését sematikusan a következő ábra mutatja:

2.12. ábra - A termelő-fogyasztó program.

A termelő-fogyasztó program.

2.5.3. Pipeline párhuzamosítás

A pipeline a termelő-fogyasztó (producer-consumer) paradigma speciális esete. A processzek sorozata egy lineáris „pipeline”-t, azaz „csővezetéket” formál, ahol minden egyes processz a baloldalé szomszéd fogyasztója és a jobboldali szomszéd termelője.

A pipeline típusú algoritmusokban adatok egy sorozata folyik keresztül a „pipeline”-on, amelyet az egyes processzek transzformálnak. Tehát az belépő adat egymásutáni transzformációk egy sorozatán megy át. A párhuzamosság abból adódik, hogy a pipeline minden egyes processze egyidejűleg dolgozik, de különböző adatokon.

A Multi-Pascal megengedi a channel változók tömbjeit is.

Például:

  VAR chan : ARRAY [1..20] OF CHANNEL OF INTEGER; 

Itt chan[i] az i-edik csatorna változója.

Például:

  chan[10]:=10; (*írjunk a „chan[10]”-be*) 

Channel tömb indexei a szabályos Pascal indexek lehetnek.

Például:

  i:=5; 
  x:=chan[i]; (*olvassunk a „chan[i]”-ből*) 

A fenti ábra pipeline sémájának Multi-Pascal nyelvben történő megvalósításához a szükséges eljárás a következő:

  PROCEDURE PipeProcess(mynumber: INTEGER); 
  VAR inval, outval, i: INTEGER; 
  BEGIN 
  FOR i:=1 TO m DO 
  BEGIN 
    inval:=chan[mynumber]; (*olvasás a baloldali csatornából*) 
    ... (*használjuk az „inval”-t, hogy kiszámoljuk az „outval”-t*) 
    chan[mynumber+1]:=outval; (*írás a jobboldali csatornába*) 
  END; 
  END; 

Amikor létrehozunk párhuzamos processzt, akkor minden egyes processznek lesz egy egyedi mynumber paraméterértéke. A mynumber számú processz az input adatokat a chan[mynumber] csatornából olvassa be, az output adatokat pedig a chan[mynumber+1] csatornába írja ki.

2.3. Megjegyzés. A relaxált párhuzamosság általában nagyobb speed-up-ot ad. Bizonyos esetekben azonban a pipeline technika is nagyon jó.

2.5.4. Fibonacci-sorozat elemeinek meghatározása

A „pipeline” technika alkalmas lineáris rekurzív sorozatok elemeinek megkeresésére. Egyik klasszikus példa lehet a Fibonacci-sorozat, amelynek megadása a következő:

2.4. Definíció. Az egész szám a Fibonacci-sorozat -dik tagja, ha esetén teljesül a következő rekurzív összefüggés:

A sorozat első két eleme adott: , .

Tekintsük a következő programot, amely visszaadja az általunk kért -dik Fibonacci számot!

  PROGRAM Fibonacci;  
  VAR i, j, s: INTEGER;  
    chan: ARRAY[1..100] OF CHANNEL OF INTEGER; 
  PROCEDURE resultF(i: integer);  
  VAR A, B: INTEGER; 
  BEGIN  
    A:=chan[i-2]; 
    chan[i-2]:=A; 
    B:=chan[i-1]; 
  chan[i-1]:=B; 
  chan[i]:=A+B;  
  END; 
  BEGIN  
    chan[1]:=1; 
    chan[2]:=1; 
    Writeln('Sorszam: '); 
    Readln(s); 
    FORALL j:=3 TO s DO  
       resultF(j); 
    Writeln('Eredmeny: ',chan[s]);  
  END. 

A program által szolgáltatott eredményt a következő ábrán tekinthetjük meg:

Az algoritmus fordítását és működését (futtatását) mutatja be a következő animáció: fibonacci.mp.swf

2.5.5. Lineáris egyenletrendszerek megoldása

Legyen

A klasszikus Gauss-módszerben az egyenletrendszert az ekvivalens

alakra transzformáljuk, ahol felső háromszög mátrix. Legyen , ahol egység alsó, pedig felső háromszög mátrix. Ekkor az miatt a klasszikus Gauss-módszerben szereplő az -felbontás tényezője és .

A felső háromszög alakra hozás költsége , az megoldási költsége (visszahelyettesítés) .

A Gauss-elimináción alapuló -módszer a következő:

ekvivalens a két

egyenletrendszerrel, amelyeknek háromszög mátrixai vannak.

A párhuzamos gyakorlatban az LU felbontáson alapuló Gauss-verziókat részesítik előnyben.

Az felbontásnak sok lehetséges architekturafüggő párhuzamosítása ismert. Ugyanez a helyzet a háromszög mátrixú egyenletrendszerek párhuzamos megoldásával kapcsolatban is.

Vizsgáljuk most meg alsó háromszög mátrixú egyenletrendszer egy „pipeline” megoldását.

Tekintsük a következő alakot:

A szekvenciális algoritmus:

illetve ennek program változata:

Sequential Back Substitution:

  FOR i:=1 TO n DO 
  BEGIN (*oldjuk meg az i-dik egyenletet az x[i]-re*) 
    sum:=0; 
    FOR j:=1 TO i-1 DO 
       sum:=sum+A[i,j]*x[j]; 
       x[i]:=(B[i]-sum)/A[i,i]; 
  END; 

Ez az algoritmus közvetlenül nem párhuzamosítható, mert x[i] értékének számításához szükségünk van az x[1], x[2], ..., x[i-1] értékekre.

Jelöljük ki az i-edik processzt az i-edik egyenlet számára, azaz az i-edik processz számolja ki az x[i] értékét.

Az i-edik processznek szüksége van az x[1], x[2], ..., x[i-1] értékekre, amelyeket az 1, 2, ..., ( i-1)-ik processz számol ki. Rendezzük el a processzeket az alábbi „pipeline” formában:

Mihelyt P1 befejezte x[1] számítását, azonnal elküldi azt a többi processznek. Mihelyt P2 befejezte x[2] számítást azonnal elküldi azokat a többi processznek, stb.

Így, bizonyos késéssel(késleltetéssel) a párhuzamos processzek is számolnak.

Az i-ik processz formája a következő lehet:

Pipeline Process i:

  sum:=0; 
  FOR j:=1 TO i-1 DO 
  BEGIN 
  Olvassuk ki az x[j] értékét balról; 
  Küldjük el az x[j] értékét jobbra; 
  sum:=sum+A[i,j]*x[j]; 
  END; 
  x[i]:=(B[i]-sum)/A[i,i]; (*számoljuk ki az x[i]-t*) 
  Küldjük el az x[i]-t jobbra; 

Vegyük észre, hogy:

  1. A processz beolvassa a következő adatot.

  2. Azonnal továbbküldi a többi processznek.

  3. Azonnal feldolgozza.

A teljes program a következő:

  PROGRAM Backsubstitution; 
  CONST n=50; 
  VAR A: ARRAY [1..n,1..n] OF REAL; 
    B,x: ARRAY [1..n] OF REAL; 
    pipechan: ARRAY [1..n+1] OF CHANNEL OF REAL; 
    i, j: INTEGER; 
  PROCEDURE PipeProcess(i: INTEGER); 
  (*Megoldja az i-dik egyenletet x[i]-re*) 
  VAR j: INTEGER; sum, xvalue: REAL; 
  BEGIN 
    sum:=0; 
    FOR j:=1 TO i-1 DO 
       BEGIN 
          xvalue:=pipechan[i]; (*olvassuk x[j]-t balról*) 
          pipechan[i+1]:=xvalue; (*küldjük x[j]-t jobbra*) 
          sum:=sum+A[i,j]*xvalue; 
       END; 
    x[i]:=(B[i]-sum)/A[i,i]; (*az x[i] értékének meghatározása*) 
    pipechan[i+1]:=x[i]; (*x[i]-t jobbra küldjük*) 
  END; 
  BEGIN 
  FOR i:=1 TO n DO 
    FOR j:=1 TO n DO 
       Readln( A[i,j]); 
  FOR i:=1 TO n DO 
    Readln( B[i]); 
  FORALL i:=1 TO n DO (*a pipeline processzek létrehozása*) 
    PipeProcess(i); 
  FOR i:=1 TO n DO 
    Writeln(x[i]); 
  END. 

A program processzort használ. A végrehajtási ideje , szemben a szekvenciális program idejével.

A program dinamikáját a PROFILE utasítással vizsgálhatjuk:

PROFILE p:q t

Az utasítás p, p+1,..., q processzorok hasznosítását jeleníti meg grafikusan t időegységenként. A maximális processzorszám 40.

A grafikus jelek a hasznosítás mértékét mutatják:

„*” =75-100%

„+”=50-75%

„-”=25-70%

„.”=0-25%

2.5.6. Csatornák és struktúrált típusok

A csatornaváltozók típusa nem csak a 4 alaptípus, hanem tetszőleges a standard Pascalban érvényes típus lehet.

Például: tömb, rekord

  C: CHANNEL OF ARRAY [1..10] OF REAL; 
  D: CHANNEL OF RECORD 
  left, right: INTEGER; 
  center: REAL; 
  END; 

Például: pointer

  TYPE 
  item: RECORD 
  x, y: INTEGER; 
  END; 
  VAR E: CHANNEL OF ^item; 

A korábbi szabályok vonatkoznak a tömb, rekord és mutató típusú csatornákra is: a jobb és baloldali típusnak meg kell egyeznie.

Például:

  TYPE artyp=ARRAY [1..10] OF REAL; 
  VAR C: CHANNEL OF artyp; 
  a: artyp; 
  ... 
  BEGIN 
  ... 
  C:=a; (*írás csatornába*) 
  ... 
  a:=C; (*olvasás csatornából*) 
  ... 

A csatorna változó, vagy csatorna tömb individuális csatornáinak elemeit nem indexelhetjük. Erre az egyetlen mód a csatorna minden elemének beolvasása egy nem csatorna tömbbe.

Például:

  TYPE rectyp=RECORD 
  left, right: INTEGER; 
  center: REAL; 
  END; 
  VAR D: CHANNEL OF rectyp; 
  e: rectyp; 
  ... 
  BEGIN 
  ... 
  D:=e; 

A D.left kifejezés szintaktikai hiba.

Például többdimenziós csatorna tömb esetén:

  A: ARRAY [1..10, 1..20] OF 
  CHANNEL OF REAL; 

Ez 200 egyedi csatornát jelent.

  p:=A[3,2] (*olvasás a csatornából*) 
  A[4,3]:=p; (*írás a csatornába*) 

2.6. Adatmegosztás

Az adatmegosztás problémája a legegyszerűbben a következőképpen fogalmazható meg:

Adott egy osztott adat, amelyhez egyidejüleg párhuzamos processz fér(het) hozzá (olvassa, felülírja). Garantálni kell az individuális processzek adatintegritását, vagyis azt, hogy a ténylegesen kívánt adatot (adatokat) más processz közben nem változtatja meg.

Ennek (Multi-Pascal) eszköze a spinlock, amely egy processznek exkluzív hozzáférést biztosít az osztott adathoz, miközben a többi processzt várakozásra készteti. Lényegében egy kaput hoz létre, amelyen a processzek egyesével haladhatnak át (juthatnak) az adathoz.

Alapfogalom: atomi művelet.

Ez alatt egy párhuzamos processz által végzett olyan „műveletet” értünk, amely nem szakítható meg más művelettel. Amíg ez dolgozik, addig más processz nem juthat az adathoz.

A spinlock az ilyen atomi művelet létrehozásának eszköze.

Következménye ennek, hogy a többi processz vár harc (contention) hatékonyság romlás (performance bottleneck).

2.8. Példa. Tekintsünk két párhuzamos processzt -t és -t, amelyek a megosztott változót olvassák és írják. Mindkét processz az értékéhez 1-et ad és visszaírja a memóriába.

Lehetséges esetek:

a) A kiolvassa értékét a 20-at, majd visszaír 21-et. Ezután olvassa ki értékét és a végeredmény 22 lesz.

b) azelőtt kiolvassa értékét, mielőtt A visszaírna. Ekkor értéke 21 lesz.

Ez egy tipikus programhibát (timing dependent error) okoz, amelyet nehéz felfedezni.

2.9. Példa. Tekintsük a következő programot! Ebben voltaképpen egy egyszerű leszámlálási folyamat, azaz keresés zajlik két értékre vonatkozólag, melyek a 11 és a 7.

  PROGRAM Search; 
  VAR A: ARRAY [1..200] OF INTEGER; 
    i, val, n: INTEGER; 
  BEGIN 
  FOR i:=1 TO 150 DO 
    A[i]:=11; 
  FOR i:=151 TO 200 DO 
    A[i]:=7; 
  n:=0; 
  Readln(val); 
  FORALL i:=1 TO 200 GROUPING 10 DO 
    IF A[i]=val THEN n:=n+1; 
  Writeln('Total occurences: ', n); 
  END. 

Ha a val=11 értéket kapjuk, akkor a program az értéket adja vissza. Ha val=7, akkor . Mindkét érték hibás.

A Multi-Pascalban a megoldás: spinlock utasítás.

A spinlocknak két állapota van: zárva (locked), nyitva (unlocked). Ezeket a Lock és an Unlock utasításokkal lehet elérni. Ha a spinlock zárva van, akkor a processz vár, amíg más processz meg nem nyitja. Tehát a spinlock lehetővé teszi, hogy egy processz más processzeket várakoztasson (busy-waiting állapotban).

2.10. Példa. Az előző példa korrekt megoldása spinlock utasítással.

  PROGRAM Search2; 
  VAR A: ARRAY [1..200] OF INTEGER; 
    i, val, n: INTEGER; 
    L: SPINLOCK; 
  BEGIN 
  FOR i:=1 TO 150 DO 
    A[i]:=11; 
  FOR i:=151 TO 200 DO 
    A[i]:=7; 
  n:=0; 
  Readln(val); 
  FORALL i:=1 TO 200 GROUPING 10 DO 
    IF A[i]=val THEN 
       BEGIN 
          Lock(L); (*a spinlock elérése és zárolása*) 
          n:=n+1; 
          Unlock(L); (*a spinlock feloldása, hogy más elérje*) 
       END; 
  Writeln('Total occurences: ', n); 
  END. 

Ez a program mindkét esetben helyes eredményt ad.

Minden processz végrehajtja a Lock(L) utasítást mielőtt módosítja -et. Minden más processz, amelyik a Lock(L) utasítást kiadja azt találja, hogy L zárt állapotban van. Ezért várnak. Ha a processz befejezi az „ ” műveletet, kiadja az Unlock(L) utasítást. Ez az L spinlock-ot nyitott állapotba helyezi. Tehát valamelyik várakozó processz beléphet.

A spinlock utasítás működését illusztrálja a következő ábra:

Más párhuzamos programnyelvekben a spinlockhoz hasonló megoldások:

  • szemafor,

  • lock

  • kritikus tartomány

  • monitor.

2.6.1. A spinlock

Implementálás: A felfüggesztett processzek un. „busy-waiting” állapotban vannak. A processz formálisan tovább fut (foglalja a processzort) és „belekerül” egy olyan ciklusba, amely a spinlock státusát vizsgálja (mint a motolla). Innen van az elnevezés is.

A spinlock egy programváltozó, amelyet egy memóriacellához rendelünk. Két állapota van: 0=unlocked, 1=locked.

Unlock(L):

  Write 0 into L; 

A másik parancshoz kell egy TestandSet művelet: Kiolvassa a memóriacella értékét és felülírja 1-el. Ezután tekinthetjük a Lock(L) művelet megvalósítását.

Lock(L):

  Repeat 
  X:=TestandSet(L); (*Olvassuk a régi állapotot és „locked”-ra állítjuk*) 
    Until X=0; (*Termináljuk a ciklust, amint az állapot „unlocked” lesz*) 

Ha L=0, akkor X=0 és L=1 lesz. Ha L=1, akkor X=1 és folytatja addig, amíg L=0 nem lesz más processz által.

A spinlock lehet struktúrált típus is.

Például:

  VAR G: ARRAY [1..10] OF SPINLOCK; 
    H: ARRAY [1..10,1..10] OF SPINLOCK; 

Érvényes műveletek:

  Lock(G[3]); 
  Unlock(H[1,5]); 
  i:=2; 
  Lock(G[i]); 
  Unlock(G[i+5]); 

A spinlock adattípus lehet rekord típus komponense is és pointerekkel is kombinálhatjuk.

Például:

  R item: RECORD 
  data: INTEGER; 
  L: SPINLOCK; 
  END; 

Érvényes műveletek:

  Lock(item.L); 
  Unlock(item.L); 

Például:

  TYPE itempnt= ^itemtype; 
  itemtype=RECORD 
  data: INTEGER; 
  L: SPINLOCK; 
  next: itempnt; 
  END; 
  VAR head: itempnt; 

2.6.2. A contention jelenség osztott adatoknál

A spinlock (atomi műveletek) használata memory contentiont okoz.

2.11. Példa. Vizsgáljuk a következő egyszerűsített példát, amely egy digitális kép intenzitás sűrűség hisztogramját számolja ki. A képintenzitások egész, 0 és max közé eső értékei egy mátrixba vannak elhelyezve.

A hisztogram szekvenciális programja:

  PROGRAM Histogram; 
  CONST n=20; (*a kép dimenziója*) 
    max=10; (*maximális pixel intenzitás*) 
  VAR image: ARRAY [1..n,1..n] OF INTEGER; 
    hist: ARRAY [0..max] OF INTEGER; 
    i, j, intensity: INTEGER; 
  BEGIN 
  FOR i:=1 TO n DO (*a képtömb feltöltése*) 
    BEGIN 
       FOR j:=1 TO n DO 
          Read(Image[i,j]); 
       Readln ;
    END; 
  FOR i:=0 TO max DO (*a hisztogram iniciálizálása*) 
    hist[i]:=0; 
  FOR i:=1 TO n DO 
    FOR j:=1 TO n DO 
       BEGIN 
          intensity:=image[i,j]; 
          hist[intensity]:=hist[intensity]+1; 
       END; 
  FOR i:=0 TO max DO 
  Writeln(hist[i]); 
  END. 

A feladat párhuzamosításánál spinlock tömböt használunk.

A probléma párhuzamos programja:

  PROGRAM ParallelHistogram; 
  CONST n=60; (*a kép dimenziója*) 
    max=5; (*maximális pixel intenzitás*) 
  VAR image: ARRAY [1..n,1..n] OF INTEGER; 
    hist: ARRAY [0..max] OF INTEGER; 
    L: ARRAY [0..max] OF SPINLOCK; 
    i,j: INTEGER; 
  BEGIN 
  FOR i:=1 TO n DO (*a képtömb feltöltése*) 
    BEGIN 
       FOR j:=1 TO n DO 
          Read(Image[i,j]); 
          Readln; 
    END; 
  FOR i:=0 TO max DO (*a hisztogram inicializálása*) 
    hist[i]:=0; 
  FORALL i:=1 TO n DO 
    VAR j, intensity: INTEGER; 
    BEGIN 
       FOR j:=1 TO n DO 
          BEGIN 
             intensity:=image[i,j]; 
             Lock(L[intensity]); 
             hist[intensity]:=hist[intensity]+1; 
             Unlock(L[intensity]); 
          END; 
    END; 
  FOR i:=0 TO max DO 
  Writeln(hist[i]); 
  END. 

A spinlock tömb használata csökkenti a memory contentiont. Ha csak egyetlen spinlockot használnánk, akkor az a következőképpen nézne ki:

  BEGIN 
    intensity:=image(A[i,j]); 
    Lock(L); 
    hist[intensity]:=hist[intensity]+1; 
  Unlock(L); 
  END; 

2.12. Példa. Közelítő integrálás trapézformulával:

A szekvenciális numerikus integrálás programja:

  sum:=0; 
  t:=a; 
  FOR i:=1 TO n DO 
  BEGIN 
    t:=t+w; (*ugorjunk a következő pontra*) 
    sum=sum+f(t); 
  END; 
  sum:=sum+(f(a)+f(b))/2; 
  answer:=w*sum; 

Egy sima FORALL megoldás a sum megosztott változó spinlock védelmével jelentős memória küzdelemhez (memory contention) vezethet. Sokkal jobb megoldás az intervallum azonos hosszúságú részekre bontása és ezen a trapézformula alkalmazása (compound trapézformula). Az utóbbi megoldás ötletét mutatja az ábra:

Az eljárás programja:

  PROGRAM NumericalIntegration; 
  CONST numproc=40; (*a processzek száma*) 
    numpoints=30; (*a pontok száma processzenként*) 
  VAR a,b,w,globalsum,answer: REAL; 
    i: INTEGER; 
  L: SPINLOCK; 
  FUNCTION f(t: REAL): REAL; (*az integrálandó függvény*) 
  BEGIN 
    f:=t+sin(t); 
  END; 
  PROCEDURE Integrate(myindex: INTEGER); 
  VAR localsum, t: REAL; j: INTEGER; 
  BEGIN 
    t:=a+myindex*(b-a)/numproc; (*a kezdő pozíció*) 
    FOR j:=1 TO numpoints DO 
       BEGIN 
          localsum:=localsum+f(t); (*a következő példapont hozzáadása*) 
          t:=t+w; 
       END; 
    localsum:=w*localsum; 
    Lock(L); 
    globalsum:=globalsum+localsum; (*atomi frissítés*) 
    Unlock(L); 
  END; 
  BEGIN 
  a:=0.0; 
  b:=1.0;í 
  w:=(b-a)/(numproc*numpoints); (*pontok közötti távolság*) 
  FORALL i:=0 TO numproc-1 DO (*processzek létrehozása*) 
    Integrate(i); 
    answer:=globalsum+w/2*(f(b)+f(a)); (*a végpontok hozzáadása*) 
    Writeln('result= ',answer); 
  END. 

A programozott példa alapján a végeredmény a következő:

Az elért speed-up kb. 24, ami nagyon jó érték.

2.6.3. Spinlockok és Channelek összehasonlítása

Channel = processzek közti kommunikáció

Spinlock = atomi művelet létrehozása (kizárás)

Mindkét megoldásnak vannak előnyei és hátrányai.

Bármelyik szimulálható a másikkal.

Működésük:

  1. Spinlock: lock esetén a processzek várnak, unlock esetén egy processz folytatódhat.

  2. Channel: ha üres a processz vár, amíg valamit bele nem írnak a csatornába, ha nem üres, akkor folytatódik.

Egy csatornába sok processz írhat, a csatornát sok processz olvashatja.

Lock, unlock szimulációja egy csatornával:

  1. Inicializáljuk a csatornát egy tétellel.

  2. Lock: egy processz kiolvassa az adatot. Minden más processz várakozik, amíg valaki bele nem ír a csatornába (Unlock)

  3. Unlock: egy atomi művelet beír a csatornába 1 adatot. Ekkor egy processz kiolvassa és folytatódik.

Fordítva: Spinlock-al is lehet csatornát szimulálni. A csatorna egy párhuzamos elérésű FIFO sor. A spinlock-al el lehet érni, hogy a sor műveletek atomi műveletek legyenek.

A Multi-Pascal nyelvben:

Lock, Unlock : 2 időegység

Channel read write: 5 időegység.

A Spinlock és channel gépi végrehajtása:

  1. Lock esetén a processz a memóriában marad és ugrásra készen várakozik (miközben iteratíven vizsgálja a spinlock állapotát). Busy-waiting technika.

  2. Channel művelet esetén: a processzt leállítja és a processzor szabad lesz . Az olvasó eljárás un. blokkolt állapotba kerül. Ha írnak a memóriába, akkor ez az olvasó eljárást készenléti (ready) állapotba helyezi, jelet küld a processznek, amely betöltődik és folytatódik.

A processz blokkolás esetén

  1. Előny: a processzor felszabadul.

  2. Hátrány: a programozása bonyolultabb és plusz overhead.

Busy-waiting technika esetén

  1. Előny: egyszerűbb programozás.

  2. Hátrány: processzor foglalása.

2.7. Szinkronizált párhuzamosság

Szinkronizált párhuzamosság, ha:

  1. Iteratív eljárás nagyméretű adattömbökön

  2. Az adattömbök kisebb részekre felbonthatók, amelyek párhuzamosan processzálhatók (adatpárhuzamosság).

  3. Minden iteráció után a processzeket szinkronizálni kell, mert az általuk adott eredmények a következő iterációban más processznek kellenek.

Processz szinkronizálás torlódás(bottleneck)

Szinkronizálás: processzek késleltetése, amíg szükséges.

Számos szinkronizációs technika van.

Tekintsük a következő feladatot:

Generáljunk egy diszkrét ponthálót az tartományban:

Egy -es felbontást mutat a következő ábra:

Az pontban jelölje az elméleti közelítését! Ekkor a belső pontokban felírhatjuk az

lineáris egyenletrendszert, mely átrendezés után a következő:

Tehát számítása az alábbi információkból történik:

Az egyenletrendszer megoldására a következő Jacobi relaxációs módszert definiálhatjuk:

Szükség van a kezdőérték eloszlásra. Ez ismert a peremen (, ).

Az eljárás mindig konvergens (de nagyon speciális).

Az eljárás szekvenciális változata:

  PROGRAM Jacobi; 
  CONST n=32; (*a tömb mérete*) 
    numiter=120; (*az iterációk száma*) 
  VAR A,B: ARRAY [0..n+1,0..n+1] OF REAL; 
    i,j,k: INTEGER; 
  BEGIN 
  FOR i:=0 TO n+1 DO (*a tömbértékek inicializációja*) 
    BEGIN 
       FOR j:=0 TO n+1 DO 
          Readln(A[i,j]); 
    END; 
  B:=A; 
  FOR k:=1 TO numiter DO 
    BEGIN 
       FOR i:=1 TO n DO 
          FOR j:=1 TO n DO (*a négy szomszéd átlagának kiszámítása*) 
             B[i,j]:=(A[i-1,j]+A[i+1,j]+A[i,j-1]+A[i,j+1])/4; 
  A:=B; 
  END; 
  Writeln(A[5,5]); 
  END. 

Vegyük észre az utasítást! A konkrét példában .

Ez eljárás kézenfekvő párhuzamosítása FORALL segítségével történhet.

Az új iterációban mindegyik érték kell, ezért szinkronizálni kell, amelyet 2 fázisban végzünk el.

A párhuzamos program a következő:

  PROGRAM Pjacobi; 
  CONST n=32; (*a tömb mérete*) 
    numiter=120; (*az iterációk száma*) 
  VAR A,B: ARRAY [0..n+1,0..n+1] OF REAL; 
    i,j,k: INTEGER; 
  BEGIN 
  FOR i:=0 TO n+1 DO (*a tömbértékek inicializációja*) 
    BEGIN 
       FOR j:=0 TO n+1 DO 
          Readln(A[i,j]); 
    END; 
  B:=A; 
  FOR k:=1 TO numiter DO 
    BEGIN 
       (*I. fázis - az új értékek kiszámítása*) 
       FORALL i:=1 TO n DO (*processz létrehozása minden sorhoz*) 
          VAR j: INTEGER; 
          BEGIN 
             FOR j:=1 TO n DO (*a négy szomszéd átlagának kiszámítása*) 
                B[i,j]:=(A[i-1,j]+A[i+1,j]+A[i,j-1]+A[i,j+1])/4; 
          END; 
       (*II. fázis - az új értékek visszamásolása az A-ba*) 
       FORALL i:=1 TO n DO (*az új értékek másolása B-ből A-ba*) 
          A[i]:=B[i]; 
    END; 
  Writeln(A[5,5]); 
  END. 

A programban 32 párhuzamos processz dolgozik. A speed-up . Tehát az overhead („haszontalan” feldolgozási idő vagy kapacitás) magas. Vegyük észre a II. fázis utasítását!

Kell egy olcsóbb szinkronizálási lehetőség: Barrier.

A barrier egy olyan pont a programban, ahol a párhuzamos processzek várnak egymásra. A barrier működését a következő ábra mutatja:

A barrier technika alkalmazása Jacobi iteráció esetén elvileg a következőképpen lehetséges:

  PROGRAM bjacobi; 
  CONST n=32; (*a tömb mérete*) 
    numiter=120; (*az iterációk száma*) 
  VAR A,B: ARRAY [0..n+1,0..n+1] OF REAL; 
    i,j,k: INTEGER; 
  BEGIN 
  FOR i:=0 TO n+1 DO (*a tömbértékek inicializációja*) 
    BEGIN 
       FOR j:=0 TO n+1 DO 
          Readln(A[i,j]); 
    END; 
  B:=A; 
  FORALL i:=1 TO n DO (*egy processz létrehozása egy sorhoz*) 
    VAR j,k: INTEGER; 
       BEGIN 
          FOR k:=1 TO numiter DO 
             BEGIN 
                FOR j:=1 TO n DO (*a négy szomszéd átlagának kiszámítása*) 
                   B[i,j]:=(A[i-1,j]+A[i+1,j]+A[i,j-1]+A[i,j+1])/4; 
                Barrier; 
                A[i]:=B[i]; 
                Barrier; 
             END; 
       END; 
  Writeln(A[5,5]); 
  END. 

Vegyük észre, hogy a FORALL utasítás kívülre került, és így a 32 processzt csak egyszer hozzuk létre.

A Multi-Pascalban nincs direkt Barrier utasítás, de többféleképpen is létre lehet hozni:

  1. Spinlockkal.

  2. Channel változókkal.

A spinlock megoldás a következő:

  PROGRAM PBjacobi;  
  CONST n=32; (*a tömb mérete*) 
    numiter=120; (*az iterációk száma*) 
  VAR A,B: ARRAY [0..n+1,0..n+1] OF REAL; 
    i,j,k: INTEGER; 
    count: INTEGER; 
    Arrival, Departure: SPINLOCK; 
  PROCEDURE Barrier; 
  BEGIN 
    (*Érkezési fázis - a beérkező processzek megszámlálása*) 
    Lock(Arrival); 
    count:=count+1; 
    IF count<n 
       THEN Unlock(Arrival) (*az Érkezési fázis folytatása*) 
       ELSE Unlock(Departure); (*az Érkezési fázis terminálása*) 
    (*Indulási fázis - az elhagyó processzek megszámlálá*) 
    Lock(Departure); 
    count:=count-1; 
    IF count>0 
       THEN Unlock(Departure) (*az Indulási fázis folytatása*) 
       ELSE Unlock(Arrival); (*az Indulási fázis terminálása*) 
  END; 
  BEGIN 
  count:=0; (*a „count” és a spinlock-ok inicializációja*) 
  Unlock(Arrival); 
  Lock(Departure); 
  FOR i:=0 TO n+1 DO (*a tömb értékeinek inicializációja*) 
    BEGIN 
       FOR j:=0 TO n+1 DO 
          Readln(A[i,j]); 
    END; 
  B:=A; 
  FORALL i:=1 TO n DO (*egy processz létrehozása egy sorhoz*) 
    VAR j,k: INTEGER; 
       BEGIN 
          FOR k:=1 TO numiter DO 
             BEGIN 
                FOR j:=1 TO n DO (*a négy szomszéd átlagának kiszámítása*) 
                   B[i,j]:=(A[i-1,j]+A[i+1,j]+A[i,j-1]+A[i,j+1])/4; 
                Barrier; 
                A[i]:=B[i]; 
                Barrier; 
       END; 
    END; 
  Writeln(A[5,5]); 
  END. 

A spinlock megoldás egy számláló változót használ.

A channel megoldás helyi szinkronizálást használ. Alapja az az észrevétel, hogy az új iteráció számításához csak az i, i-1, i+1 sor kell. Ezért a globális szinkronizálást kicseréljük egy lokálisra.

A channel megoldás a következő:

  PROGRAM PBjacob2; 
  CONST n=32; (*a tömb mérete*) 
    numiter=120; (*az iterációk száma*) 
  VAR A,B: ARRAY [0..n+1,0..n+1] OF REAL; 
    i,j: INTEGER; 
    higher, lower: ARRAY [1..n] OF CHANNEL OF INTEGER; 
  PROCEDURE LocalBarrier(i: INTEGER); 
  VAR dummy : INTEGER; 
  BEGIN 
    IF i>1 THEN higher[i-1]:=1; (*az i-1 processzhez való küldés*) 
    IF i<n THEN 
       BEGIN 
          lower[i+1]:=1; (*az i+1 processzhez való küldés*) 
          dummy:=higher[i]; (*az i-1 processztől való fogadás*) 
       END; 
    IF i>1 THEN dummy:=lower[i]; (*az i+1 processztől való fogadás*) 
  END; 
  BEGIN 
  FOR i:=0 TO n+1 DO (*a tömbértékek inicializációja*) 
    BEGIN 
       FOR j:=0 TO n+1 DO 
          Readln(A[i,j]); 
    END; 
  B:=A; 
  FORALL i:=1 TO n DO (*egy processz létrehozása egy sorhoz*) 
    VAR j,k: INTEGER; 
       BEGIN 
          FOR k:=1 TO numiter DO 
             BEGIN 
                FOR j:=1 TO n DO (*a négy szomszéd átlagának kiszámítása*) 
                   B[i,j]:=(A[i-1,j]+A[i+1,j]+A[i,j-1]+A[i,j+1])/4; 
                LocalBarrier(i); 
                A[i]:=B[i]; 
                LocalBarrier(i); 
             END; 
       END; 
  Writeln(A[5,5]); 
  END. 

2.7.1. Broadcasting és aggregálás

broadcasting (broadcast) = adatok közlése a processzekkel

aggregálás (aggregate) = adatok gyűjtése a processzektől

CAB algoritmus = Compute (számol), Aggregate (gyűjt), Broadcast (közöl) algoritmus

Minden processz számol, adatot szolgáltat egy aggregáló fázisban globális adathoz, amelyet visszaküld (broadcast-ol) a processzeknek.

A barrierek ennek a megvalósítására is jók.

Teszteljük a Jacobi módszer konvergenciáját azzal a kilépési feltétellel, hogy az egymást követő iteráltak eltérése legyen kisebb mint egy megadott küszöbérték.

A szekvenciális Jacobi módszer esetén ennek a megoldása:

  PROGRAM Jacobi2; 
  CONST n=32; (*a tömb mérete*) 
  tolerance=.01; 
  VAR A,B: ARRAY [0..n+1,0..n+1] OF REAL; 
    i,j: INTEGER; 
    change, maxchange: REAL; 
  BEGIN 
    FOR i:=0 TO n+1 DO (*a tömbértékek inicializációja*) 
       BEGIN 
          FOR j:=0 TO n+1 DO 
             Readln(A[i,j]); 
       END; 
  B:=A; 
  REPEAT (*számoljuk az új értékeket, amíg a megadott tolerancia értéket el nem érjük*) 
    BEGIN 
       maxchange:=0; 
       FOR i:=1 TO n DO 
          FOR j:=1 TO n DO 
             BEGIN (*számoljuk az új értékeket és cseréljük a régi értékekkel*) 
                B[i,j]:=(A[i-1,j]+A[i+1,j]+A[i,j-1]+A[i,j+1])/4; 
                change:=ABS(B[i,j]-A[i,j]); 
                IF change>maxchange THEN maxchange:=change; 
             END; 
       A:=B; 
    END; 
  UNTIL maxchange<tolerance; 
  Writeln(A[5,5]); 
  END. 

A fenti programban minden hálópontban az eltérés kiszámításra kerül és meghatározzuk ennek maximumát (aggregáljuk az adatokat).

A párhuzamos változatban a barrier a processzeket minden iteráció után szinkronizálja (összegyűjti őket, majd elereszti őket). Ezt a fázist lehet kihasználni:

  • az adatok összegyűjtésére

  • az adatok visszaküldésére (eleresztés alatt).

Például az első barrier megoldás felhasználásával a következő programot írhatjuk.

A programban a barrier módosításával definiálunk egy aggregáló rutint és ezzel egészítjük ki az eredeti programot.

  PROGRAM pjacobic; 
  CONST n=32; (*a tömb mérete*) 
  tolerance=.01; 
  VAR A,B: ARRAY [0..n+1,0..n+1] OF REAL; 
    i,j: INTEGER; 
    count: INTEGER; 
    Arrival, Departure: SPINLOCK; 
    globaldone: BOOLEAN; 
  PROCEDURE Barrier; 
  BEGIN 
    (*Érkezési fázis - a beérkező processzek megszámlálása*) 
    Lock(Arrival); 
    count:=count+1; 
    IF count<n THEN Unlock(Arrival) (*az Érkezési fázis folytatása*) 
    ELSE Unlock(Departure); (*az Érkezési fázis terminálása*) 
    (*Indulási fázis - az elhagyó processzek megszámlálása*) 
    Lock(Departure); 
       count:=count-1; 
    IF count>0 THEN Unlock(Departure) (*az Indulási fázis folytatása*) 
    ELSE Unlock(Arrival); (*az Indulási fázis terminálása*) 
  END; 
  FUNCTION Aggregate(mydone: BOOLEAN): BOOLEAN; 
  BEGIN 
    (*Érkezési fázis - a beérkező processzek megszámlálása*) 
    Lock(Arrival); 
    count:=count+1; 
    globaldone:=globaldone AND mydone; (*aggregációs lépés*) 
    IF count<n THEN Unlock(Arrival) (*az Érkezési fázis folytatása*) 
    ELSE Unlock(Departure); (*az Érkezési fázis terminálása*) 
    (*Indulási fázis - az elhagyó processzek megszámlálása*) 
    Lock(Departure); 
    count:=count-1; 
    Aggregate:=globaldone; (*visszaadja a „done” jelzőt a processznek*) 
    IF count>0 THEN Unlock(Departure) (*az Indulási fázis folytatása*) 
    ELSE  
       BEGIN 
          globaldone:=TRUE; (*beállítás a következő aggregáláshoz*) 
          Unlock(Arrival); (*az Indulási fázis terminálása*) 
       END; 
  END; 
  BEGIN 
    count:=0; (*a „count” és a spinlock-ok inicializálása*) 
    Unlock(Arrival); 
    Lock(Departure); 
    FOR i:=0 TO n+1 DO (*a tömbértékek inicializálása*) 
  BEGIN 
    FOR j:=0 TO n+1 DO 
       Readln(A[i,j]); 
  END; 
  B:=A; 
  FORALL i:=1 TO n DO (*egy processz létrehozása egy sorhoz*) 
    VAR j: INTEGER; 
       change, maxchange: REAL; 
       done: BOOLEAN; 
  BEGIN 
    REPEAT 
    maxchange:=0; 
    FOR j:=1 TO n DO (*a négy szomszéd átlagának kiszámolása*) 
       BEGIN 
          B[i,j]:=(A[i-1,j]+A[i+1,j]+A[i,j-1]+A[i,j+1])/4; 
          change:=ABS(B[i,j]-A[i,j]); 
          IF change>maxchange THEN maxchange:=change; 
       END; 
    Barrier; 
    A[i]:=B[i]; 
    done:=Aggregate(maxchange<tolerance);  
    UNTIL done; (*iterálás, amíg a globális terminálás el nem kezdődik*) 
  END; 
  Writeln(A[5,5]); 
  END. 

Feladatok:

1. Készítsünk el egy szekvenciális FUCTION nevű eljárást, amely kiszámolja egy egydimenziós tömb (azaz egy vektor) elemeinek összegét. Készítsünk továbbá egy olyan főprogramot, amely ezt az eljárást használja négy vektor esetén párhuzamosan!

2. Készítsen egy párhuzamos programot, hogy megtaláljunk egy adott értéket egy láncolt listában. Minden adategység a láncolt listában egy valós értékeket tartalmazó tömb. A programnak olyan párhuzamos processzt kell létrehoznia, amely megkeresi a tömböt. A következő programrészt fel kell használni a programhoz. A lista inicializálásával nem kell foglalkozni.

  PROGRAM SearchList; 
  TYPE arraytype = ARRAY [1..100] OF REAL; 
    pnttype = ^itemtype; 
    itemtype = RECORD 
       data: arraytype; 
       next: pnttype; 
    END; 
  VAR found: BOOLEAN; 
  listhead: pnttype; 
  PROCEDURE Search(inarray: arraytype); 
  VAR i: INTEGER; 
  BEGIN 
    FOR i:=1 TO 100 DO 
       IF inarray[i] = value THEN found:= TRUE; 
  END; 

3. Tekintsük a következő programot, amely párhuzamos technikával számol ki 2 hatványt.

  PROGRAM Power2; 
  VAR value, power: INTEGER; 
  BEGIN 
    Write('Power of two: '); (*feltételezzük, hogy a hatványkitevő nagyobb, mint 0*) 
    Readln(power); 
    value:= 1; 
    FORALL i:=1 TO power DO 
       value:=2 * value; 
    Writeln('Answer is ', value); 
  END. 

Készítsük el és futtassuk ezt a programot a Multi-Pascal rendszerben. Miért ad rossz végeredményt ez a program? (Használjuk a „Lock” és „Unlock” parancsokat a helyes működéshez!)

4. Készítsünk egy olyan Multi-Pascal eljárást, amely beszúr egy új elemet egy rendezett, láncolt listába! Készítsünk egy olyan főprogramot, amely ezt az eljárást hívja meg párhuzamos processzek segítségével egy megosztott listán!

5. Készítsünk egy olyan Multi-Pascal programot, amely meghatározza egy egész elemeket tartalmazó tömb legkisebb elemét. Tegyük fel, hogy a tömb elég nagy ahhoz, hogy hatékonyan alkalmazzunk párhuzamos processzeket a programban. Teszteljük a programot különböző méretű tömbökön is!

3. fejezet - PVM

3.1. Elosztott rendszerek

3.1. Definíció. Elosztott rendszernek tekinthetjük a több (nem osztott memóriával ellátott) számítógépen futó olyan alkalmazásokat, ahol az alkalmazás felhasználójának nem kell tudnia az alkalmazás kezeléséhez arról, hogy az alkalmazás több számítógépen fut.

Előnyeik:

  • Gazdaságos (több kisebb teljesítményű, olcsó komponensből állítható össze).

  • Megbízható (egy gép kiesésekor még nem dől össze a világ).

  • Erőforrásmegosztást támogatja (hálózat segítségével).

Hátrány:

  • Kevés a kiforrt szoftver-technológia (illetve a meglevő technológiák nagy része nem elosztott, azaz centralizált jegyeket visel magán - pl. a központi nyilvántartások).

  • Gyenge a tömegesen rendelkezésre álló hardware minősége.

3.2. A PVM bevezetése

A PVM (Parallel Virtual Machine) rendszert az Oak Ridge National Laboratories több más résztvevővel együtt fejlesztette ki. Céljuk egy olyan szoftver-rendszer kifejlesztése volt, mely támogatja elosztott alkalmazások készítését (UNIX esetén). A PVM lehetőséget ad több - például TCP/IP protokollal hálózatba kapcsolt - számítógép erőforrásainak összevonására, ezzel egy „virtuális számítógép” létrehozására, amelyben a futó programok egységes interfésszel kapcsolódhatnak egymáshoz.

3.2.1. A PVM rendszer szerkezete

A rendszer két főbb komponensből áll: egyrészt egy eljáráskönyvtárból, amit a PVM-et használó alkalmazásokhoz hozzá kell linkelni, másrészt pedig van egy önállóan futtatható PVM-démon része, amely gépenként és felhasználónként egy-egy példányban fut. Ez utóbbi tartalmazz a PVM szolgáltatások tényleges implementációját. A PVM elterjedtségének egyik oka a szabad elérhetősége, valamint az, hogy számos hardvergyártó a többprocesszoros/elosztott számítógépeihez ilyen interfészt (is) biztosít.

Ha például van két számítógépünk, amelyekből a PVM segítségével egyetlen virtuális gépet akarunk csinálni, azt megtehetjük úgy, hogy mindkét gépen elindítjuk a PVM-démont, majd el kell indítani a PVM-et használó alkalmazásokat.

Ha egy gépet több különböző felhasználó akar egy virtuális géppé összekapcsolni, akkor mindegyik felhasználónak saját PVM-démont kell indítania, viszont egy felhasználónak elég csak egy PVM-démont futtatnia, ez megszervezi a felhasználó összes (akár több) elosztott alkalmazásának futtatását.

Elosztott alkalmazások készítésekor az egyes alkalmazás-komponenseket különálló programokként (mondjuk UNIX processzként) kell megírni, azok szinkronizációjára/adatcseréjére kell a PVM-et használnunk, azaz a PVM nem biztosít eszközöket összetett alkalmazások komponenseinek automatikus párhuzamosításához.

A PVM lehetőséget nyújt arra, hogy egy „mezei” UNIX processz bekapcsolódjon a virtuális gépbe, valamint arra is, hogy egy PVM-be kapcsolódott UNIX processz (a továbbiakban PVM-processz) kilépjen a PVM által biztosított virtuális gépből, valamint lehetőség van PVM-processzek egymás közti kommunikációjára, PVM-processzek úgynevezett PVM-processzcsoportokba kapcsolására, és biztosított egy-egy ilyen csoport összes tagja részére az üzenetküldés is, valamint egy-egy csoportba új tag felvétele és tagok törlése a csoportból.

3.2.2. A PVM konfigurációja

A PVM telepítése: LINUX/UNIX rendszerekben nem okoz gondot, hiszen a csomagok között könnyen megtalálható. Például az Opensuse 11.x disztribúció esetén a Yast csomagkezelő segítségével telepíthető. WINDOWS operációs rendszer esetén is van telepítő, de annak beállítása nagyon körülményes, ezért csak a LINUX operációs rendszerek esetén tárgyaljuk a PVM konfigurálási lehetőségeit.

A LINUX esetén a megfelelő állomány pl. bash burok esetén a .bashrc, melynek módosítása a következő módon történhet (beszúrása az alábbiaknak, majd a

sh .bashrc futtatása egy konzolban, azaz parancssoros üzemmódban):

  PVM_ROOT=/usr/lib/pvm3 
  PVM_ARCH="LINUX" 
  PVM_RSH=/usr/bin/ssh  
  PVM_TMP=/tmp 
  #putting pvm bin to the path 
  #PATH=$PVM_ROOT/bin:$PATH 
  #make these variables available for use in the shell 
  export PVM_ROOT PVM_ARCH PVM_RSH PVM_TMP 
  export PATH=$PATH:$PVM_ROOT/lib 
  export PATH=$PATH:$PVM_ROOT/bin/$PVM_ARCH 
  export PATH=$PATH:$HOME/pvm3/bin/$PVM_ARCH 

Érdemes módosítani a pvm3 konfigurációs állományában a kapcsolat típusát (rsh helyett ssh).

Az /usr/lib/pvm3/conf/LINUX.def állományban levő rsh-t ssh-ra kell javítani. A javítandó sor a következő:

  ARCHCFLAGS = -DSYSVSIGNAL -DNOWAIT3 -DRSHCOMMAND=\"/usr/bin/ssh\"  

A fenti .bashrc-be történő beállítás beszúrásakor figyelni kell, hogy milyen disztribúció változat, azaz architektúra fut a számítógépen. Ugyanis pl. LINUX esetén is lehet 32 vagy 64 bites változat. A 32 bites változat esetén „LINUX”, a 64 bites esetben ez „LINUX64”. Ez az érték a PVM_ARCH=-ben jelenik meg.

További érdekesség, hogy a fenti beállítás segítségével nem kell használni a ./ programindítási részt, hanem csak elég a program nevét (ill. argumentumait) beírni a parancssorba. Erről a export PATH=$PATH:$HOME/pvm3/bin/$PVM_ARCH sor gondoskodik.

3.2.3. A PVM elindítása

A PVM virtuális gép felélesztéséhez el kell indítanunk az ehhez szükséges programot: a PVM-démont (neve pvmd3) vagy pedig a PVM konzolt (neve pvm). Az utóbbi a virtuális gép felélesztése mellett elindít egy PVM-konzolt, amivel interaktivan kapcsolhatunk és törölhetünk gépeket a virtuális gépből, interaktivan indíthatunk (és állíthatunk le) PVM-processzeket, és talán legfontosabb parancsa a help (amivel a további parancsok használatához kérhetünk segítséges a rendszertől).

A pvm indítását, a help parancs eredményét mutatja a következő ábra:

Látható, hogy egy konzolban való indításkor (ha minden rendben van) a pvm> promptot kapjuk vissza.

Például fontos parancsok a következők:

  • halt: terminálja az összes futó PVM-processzünket, és kilép a PVM-ből, leállítja a PVM-démont,

  • quit: kilépés a pvm konzolból, de a démon tovább fut,

  • conf: megadja a virtulis géphez csatlakozó gépeket, azok architektúráival együtt,

  • add: egy másik gép/munkaállomás (host) csatlakoztatása/hozzáadása a virtuális géphez,

  • delete: egy gép (host) törlése a virtuális gépből,

  • spawn: új PVM-processzt indít,

  • kill: egy futó PVM-processzt állít le,

  • reset: terminálja az összes futó PVM-processzt a konzol kivételével, a PVM-démonokat pedig nem bántja,

  • version: az aktuális pvm verzióját adja meg.

A programok indításához szükség van a következőkre. Ha pl. hallgato névvel jelentkezünk be a rendszerbe, akkor a saját „\home\hallgato” jegyzékkel rendelkezünk. Itt található pl. a .bashrc állomány is. Hozzunk létre egy „pvm3” jegyzéket, majd azon belül „bin” és „src” jegyzékeket.

Például: mkdir bin.

A „bin” (binary) jegyzékbe kerülnek a PVM által futtatható programok, míg az „src”-be (source) a programok forrásait kell bemásolnunk. Ezeket kell majd megfelelő módon lefordítani, a fordítás végén pedig a futatható kód a „bin”-be kerül.

3.3. PVM szolgáltatások C nyelvű interfésze

3.3.1. Alapvető PVM parancsok

A PVM szolgáltatások igénybevehetők C, C++ és Fortran programozási nyelven készült programokból - eljáráshívások formájában. Most a C-ből hívható legfontosabb eljárások ismertetése következik. A későbbiekben láthatunk majd Fortran nyelven készült programot is, de alapvetően a C nyelvet támogatjuk.

PVM-processzek kezelése:

Minden egyes PVM-processz rendelkezik egy egész típusú processz-azonosítóval (a UNIX processzek azonosítójához hasonló a szerepe, és fontos megemlíteni, hogy a PVM-processz azonosítónak semmi köze sincs a UNIX processzeinek processz-azonosítójához). A PVM-processz azonosítókat a továbbiakban tid-del jelöljük.

1. pvm_spawn: Egy új PVM-processzt a pvm_spawn eljárással hozhatunk létre. Ennek alakja a következő:

int pvm_spawn(char *task, char **argv, int flag, char *where, int ntask, int *tids);

Az első argumentum (task) az elindítandó PVM-processzt tartalmazó végrehajtható fájl nevét adja meg.

A második (argv) az átadandó program-argumentumokat tartalmazza.

A negyedik paraméter kijelöli, hogy hol kell elindítani az új PVM-processzünket (pl. CPU-architektúrára tartalmazhat utalást, de gyakran a processzt indító számítógép CPU-ján kell az új PVM-processzt futtatni, ezért gyakori, hogy az ezt kijelölő PvmTaskDefault (0 értékű) konstans adják itt meg). Amennyiben az alkalmazásnak speciális igényei vannak az új PVM-processz elindításával kapcsolatban, akkor azt a harmadik flag argumentumban jelezheti, és ilyenkor kell a where argumentumban megadni azt, hogy (fizikailag) hol is akarjuk a programunkat elindítani. Az ntask paraméterben adhatjuk meg, hogy hány példányban akarjuk a PVM-processzt elindítani, majd a rendszer a tids tömbben adja vissza az elindított PVM-processzek azonosítóit (tid-jeit). A függvény visszatérési értéke a ténylegesen elindított új PVM-processzek száma, azaz egy egész érték. Ha nem sikerül az indítás ,akkor 0-t kapunk vissza.

Használatára példa:

numt = pvm_spawn( "program", NULL, PvmTaskHost, "host", 1, &tids[0]);

Ez a parancs elindít a host nevű hoston egy új PVM-processzt a program nevű programból. Megjegyezzük, hogy a PvmTaskHost paraméter arra utal, hogy kijelöljük a negyedik argumentumban, hogy melyik hoston akarjuk elindítani az új PVM-processzt. Ha itt PvmTaskDefault értéket adtunk volna meg, akkor a PVM rendszer maga választhatott volna egy hostot, ahol a programot elindítja. A tids változó egy egészeket tartalmazó vektor.

2. pvm_exit: Egy PVM-processz a pvm_exit eljárás végrehajtásával léphet ki a PVM felügyelete alól.

Alakja:

int info = pvm_exit( void )

Nincs argumentuma, a visszatérési értéke (info) egész. Ha viszont kisebb, mint 0, akkor hiba történt.

3. pvm_mytid: Egy PVM-processz a saját tid-jét ezzel kérdezheti le.

Alakja:

int tid = pvm_mytid( void )

Nincs paramétere. A függvény visszatérési értéke az új PVM-processz tid-je. Ha a tid kisebb, mint 0, akkor hiba történt.

4. pvm_parent: Egy PVM-processz a szülőjének a tid-jét kérdezheti le.

Alakja:

int tid = pvm_parent( void )

Visszatérési értéke a szülőjének a tid-je.

5. pvm_kill: Leállítja azt a PVM-processzt, amelynek a tid-jét megadtuk az argumentumában.

Alakja:

int info = pvm_kill( int tid )

Visszatérési értéke (info) adja meg a sikeres végrehajtást.

6. pvm_joingroup: A hívó processz bekerül egy csoportba.

Alakja:

int inum = pvm_joingroup( char *group )

Argumentuma (group) a csoport neve lesz, melyhez a hívó processz csatlakozik. Visszatérési értéke a csatlakozás sikerét adja meg (lásd még a 3.3.6. alszakaszt).

7. pvm_barrier: A barrier eljárás, azaz a hívó processzt blokkolja, amíg az egy csoportba tartozó processzek meg nem hívják az eljárást.

Alakja:

int info = pvm_barrier( char *group, int count )

Ebben az esetben az első paraméter (group) a csoport neve, a második pedig egy egész szám (count), mely a csoportot elhagyó processzek számát jelenti (a blokkolás feloldásához). Általában ez a csoporthoz tartozó processzek számával egyezik meg (klasszikus barrier esetben). A visszatérési érték a végrehajtás sikerét adja meg ismét (lásd még a 3.3.6. alszakaszt).

Például:

inum = pvm_joingroup( "worker" );

...

info = pvm_barrier( "worker", 5 );

8. pvm_lvgroup: A hívó processz elhagyja a csoportot.

Alakja:

int info = pvm_lvgroup( char *group )

Argumentuma és visszatérési értéke a pvm_joingroup-pal megegyezik (lásd még a 3.3.6. alszakaszt).

3.3.2. Kommunikáció

A PVM eszközei közt vannak primitív, UNIX signal-okat, valamint vannak összetettebb adatszerkezetek/adatterületek processzek közötti mozgatására is eszközök. Megjegyezzük, hogy a Linux implementáció signal-kezelése nem minősül tökéletlennek, így azon megbízható kommunikációt biztosítanak a Linux signalok használata.

1. pvm_sendsig: A függvény segítségével egy adott signalt (UNIX signalt) küldhetünk valamelyik PVM-processznek. A függvény első paramétere a kívánt PVM-processz PVM tid-je (ez nem ugyanaz, mint a kill() UNIX rendszerhívás, ugyanis az nem képes pl. „hálózaton keresztül” más hoston futó processzeknek signal-t küldeni). A második paraméter egy egész signal szám.

Alakja:

int info = pvm_sendsig( int tid, int signum )

Például:

tid = pvm_parent();

info = pvm_sendsig( tid, SIGKILL);

2. pvm_notify: A függvény alakja a következő:

int info pvm_notify(int about, int msgtag, int ntask, int *tids)

Hatása pedig az, hogy a későbbiekben az about argumentumban specifikált esemény bekövetkeztekor egy olyan üzenetet küld a tids vektorban megadott tid-del azonosított PVM-processzeknek, amely üzenet msgtag része (ez az üzenetek egy komponensét jelöli) megegyezik a függvény második argumentumában megadott értékkel.

Az about paraméter lehet PvmTaskExit (ekkor az esemény egy PVM processz befejeződése), PvmHostDelete (ekkor az esemény egy bekapcsolt host kiesése lesz), PvmHostAdd (ekkor az esemény egy új host PVM-be kapcsolása).

Például:

info = pvm_notify( PvmTaskExit, 9999, ntask, tids );

3.3.3. Hibaüzenetek

A legutolsó sikertelen (nem elvégezhető) PVM függvényhívás sikertelenségének az okát a

pvm_perror(char *msg) függvénnyel írathatjuk ki - egy programozó által megadott megjegyzés-üzenet kíséretében.

A PVM-processzek egymás közötti üzenetátadása:

A PVM processzek egymás közt üzenetekkel kommunikálhatnak. E kommunikáció első lépéseként inicializálni kell az üzenetet tartalmazó buffert, majd fel kell tölteni az elküldendő üzenettel, végül el lehet küldeni a címzettnek. Az elküldés után az üzenetet tartalmazó buffer felszabadítható. Még meg kell említeni azt is, hogy az üzenet fogadása kétféleképpen történhet: blokkolva vagy blokkolás nélkül (ez azt jelenti, hogy érkezett üzenet hiányában a üzenetre váró PVM processz továbbfut-e üzenet beolvasása nélkül vagy megáll és vár, amíg egy üzenetet kap).

Az üzenetátadás során heterogén hálózatban (ahol több, különféle architekturájú host is előfordulhat) felmerülhetnek adatok reprezentációjának eltéréséből származó problémák (pl. a 2 vagy 4 byteos egész számoknál a reprezentáció bytesorrendje eltérhet). A PVM rendszerben ezt a problémát úgy lehet megoldani, hogy az adatokat elküldés előtt egy hálózati („külső”, szabványosított) ábrázolási formára kell alakítani, az adat/üzenet fogadáskor pedig a fogadó állomás feladata lesz a hálózati ábrázolási formára történő adatkonverzió. A PVM rendszer hálózati adatábrázolásra a Sun XDR (eXternal Data Representation) ábrázolásmódját alkalmazza.

3.3.4. Buffer(de)allokálás

1. pvm_mkbuf: A PVM rendszerben üzenetküldés céljából több üzenet-buffert is allokálhatunk, és ezek közül kijelölhetjük, hogy melyik legyen az aktív küldési illetve aktív fogadási buffer, azaz melyikbe akarunk adatokat bepakolni (adatelküldés céljából) illetve melyikből akarunk adatokat kiolvasni (egy megkapott üzenetből). Megjegyezzük, hogy az adatbeolvasási és az adatküldési buffer megegyezhet.

Egy új üzenet-buffert a pvm_mkbuf függvénnyel hozhatunk létre.

Alakja:

int bufid = pvm_mkbuf( int encoding )

Ennek egyetlen argumentuma van (egy egész szám, amely a buffer tartalmának az összeállítási módját, értelmezését/reprezentációját írja le), visszatérési értéke pedig (szintén egy egész szám) a létrehozott buffer egyértelmű azonosítóját tartalmazza.

A pvm_mkbuf argumentumának értéke a következő (makrókkal definiált konstans) értékek valamelyike lehet:

PvmDataDefault: A bufferben levő adatok XDR formában lesznek elküldve.

PvmDataRaw: A bufferben levő adatok nem lesznek konvertálva - a bufferben levő byte-ok a bufferbeli sorrendjükben lesznek elküldve, az üzenet címzettje ugyanabban a sorrendben fogja megkapni.

PvmDataInPlace: A buffer csak pointereket tartalmaz az elküldendő adatokra illetve azok hosszát.

Például:

bufid = pvm_mkbuf( PvmDataRaw );

/* send message */

info = pvm_freebuf( bufid );

2. pvm_initsend: Ezzel az adatküldési buffert ki lehet üríteni, és ez egyben inicializálni is fogja azt.

Alakja:

int bufid = pvm_initsend( int encoding )

Ennek egyetlen argumentuma van: ugyanaz, mint az előzőekben ismertetett pvm_mkbuf függvény argumentuma (ugyanazokat az értékeket veheti fel, és ugyanazt kell vele megadni).

Visszatérési értéke az egyedi buffer-azonosító. Megjegyezzük, hogy ez a függvény meghívja a pvm_mkbuf függvényt egy új buffer létrehozására, és az újonnan létrehozott adatbuffert kijelöli új aktív adatküldési buffernek (annyivel tud többet). Továbbá megjegyezzük, hogy ez a függvény felszabadítja az aktív küldési buffert a pvm_freebuf függvénnyel, így ez előtt azt nem kell végrehajtani - a pvm_initsend függvények ciklikus végrehajtása nem eredményezi a memória elfogyását.

Például:

bufid = pvm_initsend( PvmDataDefault );

info = pvm_pkint( array, 10, 1 );

msgtag = 3 ;

info = pvm_send( tid, msgtag );

3. pvm_freebuf: Egyetlen argumentuma az eldobandó (továbbiakban már nem használt) üzenet-buffer egyedi azonosítója.

Alakja:

int info = pvm_freebuf( int bufid )

A függvény eredményeként a paraméterben kijelölt buffer által lefoglalt memóriaterületeket a rendszer felszabadítja, visszarakja a memória szabad-listára.

Például:

bufid = pvm_mkbuf( PvmDataDefault );

...

info = pvm_freebuf( bufid );

4. pvm_getsbuf: Nincs argumentuma. Visszatérési értéke az aktuális (aktív) küldési buffer azonosítója.

Alakja:

int bufid = pvm_getsbuf( void )

5. pvm_getrbuf: Nincs argumentuma. Visszatérési értéke az aktuális (aktív) fogadási buffer azonosítója (ez az, amelyikből adatokat tudunk kiolvasni a később bemutatásra kerülő adatbeviteli függvényekkel).

Alakja:

int bufid = pvm_getrbuf( void )

6. pvm_setsbuf: Meg lehet változtatni vele az aktív küldési buffert. Egyetlen argumentuma van, amely megadja az új aktív küldési buffer egyedi azonosítóját kell, hogy tartalmazza, és visszaadja a korábban aktív küldési buffer azonosítóját.

Alakja:

int bufid = pvm_getrbuf( void )

7. pvm_setrbuf: Meg lehet változtatni vele az aktív fogadási buffert. Egyetlen argumentuma van, amely megadja az új aktív fogadási buffer egyedi azonosítóját kell, hogy tartalmazza, és visszaadja a korábban aktív fogadási buffer azonosítóját.

Alakja:

int oldbuf = pvm_setrbuf( int bufid )

I. Adatbepakolás

Az adatok elküldése előtt az elküldendő adatokat be kell pakolni a küldési bufferbe. Erre valók az alábbi függvények:

pvm_pkbyte, pvm_pkcplx, pvm_pkdcplx, pvm_pkdouble, pvm_pkfloat, pvm_pkint, pvm_pklong, pvm_pkshort, pvm_pkstr.

Az utolsó kivételével mindegyiket a következő paraméterekkel hívhatjuk:

int pvm_pkXXX( XXXtype *ptr, int nitems, int stride);

Ahol XXXtype típus a pvm_pkXXX-ben levő adattípusból származtatható. Az elküldendő adatokra az első argumentum mutat. A második argumentum adja meg, hogy az első argumentum által mutatott helyről hány adatelemet kell az aktuális küldési bufferbe átpakolni (a megfelelő adatábrázolási bufferbe). A stride argumentum értékben az adatpakolási lépéshosszt lehet megadni (ez az érték egyszerűen rendre hozzá lesz adva az első argumentumban megadott pointerhez (összesen nitems-szer a szokásos C pointer-aritmetikával), és az így érintett memóriacímeken levő adatokat pakolja be a küldési bufferbe).

A pvm_pkcplx függvény komplex típusú adatot kezel, amelyet a PVM rendszerben deklaráltak egy rekord típusként, melynek két komponense van: r a valós, és f az imaginárius rész neve (mindkettő float típusú - a dcplx esetén pedig double típusú).

A pvm_pkstr függvény prototípusa a következő: int pvm_pkstr( char *str );

Megjegyezzük, hogy míg a Sun XDR lehetőséget nyújt rekord/unió típusú adat ábrázolására, addig itt erre nincs automatikusan megoldás. Helyette a struktúra elemeit nekünk kell egyenként feldolgoznunk - a típusuknak megfelelő módon.

II. Adatkipakolás

Ez nagyon hasonlít az adatbepakolásra. Ehhez a következő függvényeket használhatjuk:

pvm_upkbyte, pvm_upkcplx, pvm_upkdcplx, pvm_upkdouble, pvm_upkfloat, pvm_upkint, pvm_upklong, pvm_upkshort, pvm_upkstr.

Ezeknek a függvényeknek az argumentumaik ugyanazok, mint az adatbepakoló párjuknál (de itt az első argumentum azt adja meg, hogy a kipakolt adatokat hová akarjuk tenni a memóriában). Ezek a függvények az aktív fogadási bufferből olvasnak adatokat.

3.3.5. Adatátvitel (küldés, fogadás)

Itt a PVM üzenetküldéséről ill. üzenetfogadásáról lesz szó. Az üzenetfogadó eljárás elég dinamikus ill. kiterjeszthető abban az értelemben, hogy definiálhatunk akár saját szűrőfeltételeket is, amelyekkel kiválaszthatjuk a következő „beolvasandó” („fogadandó”) üzenetet a PVM processzünk címére küldött üzenetek közül.

I. Adatküldés

A legegyszerűbb adatküldő eljárás a pvm_send, melynek első paramétere a címzett PVM-processz tid-je, a második argumentuma pedig egy egész érték (msgtag), amely az üzenetek osztályozására használható. Megadható vele, hogy a küldött üzenet milyen osztályba tartozik. (Később látni fogjuk, hogy az adatfogadási műveleteknek meg kell adni, hogy mely osztálybeli üzenetre várunk, ezért jó ez a mező.) Megjegyezzük, hogy az msgtag argumentum (az üzenet osztályának az azonosítója) is konvertálva lesz hálózati (külső) adatábrázolási formára.

Egy művelettel ugyanazt az üzenetet több PVM processznek is elküldhetjük:

int pvm_mcast( int *tids, int ntask, int msgtag) alakú az erre való művelet: itt az első argumentum egy egész (int) tömb azonosítója, amely a címzett PVM processzek azonosítóit tartalmazza; a benne levő taszkok számát pedig a második (ntask) paraméterben kell megadni.

II. Adatfogadás

Az adatfogadó műveletnek két változata van: az egyik blokkol és vár egy adott osztályba tartozó üzenet érkezésére, a másik pedig visszatér adatbeolvasás nélkül akkor, ha nincs a kívánt osztályba tartozó beérkezett üzenet.

A nem-blokkoló eljárás a következő alakú:

int pvm_nrecv( int tid, int msgtag)

Ahol tid argumentumban megadhatjuk, hogy kitől várunk üzenetet; a msgtag-ban pedig megadhatjuk a várt üzenet osztályát. Bármelyik argumentumban megadhatunk -1 értéket, ami azt jelenti, hogy az argumentum értéke nem érdekel minket.

A blokkoló eljárás a következő alakú:

int pvm_recv( int tid, int msgtag)

Az argumentumai ugyanazok mint a nem-blokkoló párjánál. Mindkét függvény visszatérési értéke a megkapott üzenet üzenet-bufferének az egyedi azonosítója, ha sikerült üzenetet olvasni. Ha nem volt beolvasandó üzenet, akkor 0-t ad vissza.

A hasonló paraméterezésű pvm_probe függvénnyel megnézhetjük, hogy érkezett-e adott küldőtől adott osztálybeli üzenet, ha igen, akkor visszakapjuk az üzenethez tartozó buffer azonosítóját, ahova a rendszer az üzenetet beolvassa. Az érkező üzenetet bennmarad a „feldolgozatlan” üzenetek között (egy újabb hívása ugyanazt az üzenetet látja majd).

Egy üzenetbufferről (a benne tárolt üzenetről, osztálykódjáról, hosszáról) információkat a pvm_bufinfo függvénnyel nyerhetünk.

3.3.6. Processz-csoportok

A PVM rendszer lehetőséget nyújt processz-csoportok szervezésére. Egy PVM processz bármikor beléphet egy csoportba - ezzel csoporttaggá válik - illetve bármikor kiléphet egy csoportból. A csoportoknak nevet adhatunk, a csoportnév ASCII karakterek sorozatából állhat. A csoportokkal kapcsolatban több művelet is van: üzenet küldése az összes csoporttag számára (az üzenet küldője nem kell, hogy tagja legyen a csoportnak) valamint lehetőség van a csoporttagok állapotának szinkronizálására.

Egy PVM-processz a pvm_joingroup függvényhívással lehet tagja egy processz-csoportnak. A függvény egyetlen paramétere a csoport neve. A függvény visszatérési értéke egy csoporton belüli azonosító szám (instance id. - példány-azonosító).

Egy PVM-processz a pvm_lvgroup függvényhívással léphet ki egy csoportból. Egyetlen argumentuma a csoport neve, amiből a PVM-processz ki akar lépni.

A fent említett példány-azonosító (instance id.) és a PVM processz-azonosítók (tid-ek) kapcsolatát a pvm_gettid és a pvm_getinst függvény teremti meg.

Egy csoport résztvevőinek a számát a pvm_gsize függvénnyel kérdezhetjük le: egyetlen argumentuma a csoport neve. Egy csoport összes tagjának küldhetünk egy művelettel üzenetet a pvm_bcast függvénnyel. Ennek alakja a következő:

int pvm_bcast( char *group, int msgtag )

(itt az első argumentum a csoport neve; a második argumentum az üzenet típusának az azonosítója; visszatérési értéke negatív ha valami hiba történt.) Megjegyezzük, hogy a küldő nem kapja meg az így küldött üzenetet, ha tagja a megadott nevű csoportnak.

Egy csoport szinkronizálására szolgál a pvm_barrier függvény. Ennek az alakja a következő:

int pvm_barrier(char *group, int count),

ahol az első a csoportnév; a második argumentum pedig egy szám.

A függvény eredményeként a hívó PVM-processz blokkolni fog egészen addig, amíg (count) darab csoporttag meg nem hívja e függvényt. A többi processznek ugyanezekkel az argumentumokkal kell meghívnia e függvényt.

Ezzel például megoldható az, hogy egyszerre „induljanak el” egy processz-csoport tagjai: például tekintsünk egy olyan szolgáltatást, ahol a szolgáltatást 17 (ez a szám csak példa) darab PVM-processz nyújtja egy processz-csoportba szerveződve. Azt szeretnénk, hogy a szolgáltatást nyújtó processzek ugyanabban az állapotban legyenek a működésük folyamán. Ehhez az is szükséges, hogy kezdetben már szinkronizálják az állapotukat és a hozzájuk érkező üzeneteket mind megkapják.

3.2. Megjegyzés. Nem kötelező, de ajánlott betartani a következőket: csoportok kommunikációját általában csoporton belül használjuk a PVM (ill. más rendszer) segítségével készült termékekben a „felhasználó” felé a szolgáltatásokat ne tisztán PVM eszközökkel nyújtsuk. A „felhasználónak” ne kelljen tudnia az implementáció részleteiről.

3.4. Példaprogramok

3.4.1. Programok futtatása

A PVM függvénykönyvtár felhasználásával pl. C programnyelvben megírt programok futtatásához szükség van egy Makefile.aimk segédállományra, melynek segítségével és az aimk programmal a fordítás és a futtatás könnyen elvégezhető.

A későbbiekben többféle Makefile.aimk állományt fogunk használni. Ezek között a különbség nemcsak a fordítandó állományok nevéből adódik (lásd a „BINS = ” részt), hanem abban is, hogy milyen függvényeket akarunk használni, pl. pvm_joingroup. Ilyen esetben a megfelelő Makefile.aimk-ban további pl. -lfpvm3 -lgpvm3 hivatkozások is megjelenhetnek. A korrekt fordítás és futtatás érdekében minden egyes programhoz megadjuk a megfelelő Makefile.aimk-t is.

A program(ok) fordítása két lépcsőben történik meg. A forrás (pl. munka.c) és a megfelelő Makefile.aimk az src jegyzékben van. Konzolban, parancssorból indítsuk az aimk-t (fordítóprogram, a Makefile.aimk szerint működik), majd annak sikeres lefutása végén az aimk links-et. Ennek eredményeképpen kerül a futtatható változat a bin jegyzékbe, ahol már a pvm konzol által elérhető és futtatható. Természetesen ezeket a programokat nem szükséges a konzolon keresztül indítani pl. paraméterátadás esetén.

Természetesen van arra is lehetőség, hogy ezen kiegészítés nélkül fordítsuk le a forrásunkat.

Például, ha egy munka.c forrást akarunk lefordítani az aimk kihagyásával, akkor a következőket kell egy konzol parancsssorába begépelnünk:

cc -L~/pvm3/lib/LINUX munka.c -lpvm3 -o munka

Ekkor a megfelelő C fordítót hívtuk meg a PVM függvénykönyvtár (jelen esetben a -lpvm3 kapcsoló/opció) segítségével.

A további alfejezetekben számos érdekes mintaprogramot mutatunk be a megfelelő Makefile.aimk-kal együtt. A programok forrása megtalálható a PVM-ről írt könyvben (lásd a [2]-t) vagy letölthető a PVM book oldalról kiindulva (innen a teljes könyv .ps formátumban is letölthető).

3.4.2. Hello

Az első mintaprogram egy egyszerű példa a PVM programozásból. Két részből áll, egy hello.c és egy hello_other.c forrásból. A megfelelő fordítás után kapjuk a futtatható állományokat hello és hello_other néven. Konzol elindításával (begépeljük a pvm prompt után: spawn --> hello) vagy a pvm démon segítségével (parancssorból: ./hello vagy hello) indíthatjuk el a hello programot (a „mester”-t (master)). Ekkor a program kiírja a saját tid-jét (a pvm_mytid segítségével kapja meg), majd elindítja a hello_other programot (processzt!) a pvm_spawn segítségével. Sikeres indítás után jut hozzá egy blokkolt üzenethez a pvm_recv-vel, melyet a pvm_upkstr csomagol ki, végül a PVM rendszerből a pvm_exit távolítja el. A „szolga” (slave) hello_other program elindításával lekérdezésre kerül a szülő tid értéke, majd a pvm_initsend (buffer allokálás), a pvm_pkstr (csomagolás) és a pvm_send (küldés) segítségével elküldésre kerül a „mester” hello programnak a gépnév (hostname) és a „hello, world from” szövegrész. Megfigyelhető a PVM függvénykönytár betöltése az

  #include "pvm3.h" 

által mindkét programforrás esetén.

A hello, hello_other programok fordításához a következő Makefile.aimk-t használjuk:

  # Makefile.aimk - PVM programok fordításához  
  # 
  # Használat: 
  # Ezt a Makefile-t egy tetszőleges könyvtárba el lehet helyezni, amely 
  # C vagy C++ nyelvű forrásfile-okat tartalmaz. Minden forrásfile-ból egy 
  # programfile-t készít, az eredményt a LINUX alkönyvtárba helyezi el. 
  # 
  # Az `aimk' parancs az összes, alább megadott programot lefordítja. 
  # Az `aimk links' parancs linkeket készít a lefordított programokra, 
  # hogy a futtatás során megtalálja őket a PVM. Ezt a parancsot akkor 
  # kell kiadni, ha a programok listája megváltozik. 
  # A lefordítandó programok nevei (szóközzel elválasztott felsorolás) 
  BINS = hello hello_other 
  # Kapcsolók a fordítónak 
  OPTIONS = -W -Wall 
  # Kapcsolók a linkernek 
  LOPT = 
  #################################################################### 
  # # # Innentől semmit nem kell módosítani # # # 
  #################################################################### 
  BDIR = $(HOME)/pvm3/bin 
  XDIR = $(BDIR)/$(PVM_ARCH) 
  CFLAGS = $(OPTIONS) -I$(PVM_ROOT)/include $(ARCHCFLAGS) 
  LIBS = -lpvm3 $(ARCHLIB) 
  LFLAGS = $(LOPT) -L$(PVM_ROOT)/lib/$(PVM_ARCH) 
  default: $(BINS) 
  % : ../%.c 
    $(CC) $(CFLAGS) -o $@ $< $(LDFLAGS) $(LIBS) 
  % : ../%.cc 
    $(CXX) $(CFLAGS) -o $@ $< $(LDFLAGS) $(LIBS) 
  $(XDIR): 
    mkdir -p $(XDIR) 
  links: $(XDIR) 
    @( CURRENT=`pwd` ;\  
    echo cd $(XDIR) ;\  
    cd $(XDIR) ;\  
    for i in $(BINS) ;\  
    do \  
       echo ln -sf $$CURRENT/$$i $$i ;\  
       ln -sf $$CURRENT/$$i $$i ;\  
    done ; )  
  clean:  
    - rm *.o  
    - rm $(BINS)  
    - (cd $(XDIR); rm $(BINS)) 

I. A hello.c programforrás

  #include "pvm3.h"  
  #include <stdio.h> 
  int main() 
    { 
    int cc, tid, msgtag; 
    char buf[100]; 
    printf("i'm t%x\n", pvm_mytid()); 
    cc = pvm_spawn( "hello_other", (char**)0, 0, "", 1, &tid ); 
    if (cc== 1) { 
       msgtag = 1; 
    pvm_recv(tid, msgtag); 
    pvm_upkstr(buf); 
    printf("from t%x: %s\n", tid, buf);  
    } else 
    printf("can't start hello_other\n"); 
    pvm_exit();  
    return 0; 
  } 

II. A hello_other.c programforrás

  #include "pvm3.h" 
  #include <string.h> 
  #include <unistd.h> 
  int main() 
    { 
    int ptid, msgtag; 
    char buf[100]; 
    ptid = pvm_parent(); 
    strcpy(buf, "hello, world from "); 
    gethostname(buf + strlen(buf), 64);  
       msgtag = 1; 
    pvm_initsend(PvmDataDefault); 
    pvm_pkstr(buf); 
    pvm_send(ptid, msgtag); 
    pvm_exit(); 
    return 0; 
  } 

A programok fordítását és futtatását a következő animáció mutatja be (a pvm démon már fut a háttérben): hello.pvm.swf

3.4.3. Forkjoin

A következő programok esetén már egy másik Makefile.aimk-kat használunk, mivel szükség van további PVM-es parancsokra. Minden további program esetén megadásra kerülnek a Makefile.aimk-k is. Ezek csak kis mértékben, de lényeges opció megadásokban térnek el egymástól (az egyes fordításoknál).

A forkjoin program működése a következő:

Fontos észrevenni, hogy a forrás egyszerre tartalmazza a „szülő” (mester) és a „gyermek” (szolga) processzek kódját. A program indulásakor megtörténik a tid lekérdezése (pvm_mytid), amely minden más parancsot megelőz. Ez az azonosító pozitív egész szám, ha nem, akkor valamilyen probléma lépett fel pl. a futtatás során. Ezt a program ellenőrzi és adott esetben a pvm_perror által terminálódik. Ezen utóbbi parancs kiírja, hogy mi volt a probléma a legutolsó PVM függvény hívásakor. Ha az utolsó hívásunk pvm_mytid volt, akkor valószínűleg azt az üzenetet fogjuk kapni, hogy a PVM nem indult el a gépen. A pvm_perror argumentuma az argv[0], amely a program neve.

Ha a tid lekérdezése rendben megtörtént, akkor jön a szülő tid-jének lekérdezése. Mivel induláskor ez a szülő, így ekkor egy hiba kódot kapunk vissza egész érték helyett: PvmNoParent. Ennek segítségével tudjuk szétválasztani a szülő-gyermek programrészeket. A program indításakor meg lehet adni, hogy mennyi processz induljon el. Ez az érték az ntask paraméteren keresztül jelenik meg a programban. Alapértelmezett értéke 3, ha viszont 0-tól kisebb, vagy a MAXNCHILD-tól (alapértelmezett értéke 20) nagyobb értéket adunk meg a programnak, akkor a program meghívja a pvm_exit-et, azaz kilép a rendszerből.

A létrehozott gyermek processzek mindegyike üzenetet küld a szülőnek, melyből a szülő pl. a küldő processz tid-jét kiíratja a képernyőre.

A program futtatása a konzolon kívül érdekes, hiszen ekkor tudunk megadni paramétert, azaz az ntask értéke más lehet, mint 3.

Ebben az esetben a Makefile.aimk állomány a következőképpen adható meg:

  # Makefile.aimk - PVM programok fordításához 
  # 
  # Használat: 
  # Ezt a Makefile-t egy tetszőleges könyvtárba el lehet helyezni, amely 
  # C vagy C++ nyelvű forrásfile-okat tartalmaz. Minden forrásfile-ból egy 
  # programfile-t készít, az eredményt a LINUX alkönyvtárba helyezi el.  
  # 
  # Az `aimk' parancs az összes, alább megadott programot lefordítja. 
  # Az `aimk links' parancs linkeket készít a lefordított programokra, 
  # hogy a futtatás során megtalálja őket a PVM. Ezt a parancsot akkor 
  # kell kiadni, ha a programok listája megváltozik. 
  # A lefordítandó programok nevei (szóközzel elválasztott felsorolás) 
  BINS = forkjoin 
  # Kapcsolók a fordítónak 
  OPTIONS = -W -Wall 
  # Kapcsolók a linkernek 
  LOPT = 
  #################################################################### 
  # # # Innentől semmit nem kell módosítani # # #  
  #################################################################### 
  BDIR = $(HOME)/pvm3/bin 
  XDIR = $(BDIR)/$(PVM_ARCH) 
  CFLAGS = $(OPTIONS) -I./usr/include 
  LIBS = -lpvm3 -lfpvm3 -lgpvm3 
  LFLAGS = $(LOPT) -L$(PVM_ROOT)/lib/$(PVM_ARCH) 
  default: $(BINS) 
  % : ../%.c 
    $(CC) $(CFLAGS) -o $@ $< $(LDFLAGS) $(LIBS) 
  % : ../%.cc 
    $(CXX) $(CFLAGS) -o $@ $< $(LDFLAGS) $(LIBS) 
  $(XDIR): 
    mkdir -p $(XDIR) 
  links: $(XDIR) 
    @( CURRENT=`pwd` ;\  
    echo cd $(XDIR) ;\  
    cd $(XDIR) ;\  
    for i in $(BINS) ;\  
    do \  
       echo ln -sf $$CURRENT/$$i $$i ;\  
       ln -sf $$CURRENT/$$i $$i ;\  
    done ; ) 
  clean: 
    - rm *.o 
    - rm $(BINS) 
    - (cd $(XDIR); rm $(BINS)) 

A forkjoin.c programforrás:

  /*  
  a Fork Join példa demonstrálja, hogy miképpen jönnek létre a processzek és váltanak üzenetet egymás között 
  */ 
  /* a PVM függvénykönyvtár definíciói és protopípusai */ 
  #include <pvm3.h> 
  #include <stdio.h> 
  #include <stdlib.h> 
  /* a program által meghívható processzek számának maximuma */ 
  #define MAXNCHILD 20 
  /* a csatlakozási üzenethez felhasznált címke */ 
  #define JOINTAG 11 
  int 
  main(int argc, char* argv[]) 
  { 
    /* az indítandó processzek száma, 3 az alapértelmezett érték */ 
    int ntask = 3; 
    /* a pvm hívásokból visszakapott kód */ 
    int info; 
    /* a saját processz azonosító */ 
    int mytid; 
    /* a szülő processz azonosító */ 
    int myparent; 
    /* a gyermek processz azonosítók tömbje */ 
    int child[MAXNCHILD]; int i, mydata, buf, len, tag, tid; 
    /* a saját processz azonosító lekérdezése */ 
    mytid = pvm_mytid(); 
    /* hibaellenőrzés */ 
    if (mytid < 0) { 
       /* a hiba kiíratása */ 
       pvm_perror(argv[0]); 
       /* kilépés a programból */ 
       return -1; 
    } 
    /* a szülő processz azonosító lekérdezése */ 
    myparent = pvm_parent(); 
    /* kilépés ha, a PvmNoParent-től eltérő hiba van */ 
    if ((myparent < 0) && (myparent != PvmNoParent)) {  
       pvm_perror(argv[0]); 
       pvm_exit(); 
  return -1; 
    } 
    /* ha nincs szülőm, akkor én vagyok a szülő */ 
    if (myparent == PvmNoParent) { 
       /* az elindítandó processzek számának meghatározása */ 
       if (argc == 2) ntask = atoi(argv[1]); 
       /* az ntask ellenőrzése, hogy megfelelő */  
       if ((ntask < 1) || (ntask > MAXNCHILD)) { pvm_exit(); return 0; } 
       /* a gyermek processzek indítása */ 
       info = pvm_spawn(argv[0], (char**)0, PvmTaskDefault, (char*)0, ntask, child); 
       /* a processz azonosítók megjelenítése */  
       for (i = 0; i < ntask; i++) 
          if (child[i] < 0) /* a hibakód megjelenítése decimális számként */ 
             printf(" %d", child[i]); 
          else /* a processz azonosító megjelenítése hecadecimális számként */ 
             printf("t%x\t", child[i]); 
       putchar('\n'); 
       /* az indítás sikerének ellenőrzése */ 
       if (info == 0) { pvm_exit(); return -1; } 
       /* only expect responses from those spawned correctly */ 
       ntask = info; 
       for (i = 0; i < ntask; i++) {  
          /* egy üzenet fogadása bármely gyermek processztől */  
          buf = pvm_recv(-1, JOINTAG); 
          if (buf < 0) pvm_perror("calling recv"); 
          info = pvm_bufinfo(buf, &len, &tag, &tid); 
          if (info < 0) pvm_perror("calling pvm_bufinfo"); 
          info = pvm_upkint(&mydata, 1, 1); 
          if (info < 0) pvm_perror("calling pvm_upkint"); 
          if (mydata != tid) printf("This should not happen!\n"); 
          printf("Length %d, Tag %d, Tid t%x\n", len, tag, tid); } 
       pvm_exit(); 
       return 0;  
       } 
    /* gyermek vagyok */  
    info = pvm_initsend(PvmDataDefault); 
    if (info < 0) {  
       pvm_perror("calling pvm_initsend"); pvm_exit(); return -1;  
       } 
    info = pvm_pkint(&mytid, 1, 1); 
    if (info < 0) {  
       pvm_perror("calling pvm_pkint"); pvm_exit(); return -1;  
       } 
    info = pvm_send(myparent, JOINTAG); 
    if (info < 0) {  
       pvm_perror("calling pvm_send"); pvm_exit(); return -1;  
       } 
    pvm_exit(); 
    return 0; 
  } 

A forkjoin fordítását és futtatását (több esetben) mutatja be a következő animáció (interaktív animáció): forkjoin.pvm.swf

3.4.4. Belső szorzat (Fortran)

A következő program Fortran programnyelven készült, de könnyen elkészíthető a C változata is. Ehhez egyszerűen csak használni kell a man lapokat, ugyanis minden PVM-beli parancs C és Fortran változata is ebben megtalálható.

A program neve: Psdot.f, a működése a következőképpen foglalható össze: a program (párhuzamosan) számolja ki az és vektorok belső (skaláris) szorzatát. A Psdot először a PVMFMYTID és a PVMFPARENT függvényeket hívja meg, hogy a pontosan meghatározásra kerüljön a szülő-gyermek viszony. A program a felhasználótól kapja a processzek számát (amennyit a program indíthat és használhat: ) és a vektorok hosszát ( ). Ennek megfelelően fog minden egyes processz elemet meghatározni.A SGENMAT függvény legenerálja az és vektorokat. Utána processz megkapja a vektorok megfelelő részeit, majd a végén az eredmények egyesítésével megkapjuk a végeredményt.

A program forrása:

  PROGRAM PSDOT 
  * 
  * A PSDOT egy párhuzamos belső (vagy skaláris) szorzatot végez el. 
  * 
  * .. külső alprogramok ..  
  EXTERNAL PVMFMYTID, PVMFPARENT, PVMFSPAWN, PVMFEXIT, PVMFINITSEND EXTERNAL PVMFPACK, PVMFSEND, PVMFRECV, PVMFUNPACK, SGENMAT  
  * 
  * .. külső függvények .. 
  INTEGER ISAMAX  
  REAL SDOT EXTERNAL  
  ISAMAX, SDOT 
  * 
  * .. belső függvények .. 
  INTRINSIC MOD 
  * 
  * .. paraméterek ..  
  INTEGER MAXN 
  PARAMETER ( MAXN = 8000 ) 
  INCLUDE 'fpvm3.h' 
  *  
  * .. skalárok ..  
  INTEGER N, LN, MYTID, NPROCS, IBUF, IERR 
  INTEGER I, J, K 
  REAL LDOT, GDOT 
  *  
  * .. tömbök ..  
  INTEGER TIDS(0:63) 
  REAL X(MAXN), Y(MAXN) 
  * 
  * bejegyzés a PVM-be és a saját és a szülő azonosítójának lekérdezése 
  * 
  CALL PVMFMYTID 
  ( MYTID ) CALL PVMFPARENT( TIDS(0) ) 
  * 
  * ha további processzeket kell indítani (én vagyok a szülő processz) 
  * 
  IF ( TIDS(0) .EQ. PVMNOPARENT ) THEN 
  * 
  * az indítási információ bekérése 
  * 
  WRITE(*,*) 'How many processes should participate (1-64)?' 
  READ(*,*) NPROCS 
  WRITE(*,2000) MAXN 
  READ(*,*) N  
  TIDS(0) = MYTID 
  IF ( N .GT. MAXN ) THEN 
    WRITE(*,*) 'N too large. Increase parameter MAXN to run'// 
  $ 'this case.' 
    STOP  
  END IF 
  * 
  * az LN a skaláris szorzat elemeinek száma a lokális használathoz 
  * 
  J = N / NPROCS LN = J + MOD(N, NPROCS) I = LN + 1  
  * 
  * Randomly generate X and Y  
  * 
  CALL SGENMAT( N, 1, X, N, MYTID, NPROCS, MAXN, J ) 
  CALL SGENMAT( N, 1, Y, N, I, N, LN, NPROCS ) 
  * 
  * Loop over all worker processes 
  * 
  DO 10 K = 1, NPROCS-1 
  * 
  * a processzek indítása és hibaellenőrzés 
  * 
  CALL PVMFSPAWN( 'psdot', 0, 'anywhere', 1, TIDS(K), IERR ) 
  IF (IERR .NE. 1) THEN 
    WRITE(*,*) 'ERROR, could not spawn process #',K,  
    $ '. Dying . . .' 
    CALL PVMFEXIT( IERR ) 
    STOP 
  END IF 
  * 
  * az indítási információ kiküldése  
  * 
  CALL PVMFINITSEND( PVMDEFAULT, IBUF ) 
  CALL PVMFPACK( INTEGER4, J, 1, 1, IERR ) 
  CALL PVMFPACK( REAL4, X(I), J, 1, IERR ) 
  CALL PVMFPACK( REAL4, Y(I), J, 1, IERR ) 
  CALL PVMFSEND( TIDS(K), 0, IERR )  
  I = I + J  
  10 CONTINUE 
  * 
  * a szülőhöz tartozó skaláris szorzatrész számítása  
  * 
  GDOT = SDOT( LN, X, 1, Y, 1 ) 
  * 
  * a lokális szorzatrészek fogadása és hozzáadása a globálishoz 
  * 
  DO 20 K = 1, NPROCS-1 
    CALL PVMFRECV( -1, 1, IBUF ) 
    CALL PVMFUNPACK( REAL4, LDOT, 1, 1, IERR ) 
    GDOT = GDOT + LDOT 
  20 CONTINUE  
  * 
  * az eredmény megjelenítése 
  * 
  WRITE(*,*) ' ' 
  WRITE(*,*) '<x,y> = ',GDOT 
  * 
  * készítsük el a szekvencicális skaláris szorzatot és vonjuk ki a szétosztott skaláris szorzatból, hogy megkapjuk a kívánt hibabecslést 
  * 
  LDOT = SDOT( N, X, 1, Y, 1 ) 
  WRITE(*,*) '<x,y> : sequential dot product. <x,y>^ : '// 
  $ 'distributed dot product.' 
  WRITE(*,*) '| <x,y> - <x,y>^ | = ',ABS(GDOT - LDOT) 
  WRITE(*,*) 'Run completed.' 
  * 
  * ha egy dolgozó processz (azaz a szülő által elindított processz)  
  * 
  ELSE 
  * 
  * az indítási információ fogadása 
  * 
  CALL PVMFRECV( TIDS(0), 0, IBUF ) 
  CALL PVMFUNPACK( INTEGER4, LN, 1, 1, IERR ) 
  CALL PVMFUNPACK( REAL4, X, LN, 1, IERR ) 
  CALL PVMFUNPACK( REAL4, Y, LN, 1, IERR ) 
  * 
  * a lokális szorzatrészek kiszámítása és elküldése a szülőnek 
  * 
  LDOT = SDOT( LN, X, 1, Y, 1 ) 
  CALL PVMFINITSEND( PVMDEFAULT, IBUF ) 
  CALL PVMFPACK( REAL4, LDOT, 1, 1, IERR ) 
  CALL PVMFSEND( TIDS(0), 1, IERR ) 
  END IF 
  * 
  CALL PVMFEXIT( 0 ) 
  * 
  1000 FORMAT(I10,' Successfully spawned process #',I2,', TID =',I10) 
  2000 FORMAT('Enter the length of vectors to multiply (1 -',I7,'):')  
  STOP 
  * 
  * a PSDOT program vége 
  * 
  END 

3.4.5. Meghibásodás (failure)

A failure.c program működése a következőképpen fogalmazható meg: a program demonstrálja, hogy miképpen lehet terminálni processzeket („taszk”-okat) és hogyan lehet kitalálni mikor terminálódtak vagy hibásodtak meg. Ebben a példában számos processz indul el, mint az ezt megelőzőekben. Mivel minket az érdekel, hogy mely processzek buknak meg, ezért meghívjuk a pvm_notify parancsot az indítások után. Ez biztosítja az üzenetküldést a hívó processznek, ha egy másik processz exitál. A program terminál egy processzt és várja a pvm_recv-en keresztül a megfelelő információkat pl. a terminált processz azonosítóját. Ezt ki is írja a képernyőre.

Ebben az esetben a megfelelő Makefile.aimk a következő:

  # Makefile.aimk - PVM programok fordításához  
  # 
  # Használat: 
  # Ezt a Makefile-t egy tetszőleges könyvtárba el lehet helyezni, amely 
  # C vagy C++ nyelvű forrásfile-okat tartalmaz. Minden forrásfile-ból egy 
  # programfile-t készít, az eredményt a LINUX alkönyvtárba helyezi el. 
  # 
  # Az `aimk' parancs az összes, alább megadott programot lefordítja. 
  # Az `aimk links' parancs linkeket készít a lefordított programokra, 
  # hogy a futtatás során megtalálja őket a PVM. Ezt a parancsot akkor 
  # kell kiadni, ha a programok listája megváltozik. 
  # A lefordítandó programok nevei (szóközzel elválasztott felsorolás) 
  BINS = failure 
  # Kapcsolók a fordítónak 
  OPTIONS = -W -Wall 
  # Kapcsolók a linkernek 
  LOPT = 
  #################################################################### 
  # # # Innentől semmit nem kell módosítani # # # 
  #################################################################### 
  BDIR = $(HOME)/pvm3/bin 
  XDIR = $(BDIR)/$(PVM_ARCH) 
  CFLAGS = $(OPTIONS) -I$(PVM_ROOT)/include $(ARCHCFLAGS) 
  LIBS = -lpvm3 $(ARCHLIB) 
  LFLAGS = $(LOPT) -L$(PVM_ROOT)/lib/$(PVM_ARCH) 
  default: $(BINS) 
  % : ../%.c 
    $(CC) $(CFLAGS) -o $@ $< $(LDFLAGS) $(LIBS) 
  % : ../%.cc 
    $(CXX) $(CFLAGS) -o $@ $< $(LDFLAGS) $(LIBS) 
  $(XDIR): 
    mkdir -p $(XDIR) 
  links: $(XDIR) 
    @( CURRENT=`pwd` ;\  
    echo cd $(XDIR) ;\  
    cd $(XDIR) ;\  
    for i in $(BINS) ;\  
    do \  
       echo ln -sf $$CURRENT/$$i $$i ;\  
       ln -sf $$CURRENT/$$i $$i ;\  
    done ; )  
  clean:  
    - rm *.o  
    - rm $(BINS)  
    - (cd $(XDIR); rm $(BINS)) 

A failure.c programforrás a következő:

  /* A hibajelző példa demonstrálja, hogy miképpen mondjuk meg melyik processz terminált. */ 
  /* a PVM függvénykönyvtár definíciói és prototípusai */ 
  #include <pvm3.h> 
  #include <stdlib.h> 
  #include <stdio.h> 
  #include <unistd.h>  
  /* az indítható gyermek processzek maximális száma */ 
  #define MAXNCHILD 20 
  /* címke az üzenet használatához */ 
  #define TASKDIED 11 
  int main(int argc, char* argv[])  
  { 
    /* az indítható processzek száma, az alapértelmezett érték 3 */ 
    int ntask = 3; 
    /* a visszatérési kód a pvm hvásokhoz */ 
    int info; 
    /* a saját azonosító */ 
    int mytid; 
    /* a szülő azonosítója */ 
    int myparent; 
    /* a gyermek azonosítók tömbje */ 
    int child[MAXNCHILD]; 
    int i, deadtid;  
    /* a saját azonosító lekérdezése */ 
    mytid = pvm_mytid(); 
    /* hibaellenőrzés */ 
    if (mytid < 0) { 
       /* a hiba megjelenítése */ 
       pvm_perror(argv[0]); 
       /* kilépés a programból */ 
       return -1; 
       } 
    /* a szülő azonosítójának lekérdezése */ 
    myparent = pvm_parent(); 
    /* kilépés, ha a PvmNoParent-től különböző hiba van */ 
    if ((myparent < 0) && (myparent != PvmNoParent)) {  
       pvm_perror(argv[0]); 
       pvm_exit(); 
       return -1; 
       } 
    /* ha nincs szülőm, akkor én vagyok a szülő */ 
    if (myparent == PvmNoParent) { 
       /* az indítandó processzek számának meghatározása */ 
       if (argc == 2) ntask = atoi(argv[1]); 
       /* az ntask ellenőrzése */ 
       if ((ntask < 1) || (ntask > MAXNCHILD)) { pvm_exit(); return 0; } 
       /* a gyermek processzek indítása */ 
       info = pvm_spawn(argv[0], (char**)0, PvmTaskDebug, (char*)0, ntask, child); 
       /* az indítás sikerességének ellenőrzése */ 
       if (info != ntask) { pvm_exit(); return -1; } 
       /* az azonosítók megjelenítése */ 
       for (i = 0; i < ntask; i++) printf("t%x\t",child[i]); putchar('\n'); 
       /* jelzés kérése, amikor egy proceszz kilép */ 
       info = pvm_notify(PvmTaskExit, TASKDIED, ntask, child);  
       if (info < 0) { pvm_perror("notify"); pvm_exit(); return -1; } 
       /* a középső processz terminálása */ 
       info = pvm_kill(child[ntask/2]); 
       if (info < 0) { pvm_perror("kill"); pvm_exit(); return -1; } 
       /* várakozás a jelzésre */ 
       info = pvm_recv(-1, TASKDIED); 
       if (info < 0) { pvm_perror("recv"); pvm_exit(); return -1; } 
       info = pvm_upkint(&deadtid, 1, 1); 
       if (info < 0) pvm_perror("calling pvm_upkint"); 
       /* a középső gyermeknek kell lennie */ 
       printf("Task t%x has exited.\n", deadtid);  
       printf("Task t%x is middle child.\n", child[ntask/2]); 
       pvm_exit();  
       return 0;  
       } 
    /* gyermek vagyok */  
    sleep(63); 
    pvm_exit();  
    return 0;  
  } 

3.4.6. Mátrixok szorzása (mmult)

Ebben az alfejezetben levő PVM program mátrixok szorzását valósítja meg párhuzamosan. A felhasznált eljárás eltér a korábban használt algroitmusoktól (lásd a 2.2.5. alszakaszt vagy a 2.4. ábrát).

A program alapelve a következő:

Az algoritmus a mátrixot számítja ki ahol , és négyzetes mátrixok. Feltételezzük hogy taszkot használunk a megoldáshoz, és minden taszk egy blokkját számítja ki. A blokk méretét és az értéket parancssori argumentumként kapja a program. Az és mátrixok blokkonként tárolódnak a taszkokban. Az algoritmus nagy vonalakban:

Tegyük fel hogy taszkunk van rácsos formában. Minden taszk ( ahol ) kezdetben a , , blokkokat tartalmazza. Első lépésben a diagonálisban lévő taszkok ( ahol ) elküldik az blokkjaikat az összes többi tasznak a sorban ( sor taszkjai). továbbítása után minden taszk kiszámítja -t és hozzáadja az eredményt -hez. A következő lépésben blokkjai az oszlopokon belül felcserélődnek, azaz elküldi a blokkját -nek. (A taszk elküldi a -nek a saját blokkját.) Ezután a program visszatér az első lépéshez. A következő iterációban „multicastol” (továbbít) az összes többi taszknak a sorban, és az algoritmus folytatódik. iteráció után a mátrix tartalmazza -t, és a mátrix „visszaforog” az eredeti helyére.

PVM-ben történő megvalósítás:

A PVM-ben nincs megszorítás arra, hogy melyik taszk melyikkel kommunikálhat. A taszkokra ebben a programban két dimenziós tóruszként gondolunk. Azért, hogy a processzeket meg tudjuk számolni, mindegyik csatlakozik az mmult csoporthoz. A taszk ID-k segítenek feltérképezni a tóruszt.

Az első processz, amelyik csatlakozik, 0-s ID-t kap a csoportban. A nullás csoport ID-vel rendelkező taszk hozza létre a többi taszkot (spawn) és elküldi a paramétereket ezeknek a taszkoknak. A paraméter az m és a blocksize, mely a blokkok számának négyzetgyöke és a blokkméret rendre. Miután az összes taszk létrejött és a paraméterek el lettek küldve, pvm_barrier hívással megbizonyosodhatunk, hogy az összes taszk csatlakozott a csoporthoz.

Ha nem hívjuk a pvm_barrier-t, akkor a későbbi pvm_gettid hívások sikertelenek lehetnek, ha még valamelyik taszk nem csatlakozott a csoporthoz. A várakoztatás után (barrier) eltároljuk az egy sorban lévő taszkok taszk ID-jeit - az adott i. sorban lévő taszkok ID-jeit a myrow tömbben tároljuk. Ez úgy történik, hogy minden taszk ID-jét kiszámoljuk a csoportban az adott sorban, és a pvm-től lekérdezzük az adott taszk taszk ID-jét a csoport ID alapján. (A csoport ID alapján lekérdezzük a taszk ID-t). Ezután a malloc-kal helyet foglalunk le a blokkok részére.

Ezután a program kiszámolja oszlopainak és sorainak blokkjait (pontosabban az indexeket), amellyel számolni fog. Ez a csoport ID alapján történik. A csoport ID-k -tól -ig terjednek, így az egész osztás megadja a taszk sorának indexét, és megadja az oszlop indexét, ha a csoport ID-k és a taszkok között soros megfeleltetést feltételezünk. Hasonló leképezést használva kiszámítjuk a taszk alatt és felett közvetlenül álló taszk csoport ID-jét és eltároljuk az up és down változókban a taszk ID-jüket. Ezután a blokkok inicializálódnak az InitBlock hívással. Ez a függvény egyszerűen inicializálja -t véletlen számokkal, -t az egységmátrixra, -t pedig nullákra.

Az algoritmus végén -vel ellenőrizhetjük, hogy helyes-e a számolás (ha minden rendben megy végbe, akkor a program a „Done.” szöveget írja ki). Az inicializálás után belépünk a fő ciklusba ami a számolást végzi. A diagonálisban levő taszkok „multicastolnak”, elküldik az blokkukat a sorban a többieknek. A myrow tömb tartalmazza az éppen „multicastoló” taszk ID-jét. Minden taszk kiszámítja a taszkban tárolt blokk és a diagonális blokk szorzatát, majd hozzáadják a megfelelő blokkhoz. Ezután blokkjait függőlegesen eltoljuk. A blokkot betesszük egy üzenetbe és elküldjük az up taszk ID-vel rendelkező taszknak, és kapunk egy új blokkot a down taszk ID-jű taszktól. Jegyezzük meg, hogy különböző üzenet tag-eket (cimkéket) használunk az és blokkok küldésekor a ciklus különböző iterációiban.

Tökéletesen specifikáljuk a taszk ID-ket, amikor a pvm_recv-t használunk. Nem szükséges pvm_lvgroup-t hívni, mivel a PVM észleli, ha egy taszk kilépett és így eltávolítja a csoportból. De azért jó elhagyni a csoportot mielőtt a pvm_exit-et hívjuk. A reset parancs a PVM konzolból az összes PVM csoportot eltávolítja. A pvm_gstat parancs az összes létező csoport állapotát adja vissza (jeleníti meg az ablakban).

A megfelelő Makefile.aimk a következő:

  # Makefile.aimk - PVM programok fordításához 
  # 
  # Használat: 
  # Ezt a Makefile-t egy tetszőleges könyvtárba el lehet helyezni, amely 
  # C vagy C++ nyelvű forrásfile-okat tartalmaz. Minden forrásfile-ból egy 
  # programfile-t készít, az eredményt a LINUX alkönyvtárba helyezi el.  
  # 
  # Az `aimk' parancs az összes, alább megadott programot lefordítja. 
  # Az `aimk links' parancs linkeket készít a lefordított programokra, 
  # hogy a futtatás során megtalálja őket a PVM. Ezt a parancsot akkor 
  # kell kiadni, ha a programok listája megváltozik. 
  # A lefordítandó programok nevei (szóközzel elválasztott felsorolás) 
  BINS = mmult 
  # Kapcsolók a fordítónak 
  OPTIONS = -W -Wall 
  # Kapcsolók a linkernek 
  LOPT = 
  #################################################################### 
  # # # Innentől semmit nem kell módosítani # # #  
  #################################################################### 
  BDIR = $(HOME)/pvm3/bin 
  XDIR = $(BDIR)/$(PVM_ARCH) 
  CFLAGS = $(OPTIONS) -I./usr/include 
  LIBS = -lpvm3 -lfpvm3 -lgpvm3 
  LFLAGS = $(LOPT) -L$(PVM_ROOT)/lib/$(PVM_ARCH) 
  default: $(BINS) 
  % : ../%.c 
    $(CC) $(CFLAGS) -o $@ $< $(LDFLAGS) $(LIBS) 
  % : ../%.cc 
    $(CXX) $(CFLAGS) -o $@ $< $(LDFLAGS) $(LIBS) 
  $(XDIR): 
    mkdir -p $(XDIR) 
  links: $(XDIR) 
    @( CURRENT=`pwd` ;\  
    echo cd $(XDIR) ;\  
    cd $(XDIR) ;\  
    for i in $(BINS) ;\  
    do \  
       echo ln -sf $$CURRENT/$$i $$i ;\  
       ln -sf $$CURRENT/$$i $$i ;\  
    done ; ) 
  clean: 
    - rm *.o 
    - rm $(BINS) 
    - (cd $(XDIR); rm $(BINS)) 

Az mmult.c programforrás a következő:

  /* Mátrixszorzás */ 
  /* a PVM függvénykönyvtár definíció és prototípusai */  
  #include <pvm3.h> 
  #include <stdio.h> 
  #include <stdlib.h> 
  /* a maximálisan indítható gyermek processzek száma */  
  #define MAXNTIDS 100  
  #define MAXROW 10 
  /* üzenet címkek */ 
  #define ATAG 2 
  #define BTAG 3 
  #define DIMTAG 5 
  void  
  InitBlock(float *a, float *b, float *c, int blk, int row, int col)  
  { 
    int len, ind; 
    int i,j; 
    srand(pvm_mytid());  
    len = blk*blk; 
    for (ind = 0; ind < len; ind++)  
       { a[ind] = (float)(rand()%1000)/100.0; c[ind] = 0.0; }  
    for (i = 0; i < blk; i++) {  
       for (j = 0; j < blk; j++) {  
          if (row == col)  
             b[j*blk+i] = (i==j)? 1.0 : 0.0;  
          else 
             b[j*blk+i] = 0.0;  
          } 
       } 
  } 
  void 
  BlockMult(float* c, float* a, float* b, int blk) 
  {  
    int i,j,k; 
    for (i = 0; i < blk; i++)  
       for (j = 0; j < blk; j ++)  
          for (k = 0; k < blk; k++)  
             c[i*blk+j] += (a[i*blk+k] * b[k*blk+j]);  
  } 
  int 
  main(int argc, char* argv[])  
  { 
    /* az indítandó processzek száma, az alapértelmezett érték 3 */ 
    int ntask = 2;  
    /* a visszatérési érték a pvm hivásokból */ 
    int info; 
    /* a taszk/processz és a csoport ID, azaz azonosító */  
    int mytid, mygid; 
    /* a gyermek taszk ID tömb */  
    int child[MAXNTIDS-1]; 
    int i, m, blksize;  
    /* a saját sorban levő tid-ek tömbje */  
    int myrow[MAXROW]; 
    float *a, *b, *c, *atmp;  
    int row, col, up, down; 
    /* a saját tid meghatározása */ 
    mytid = pvm_mytid(); 
    pvm_setopt(PvmRoute, PvmRouteDirect); 
    /* hibaellenőrzés */  
    if (mytid < 0) {  
       /* a hiba kiíratása/megjelenítése */  
       pvm_perror(argv[0]);  
       /* kilépés a programból */  
       return -1; 
       } 
    /* belépés az mmult csoportba */  
    mygid = pvm_joingroup("mmult");  
    if (mygid < 0) { 
       pvm_perror(argv[0]); pvm_exit(); return -1;  
       } 
    /* ha a csoport ID-m 0, akkor én indítom a többi processzt/taszkot */  
    if (mygid == 0) { 
       /* az indítandó processzek számának lekérdezése */  
       if (argc == 3) { 
          m = atoi(argv[1]); 
          blksize = atoi(argv[2]);  
          }  
       if (argc < 3) { 
          fprintf(stderr, "usage: mmult m blk\n"); 
          pvm_lvgroup("mmult"); pvm_exit(); return -1;  
          } 
       /* az ntask ellenőrzése */  
       ntask = m*m;  
       if ((ntask < 1) || (ntask >= MAXNTIDS)) {  
          fprintf(stderr, "ntask = %d not valid.\n", ntask);  
          pvm_lvgroup("mmult"); pvm_exit(); return -1;  
          } 
       /* nincs szükség a spawn parancsra, ha csak egy taszk van */  
       if (ntask == 1) goto barrier; 
       /* a gyermek processzek indítása */  
       info = pvm_spawn(argv[0], (char**)0, PvmTaskDefault, (char*)0, ntask-1, child); 
       /* az indítás sikerességének ellenőrzése */ 
       if (info != ntask-1) {  
          pvm_lvgroup("mmult"); pvm_exit(); return -1;  
          } 
       /* a mátrix dimenziójának küldése */  
       pvm_initsend(PvmDataDefault);  
       pvm_pkint(&m, 1, 1); 
       pvm_pkint(&blksize, 1, 1); 
       pvm_mcast(child, ntask-1, DIMTAG);  
       } 
    else { 
       /* a mátrix dimenziójának fogadása */  
       pvm_recv(pvm_gettid("mmult", 0), DIMTAG);  
       pvm_upkint(&m, 1, 1); 
       pvm_upkint(&blksize, 1, 1);  
       ntask = m*m; 
       } 
    /* az összes processz csoportba kerülésének ellenőrzése */  
  barrier:  
    info = pvm_barrier("mmult",ntask); 
    if (info < 0) pvm_perror(argv[0]); 
    /* a tid-ek megkeresése a saját sorban */  
    for (i = 0; i < m; i++) 
       myrow[i] = pvm_gettid("mmult", (mygid/m)*m + i); 
    /* memória allokálása a lokális blokk részére */ 
    a = (float*)malloc(sizeof(float)*blksize*blksize); 
    b = (float*)malloc(sizeof(float)*blksize*blksize); 
    c = (float*)malloc(sizeof(float)*blksize*blksize); 
    atmp = (float*)malloc(sizeof(float)*blksize*blksize); 
    /* az érvényes mutatók ellenőrzése */  
    if (!(a && b && c && atmp)) { 
       fprintf(stderr, "%s: out of memory!\n", argv[0]); 
       free(a); free(b); free(c); free(atmp); 
       pvm_lvgroup("mmult"); pvm_exit(); return -1;  
       } 
    /* a saját blokk sorának és oszlopának meghatározása */  
    row = mygid/m; col = mygid % m;  
    /* a szomszédos indexek kiszámítása, up és down */  
    up = pvm_gettid("mmult", ((row)?(row-1):(m-1))*m+col); 
    down = pvm_gettid("mmult", ((row == (m-1))?col:(row+1)*m+col)); 
    /* a blokkok inicializációja */  
    InitBlock(a, b, c, blksize, row, col); 
    /* a mátrixszorzás végrehajtása */  
    for (i = 0; i < m; i++) {  
       /* az A mátrix blokkjának multicast-olása */  
       if (col == (row + i)%m) { 
          pvm_initsend(PvmDataDefault);  
          pvm_pkfloat(a, blksize*blksize, 1); 
          pvm_mcast(myrow, m, (i+1)*ATAG); 
          BlockMult(c,a,b,blksize);  
          } 
       else { 
          pvm_recv(pvm_gettid("mmult", row*m + (row +i)%m), (i+1)*ATAG); 
          pvm_upkfloat(atmp, blksize*blksize, 1);  
          BlockMult(c,atmp,b,blksize);  
          } 
       /* A B oszlopainak rotációja */  
       pvm_initsend(PvmDataDefault); 
       pvm_pkfloat(b, blksize*blksize, 1); 
       pvm_send(up, (i+1)*BTAG); 
       pvm_recv(down, (i+1)*BTAG); 
       pvm_upkfloat(b, blksize*blksize, 1); 
       } 
    /* ellenőrzés */  
    for (i = 0 ; i < blksize*blksize; i++) 
       if (a[i] != c[i]) 
          printf("Error a[%d] (%g) != c[%d] (%g) \n", i, a[i],i,c[i]); 
    printf("Done.\n"); 
    free(a); free(b); free(c); free(atmp); 
    pvm_lvgroup("mmult"); 
    pvm_exit(); 
    return 0;  
  } 

3.4.7. Maximum keresése (maxker)

Asszociatív függvény kiszámítása bináris fa módszerrel. Minden maxker taszk kap parancssor argumentumként valamennyi számot. (Nem csak kettőhatványra működik!) Ha háromnál többet kap, újabb maxker taszkokat hoz létre és rekurzívan számolja ki a maximumot, amit elküld a szülőjének. A legelső taszk, amelyiket mi indítottunk (a szülő), nem küldi tovább a maximumot, hanem kiirja a képernyőre. (Alapvetően a konzolon kívüli indítás a preferált.) Elkészült a szekvenciális és a párhuzamos megoldás is.

A maxker_seq.c-ben találjuk a szekvenciális megvalósítást.

A megfelelő Makefile.aimk a következő:

  # Makefile.aimk - PVM programok fordításához  
  # 
  # Használat: 
  # Ezt a Makefile-t egy tetszőleges könyvtárba el lehet helyezni, amely 
  # C vagy C++ nyelvű forrásfile-okat tartalmaz. Minden forrásfile-ból egy 
  # programfile-t készít, az eredményt a LINUX alkönyvtárba helyezi el. 
  # 
  # Az `aimk' parancs az összes, alább megadott programot lefordítja. 
  # Az `aimk links' parancs linkeket készít a lefordított programokra, 
  # hogy a futtatás során megtalálja őket a PVM. Ezt a parancsot akkor 
  # kell kiadni, ha a programok listája megváltozik. 
  # A lefordítandó programok nevei (szóközzel elválasztott felsorolás) 
  BINS = maxker 
  # Kapcsolók a fordítónak 
  OPTIONS = -W -Wall 
  # Kapcsolók a linkernek 
  LOPT = 
  #################################################################### 
  # # # Innentől semmit nem kell módosítani # # # 
  #################################################################### 
  BDIR = $(HOME)/pvm3/bin 
  XDIR = $(BDIR)/$(PVM_ARCH) 
  CFLAGS = $(OPTIONS) -I$(PVM_ROOT)/include $(ARCHCFLAGS) 
  LIBS = -lpvm3 $(ARCHLIB) 
  LFLAGS = $(LOPT) -L$(PVM_ROOT)/lib/$(PVM_ARCH) 
  default: $(BINS) 
  % : ../%.c 
    $(CC) $(CFLAGS) -o $@ $< $(LDFLAGS) $(LIBS) 
  % : ../%.cc 
    $(CXX) $(CFLAGS) -o $@ $< $(LDFLAGS) $(LIBS) 
  $(XDIR): 
    mkdir -p $(XDIR) 
  links: $(XDIR) 
    @( CURRENT=`pwd` ;\  
    echo cd $(XDIR) ;\  
    cd $(XDIR) ;\  
    for i in $(BINS) ;\  
    do \  
       echo ln -sf $$CURRENT/$$i $$i ;\  
       ln -sf $$CURRENT/$$i $$i ;\  
    done ; )  
  clean:  
    - rm *.o  
    - rm $(BINS)  
    - (cd $(XDIR); rm $(BINS)) 

A szekvenciális kód (maxker_seq.c):

  #include <stdio.h> 
  #include <stdlib.h> 
  #define maximum(a,b) ( (eatUpTime((a))>(b)) ? (a) : (b) ) 
  int 
  eatUpTime(int a){  
    int i,j; 
    for( i=0; i<8000; i++ ){  
       for( j=0; j<8000; j++){ 
          if ((i==j-2) && (i==65000-1)) return i+a-j-2;  
       }  
    } 
    return a;  
  } 
  int 
  main( int argc, char *argv[] ){ 
    int max; 
  /* meghatározzuk maximális értéket */  
    if (argc<2) { /* hibas mukodes */  
       fprintf(stderr,"Meg kell adni parancssorban a szamokat!\n");  
       exit(1);  
    } else {  
       int i;  
       max = atoi(argv[1]);  
       for( i=2; i<argc; i++ )  
          max = maximum(max,atoi(argv[i]));  
    } 
    printf("A maximum: %d\n",max);  
  exit(0);  
  } 

A párhuzamos kód (maxker.c):

  #include <stdio.h> 
  #include <stdlib.h> 
  #include "pvm3.h" 
  #define maximum(a,b) ( ((a)>(b)) ? (a) : (b) )  
  int 
  eatUpTime(int a){  
    int i,j;  
    for( i=0; i<8000; i++ ){  
       for( j=0; j<8000; j++){  
          if ((i==j-2) && (i==65000-1)) return i+a-j-2; 
       }  
    } 
    return a;  
  } 
  int  
  main( int argc, char *argv[] ){ 
    int max; 
    pvm_mytid(); 
    /* meghatározzuk maximális értéket */  
    if (argc<2) {/* hibas mukodes */  
       fprintf(stderr,"Meg kell adni parancssorban a számokat!\n");  
       pvm_exit();  
       exit(1);  
    } else if (argc==2) { /* csak 1 számot kapott */  
       max = atoi(argv[1]);  
    } else if (argc==3) { /* ket számot kapott */  
       max = maximum(atoi(argv[1]),atoi(argv[2]));  
    } else { /* legalább három számot kapott */  
       int dummytid; /* pvm_spawn-hoz szükséges */ 
       int i; /* a for ciklushoz kell */  
       int egyik, masik; /* a két részmaximumot ezekbe gyűjtjük */ 
       /* ebbe bontjuk két részre argv-t */  
       char **args = malloc((argc+1)*sizeof(char *)); 
       /* az egyik taszk az argumentumok egyik felével */  
       for (i=1;i<argc/2;i++) args[i-1] = argv[i];  
       args[i-1] = (char *)NULL;  
       pvm_spawn(argv[0],args,0,"",1,&dummytid); 
       /* a másik taszk az argumentumok másik felével */  
       for (;i<argc;i++) args[i] = argv[i];  
       args[i] = (char *)NULL; 
       pvm_spawn(argv[0],&args[argc/2],0,"",1,&dummytid); 
       pvm_recv(-1,-1); /* az egyik részmaximum */  
       pvm_upkint(&egyik,1,1); 
       pvm_recv(-1,-1); /* a másik részmaximum */  
       pvm_upkint(&masik,1,1); 
       max = maximum(egyik,masik); 
       /* felszabaditom args-t */  
       free(args); 
    } 
    /* a kapott maximum kiíratása */  
    if (pvm_parent()==PvmNoParent) /* én vagyok a szülő */  
       printf("A maximum: %d\n",max);  
    else {  
       pvm_initsend(PvmDataDefault);  
       pvm_pkint(&max,1,1);  
       pvm_send(pvm_parent(),0);  
    } 
    pvm_exit(); 
    exit(0);  
  } 

A párhuzamos kód fordítása és futtatása különböző adatokon megtekinthető a következő animációban: maxker.pvm.swf

3.4.8. Egy dimenziós hővezetési egyenlet

A következő PVM program segítségével számoljuk ki a hővezetést egy anyagon keresztül (lásd még ugyanezt az algoritmust a Multi-Pascal programmal a 2.7. alfejezetben kétdimenziós esetben), a mi esetükben egy dróton. Tekintsük a megfelelő egy dimenziós hő-egyenletet

melynek diszkrét alakja

származtatja nekünk az explicit formulát

A kezdeti értékek és feltételek:

, minden -re, , ahol . A számítás pszeudo kódja a következő:

  for i = 1:tsteps-1; 
    t = t+dt; 
    a(i+1,1)=0; 
    a(i+1,n+2)=0; 
    for j = 2:n+1; 
       a(i+1,j)=a(i,j) + mu*(a(i,j+1)-2*a(i,j)+a(i,j-1)); 
    end; 
    t; 
    a(i+1,1:n+2); 
    plot(a(i,:)) 
  end 

Ebben a példában egy mester-szolga programmodellt használunk. A mester, heat.c öt másolatát hívja meg a heatslv-nek (a szolga: heatslv.c). A szolga párhuzamosan számolja ki a hővezetést a drót részeire. Minden egyes időlépésnél a szolga kicseréli a korlát információkat, a mi esetünkben a processzek közötti perem hőmérsékleteket. A programban a solution tömb tartalmazza a megoldást minden egyes időlépésnél. Ez a tömb lesz a kimeneti eredmény xgraph formátumban (xgraph egy program, amellyel a pontokat ábrázolhatjuk, letöltési és használati segítséget kaphatunk a xgraph oldalról).

Először a heatslv processzek indulnak el, aztán a kezdeti adatok kiszámítása történik meg. Megjegyezzük, hogy a drót végeire megadott kezdeti hőmérséklet értéke zérus. A fő része a programnak, hogy 4-szer fut le különböző értékekkel. Az időzítőt az egyes fázisokban eltelt idő kiszámítására használjuk fel. A kezdeti adathalmazok elküldésre kerülnek a heatslv processzeknek. A bal és jobboldali szomszédos processz azonosítók (tid-ek) elküldésre kerülnek a kezdeti adathalmazokkal együtt. A processzek felhasználják ezeket, hogy kommunikáljanak a perem információkról. Az kezdeti adatok elküldése után a mester vár az eredményekre. Amikor az eredmények megérkeznek, akkor ezek belekerülnek egy megoldási mátrixba, az eltelt idő kiszámításra kerül és a megoldások kiíratásra kerülnek egy xgraph állományba (lásd majd graph1, graph2, graph3, és graph4). Amikor mind a négy fázishoz tartozó adathalmaz kiszámolásra és tárolásra került, a mester program kiírja az eltelt időket és terminálja a szolga processzeket.

A megfelelő Makefile.aimk a következő:

  # Makefile.aimk - PVM programok fordításához  
  # 
  # Használat: 
  # Ezt a Makefile-t egy tetszőleges könyvtárba el lehet helyezni, amely 
  # C vagy C++ nyelvű forrásfile-okat tartalmaz. Minden forrásfile-ból egy 
  # programfile-t készít, az eredményt a LINUX alkönyvtárba helyezi el. 
  # 
  # Az `aimk' parancs az összes, alább megadott programot lefordítja. 
  # Az `aimk links' parancs linkeket készít a lefordított programokra, 
  # hogy a futtatás során megtalálja őket a PVM. Ezt a parancsot akkor 
  # kell kiadni, ha a programok listája megváltozik. 
  # A lefordítandó programok nevei (szóközzel elválasztott felsorolás) 
  BINS = heat heatslv 
  # Kapcsolók a fordítónak 
  OPTIONS = -W -Wall -lm 
  # Kapcsolók a linkernek 
  LOPT = 
  #################################################################### 
  # # # Innentől semmit nem kell módosítani # # # 
  #################################################################### 
  BDIR = $(HOME)/pvm3/bin 
  XDIR = $(BDIR)/$(PVM_ARCH) 
  CFLAGS = $(OPTIONS) -I$(PVM_ROOT)/include $(ARCHCFLAGS) 
  LIBS = -lpvm3 $(ARCHLIB) 
  LFLAGS = $(LOPT) -L$(PVM_ROOT)/lib/$(PVM_ARCH) 
  default: $(BINS) 
  % : ../%.c 
    $(CC) $(CFLAGS) -o $@ $< $(LDFLAGS) $(LIBS) 
  % : ../%.cc 
    $(CXX) $(CFLAGS) -o $@ $< $(LDFLAGS) $(LIBS) 
  $(XDIR): 
    mkdir -p $(XDIR) 
  links: $(XDIR) 
    @( CURRENT=`pwd` ;\  
    echo cd $(XDIR) ;\  
    cd $(XDIR) ;\  
    for i in $(BINS) ;\  
    do \  
       echo ln -sf $$CURRENT/$$i $$i ;\  
       ln -sf $$CURRENT/$$i $$i ;\  
    done ; )  
  clean:  
    - rm *.o  
    - rm $(BINS)  
    - (cd $(XDIR); rm $(BINS)) 

A heat.c program forrása:

  /* heat.c A PVM felhasználásával oldunk meg egy egyszerű differenciál egyenletet a hővezetésre vonatkozólag, ahol 1 mester és 5 szolga processz működik. 
  A mester program beállítja az adatokat, elküldi a szolgáknak és vár az eredményre, melyek a szolgáktól érkeznek. Az eredmény xgraph állományokban jön létre.*/ 
  #include "pvm3.h" 
  #include <stdio.h> 
  #include <math.h> 
  #include <time.h> 
  #define SLAVENAME "heatslv"  
  #define NPROC 5  
  #define TIMESTEP 100 
  #define PLOTINC 10  
  #define SIZE 1000 
  int num_data = SIZE/NPROC; 
  int wh(x, y) 
  int x, y;  
  { return(x*num_data+y); } 
  int main()  
  {  
    int mytid, task_ids[NPROC], i, j; 
    int left, right, k, l;  
    int step = TIMESTEP;  
    int info; 
    double init[SIZE], solution[TIMESTEP][SIZE];  
    double result[TIMESTEP*SIZE/NPROC], deltax2;  
    FILE *filenum; 
    char *filename[4][7]; 
    double deltat[4]; 
    time_t t0; 
    int etime[4]; 
    filename[0][0] = "graph1"; 
    filename[1][0] = "graph2"; 
    filename[2][0] = "graph3"; 
    filename[3][0] = "graph4"; 
    deltat[0] = 5.0e-1; 
    deltat[1] = 5.0e-3;  
    deltat[2] = 5.0e-6;  
    deltat[3] = 5.0e-9; 
    /* a tid lekérdezése */  
    mytid = pvm_mytid(); 
    /* a szolga processzek indítása */  
    info = pvm_spawn(SLAVENAME,(char **)0,PvmTaskDefault,"", NPROC,task_ids);  
    /* a kezdeti adathalmaz létrehozása */  
    for (i = 0; i < SIZE; i++)  
       init[i] = sin(M_PI * ( (double)i / (double)(SIZE-1) ));  
    init[0] = 0.0;  
    init[SIZE-1] = 0.0; 
    /* a program futtatása négyszer különböző delta t-kre */  
    for (l = 0; l < 4; l++) {  
       deltax2 = (deltat[l]/pow(1.0/(double)SIZE,2.0));  
       /* az időszámlálás indítása ehhez a futtatáshoz */  
       time(&t0);  
       etime[l] = t0;  
       /* a kezdeti adatok elküldése a szolgáknak */  
       /* csatolása a szomszédok információjának a perem adatok cseréjéhez */  
       for (i = 0; i < NPROC; i++) {  
          pvm_initsend(PvmDataDefault);  
          left = (i == 0) ? 0 : task_ids[i-1];  
          pvm_pkint(&left, 1, 1);  
          right = (i == (NPROC-1)) ? 0 : task_ids[i+1]; 
          pvm_pkint(&right, 1, 1); 
          pvm_pkint(&step, 1, 1); 
          pvm_pkdouble(&deltax2, 1, 1); 
          pvm_pkint(&num_data, 1, 1); 
          pvm_pkdouble(&init[num_data*i], num_data, 1); 
          pvm_send(task_ids[i], 4);  
          } 
       /* várakozás az eredményekre */  
       for (i = 0; i < NPROC; i++) {  
          pvm_recv(task_ids[i], 7); 
          pvm_upkdouble(&result[0], num_data*TIMESTEP, 1);  
          /* a megoldás frissítése */  
          for (j = 0; j < TIMESTEP; j++)  
             for (k = 0; k < num_data; k++)  
                solution[j][num_data*i+k] = result[wh(j,k)];  
          } 
       /* az időszámlálás leállítása */  
       time(&t0); 
       etime[l] = t0 - etime[l]; 
       /* a kimenet létrehozása */  
       filenum = fopen(filename[l][0], "w"); 
       fprintf(filenum,"TitleText: Wire Heat over Delta Time: %e\n", deltat[l]); 
       fprintf(filenum,"XUnitText: Distance\nYUnitText: Heat\n"); 
       for (i = 0; i < TIMESTEP; i = i + PLOTINC) {  
          fprintf(filenum,"\"Time index: %d\n",i); 
          for (j = 0; j < SIZE; j++)  
             fprintf(filenum,"%d %e\n",j, solution[i][j]);  
          fprintf(filenum,"\n");  
          }  
       fclose (filenum);  
    } 
    /* az időmérési információk kiíratása */  
    printf("Problem size: %d\n",SIZE); 
    for (i = 0; i < 4; i++)  
       printf("Time for run %d: %d sec\n",i,etime[i]); 
    /* a szolga processzek terminálása */  
    for (i = 0; i < NPROC; i++) pvm_kill(task_ids[i]);  
    pvm_exit();  
  return 0; } 

A heatslv.c forrás:

  /*heatslv.c A szolgák megkapják a kezdeti adatokat, kicserélik a perem információkat a szomszédokkal és kiszámítják a hőváltozást a drótban.*/ 
  #include <stdlib.h> 
  #include "pvm3.h"  
  #include <stdio.h> 
  int num_data; 
  int wh(x, y)  
  int x, y;  
  { return(x*num_data+y); } 
  int main() {  
    int mytid, left, right, i, j, master;  
    int timestep; 
    double *init, *A;  
    double leftdata, rightdata, delta, leftside, rightside;  
    /* a tid-ek lekérdezése */  
    mytid = pvm_mytid(); 
    master = pvm_parent(); 
    /* a saját adatok fogadása a mester programtól */  
    while(1) { 
    pvm_recv(master, 4); 
    pvm_upkint(&left, 1, 1); 
    pvm_upkint(&right, 1, 1); 
    pvm_upkint(&timestep, 1, 1); 
    pvm_upkdouble(&delta, 1, 1);  
    pvm_upkint(&num_data, 1, 1); 
    init = (double *) malloc(num_data*sizeof(double)); 
    pvm_upkdouble(init, num_data, 1); 
    /* a kezdeti adatok bemásolása a saját tömbbe */ 
    A = (double *) malloc(num_data * timestep * sizeof(double));  
    for (i = 0; i < num_data; i++) A[i] = init[i]; 
    /* a számítás végrehajtása */ 
    for (i = 0; i < timestep-1; i++) {  
       /* a peremadatok cseréje a szomszédokkal */  
       /* küldés balra, fogadás jobbról */  
       if (left != 0) {  
          pvm_initsend(PvmDataDefault); 
          pvm_pkdouble(&A[wh(i,0)],1,1); 
          pvm_send(left, 5); 
          } 
       if (right != 0) {  
          pvm_recv(right, 5); 
          pvm_upkdouble(&rightdata, 1, 1); 
          /* küldés jobbra, fogadás balról */  
          pvm_initsend(PvmDataDefault); 
          pvm_pkdouble(&A[wh(i,num_data-1)],1,1); 
          pvm_send(right, 6);  
          } 
       if (left != 0) {  
          pvm_recv(left, 6); 
          pvm_upkdouble(&leftdata,1,1);  
          } 
       /* készítse el a számításokat erre az iterációra */ 
       for (j = 0; j < num_data; j++) {  
          leftside = (j == 0) ? leftdata : A[wh(i,j-1)];  
          rightside = (j == (num_data-1)) ? rightdata : A[wh(i,j+1)]; 
          if ((j==0)&&(left==0))  
             A[wh(i+1,j)] = 0.0;  
          else if ((j==(num_data-1))&&(right==0)) 
             A[wh(i+1,j)] = 0.0; 
          else  
             A[wh(i+1,j)]= A[wh(i,j)]+delta*(rightside-2*A[wh(i,j)]+leftside);  
          }  
    } 
    /* az eredmények visszaküldése a mester programnak */ 
    pvm_initsend(PvmDataDefault); 
    pvm_pkdouble(&A[0],num_data*timestep,1);  
    pvm_send(master,7); }  
    /* a helyes mérték miatt */  
    pvm_exit();  
  } 

A program fordítását és futtatását a következő animációban tekinthetjük meg: heat.pvm.swf

Feladatok:

1. Készítsük el a Multi-Pascal rendszerben megismert rangsorolás (Ranksort) program PVM változatát C programnyelv felhasználásával!

2. Készítsük el a Multi-Pascal rendszerben megismert mátrixszorzás (ParallelMatrixMultiply) program PVM változatát C programnyelv felhasználásával!

3. Készítsük el a Multi-Pascal rendszerben megismert négyzetgyökvonás (ParallelSqroot) program PVM változatát C programnyelv felhasználásával!

4. Készítsük el a Multi-Pascal rendszerben megismert termelő-fogyasztó (Producer-Consumer) program PVM változatát C programnyelv felhasználásával!

4. fejezet - MPI

4.1. Bevezetés

A Message Passing Interface (Üzenet Átadó Interfész) szabvány egy üzenet átadó könyvtár, amely az MPI Forum megállapodásain alapszik, melynek több mint 40 résztvevő szervezete van, mint például forgalmazók, kutatók, szoftverkönyvtár fejlesztők és felhasználók. A Message Passing Interface célja, hogy létrehozzon egy hordozható, hatékony és rugalmas szabványt az üzenet átadásra és így üzenet átadó programok írására széles körben használható. Ebből a szempontból az MPI az első szabványosított, forgalmazóktól független üzenet átadó könyvtár. Az előnyei az MPI-t használó üzenetküldő programoknak, hogy közel állnak a tervezéskor megfogalmazott hordozhatósághoz, hatékonysághoz és rugalmassághoz. Az MPI nem IEEE vagy ISO szabvány, de valójában ez vált az „ipari szabvánnyá” a HPC platformokon futó üzenetátadó programoknak.

Az MPI egy specifikáció a fejlesztők és üzenetátadó könyvtárak felhasználói számára. Önmagában azonban NEM egy könyvtár - sokkal inkább egy specifikációja annak, hogy egy ilyen könyvtár milyen is legyen. Az interfész céljai, hogy az alábbiakat teljesítse:

  • praktikusság,

  • hordozhatóság,

  • hatékonyság,

  • rugalmasság.

Az interfész specifikációt C/C++ és Fortran nyelvekhez definiálták.

4.2. Történet és Fejlődés

Az MPI számos egyén és csoport fáradozásai kapcsán jött létre egy 2 éves periódusban 1992 és 1994 között. A '80-as és a korai '90-es években fejlődésnek indultak az elosztott memóriájú, párhuzamos számításokat támogató rendszerek, és ezzel egy időben rengeteg egymással nem kompatibilis eszköz jelent meg, amely támogatta párhuzamos programok írását, de ezek gyakran a hordozhatóság, a teljesítmény, vagy a költség szempontjából kedvezőtlenek voltak. Mindezek következtében megjelent az igény egy szabvány bevezetésére:

1992 áprilisában megalakult egy munkacsoport, amely az üzenetek átadásának szabványosítását tűzte ki célül egy eloszott memóriájú környezetben. A csoportot a virginiai Center for Research on Parallel Computing szponzorálta Williamsburgből. Az alapvető, létfontosságú elemeket tárgyalták, amelyet egy üzenet átadó szabványnak támogatnia kell, és létrejött egy csoport, amely a szabványosítási folyamatot tovább folytatta. Ezek után beterjesztésre kerültek az előzetes vázlatok.

1992 novemberében a csoport összeült Minneapolisban. Bemutatták az MPI első vázlatát (MPI-1), majd a csoport elindítja az eljárást hogy megalakítsák az MPI Forumot. Ezáltal létrejön az MPIF, amely hozzávetőleg 40 szervezet 175 tagjából állt, köztük párhuzamos számítógép forgalmazók, szoftver írók, egyetemi és alkalmazott tudósok.

1993 novemberében a Supercomputing konferencián bemutatásra került az MPI vázlata.

A végleges változat 1994 májusában került forgalomba.

Elérhetősége:

Message Passing Interface.

Egyéb dokumentáció:

Tutorials MPI.

Az MPI-2 ott folytatta a specifikációt, ahol az előző abbamaradt, és olyan témákat is érint, amelyek túlmutatnak az első MPI specifikáción. Az eredeti MPI ezután MPI-1 néven vált ismerté. Az MPI-2 1996-ban vált véglegessé.

Manapság az MPI implementációk az MPI-1 és MPI-2 kombinációjából állnak, de kevés olyan megvalósítás létezik, amelyik mindkettő teljes funkcionalitását tartalmazná.

4.2.1. Miért használjunk MPI-t

Szabványosítás: - Az MPI az egyetlen üzenet átadásos könyvtár, amelyet szabványnak tekinthetünk. Gyakorlatilag minden HPC platformon támogatott, valamint minden korábbi üzenet átadásos könyvtárat lecserélt.

Hordozhatóság: - Nincs szükség a forráskód módosítására, amennyiben az alkalmazásunkat egy olyan platformon dolgozunk, amely támogatja az MPI szabványt.

Teljesítmény: - A gyártók implementációi képesek lehetnek bizonyos natív hardver jellemzőket kihasználni, hogy optimális teljesítményt érjenek el.

Funkcionalitás: - Több mint 115 rutin található csak az MPI-1 szabványban.

Elérhetőség: - Több implementáció is elérhető, mind kereskedelmi, mint nyilvános.

Programozási Modell: - Az MPI bármely elosztott memóriájú rendszeren működőképes. Továbbá az MPI-t leggyakrabban arra használják, hogy (a háttérben) megosztott memória modelleket (például adatpárhuzamosítás vagy elosztott rendszerek) hozzanak létre. A következő fejezetekben a C programnyelvet használjuk fel az MPI bemutatására.

4.3. Az MPI használata

A „header” állomány:

Minden program és rutin számára, amely MPI könyvtári hívásokat kezdeményez szükséges az alábbi „header” állomány:

C include állomány: - #include "mpi.h"

MPI hívások formátuma:

Egy MPI program általános felépítését mutatja a következő ábra:

Kommunikátorok és csoportok:

Az MPI kommunikátoroknak és csoportoknak nevezett objektumokat használ, hogy meghatározza processzek mely gyűjteménye kommunikálhat egymással. A legtöbb MPI rutin elvárja egy kommunikátor megadását az argumentumai közt. A későbbiekben a kommunikátorokat és csoportokat részletesen megvizsgáljuk. Jelenleg használjuk egyszerűen az MPI_COMM_WORLD értéket bárhol ahol egy kommunikátor megadása szükséges - ez az előre definiált kommunikátor, amely minden MPI processzt lefed.

Rang:

Egy kommunikátoron belül minden processznek megvan a saját egyedi egész azonosítója, melyet a rendszer oszt ki, amikor a processz elindul. A rangot szokás más néven „taszk ID”-nek is nevezni. A rangok folytonosak, és nullától indulnak. Továbbá a rangot a programozó is használhatja, hogy meghatározza az üzenetek forrását illetve célját. Gyakran feltételekben használjuk az alkalmazáson belül a program vezérléséhez (pl: ha a rang=0 csináld ezt / ha a rang=1 csináld azt).

4.4. Környezet menedzsment rutinok

4.4.1. Parancsok

Az MPI környezet menedzsment rutinok többféle célra használhatók, mint például az MPI környezet inicializálására és lezárására, a környezet azonosítására, stb. A leggyakrabban használatosak az alábbiakban láthatók.

MPI_Init:

Inicializálja az MPI futtatási környezetet. Ezt a függvényt minden MPI programban meg kell hívni, és minden más MPI függvény hívás előtt kell pontosan egyszer meghívni. C programokban az MPI_Init segítségével átadhatóak a parancssori argumentumok minden egyes processznek, habár ezt a szabvány nem követeli meg, és implementáció függő.

Szintaxis:

MPI_Init (&argc,&argv)

MPI_Comm_size:

Megadja, hogy a kommunikátorral kijelölt csoportban hány processz lehet. Leggyakrabban az MPI_COMM_WORLD kommunikátorral használjuk, hogy megadjuk hány processzt használ az alkalmazásunk.

Szintaxis:

MPI_Comm_size (comm,&size)

MPI_Comm_rank:

Megadja a hívó processz rangját a kommunikátoron belül. Kezdetben minden processz kap egy egyedi egész rangot 0 és az MPI_COMM_WORLD kommunikátoron belüli processzek száma-1 között. Erre a rangra gyakran a taszk ID-jeként hivatkozunk. Ha egy processz más kommunikátorokhoz is társítja magát, akkor azokon belül is egyedi rangot kap.

Szintaxis:

MPI_Comm_rank (comm,&rank)

MPI_Abort:

Terminál minden MPI processzt, amely az adott kommunikátorhoz van társítva. A legtöbb MPI implementációban leállít minden processzt, függetlenül a megadott kommunikátortól.

Szintaxis:

MPI_Abort (comm,errorcode)

MPI_Get_processor_name:

Visszadja a processzor nevét. Ezen felül a név hosszát is visszaadja. A „name'” számára kijelölt puffer mérete legalább MPI_MAX_PROCESSOR_NAME karakter méretű legyen. Implementáció függő, hogy mi kerül vissza a „name” változóba (nem feltétlenül ugyanaz mint a kimenete a „hostname” vagy „host” shell parancsoknak).

Szintaxis:

MPI_Get_processor_name (&name,&resultlength)

MPI_Initialized:

Jelzi, hogy az MPI_Init-re vonatkozóan volt-e már hívás - a jelzőkben vagy logikai igaz (1) vagy logikai hamis (0) értéket ad vissza. Az MPI igényli, hogy az MPI_Init-re egyszer, és pontosan egyszer legyen hívás minden egyes processz által. Ez egy problémát vet fel azon modulok számára, amelyek az MPI-t használni akarják és készen állnak arra, hogy meghívják az MPI_Init függvényt. Az MPI_Initialized ezt a problémát oldja meg.

Szintaxis:

MPI_Initialized (&flag)

MPI_Wtime:

Visszaadja az eltelt időt másodpercekben (dupla pontosság) a hívó processzoron.

Szintaxis:

MPI_Wtime ()

MPI_Wtick:

Visszaadja az MPI_Wtime felbontását másodpercekben (dupla pontosság).

Szintaxis:

MPI_Wtick ()

MPI_Finalize:

Lezárja az MPI futtatási környezetet. Ez a függvény az utolsó MPI hívás kell legyen minden MPI programban - semmilyen más MPI hívás nem kezdeményezhető ezután.

Szintaxis:

MPI_Finalize ()

4.4.2. Példa: környezet menedzsment rutinok

  #include "mpi.h" 
  #include <stdio.h> 
  int main(argc,argv) 
  int argc; 
  char *argv[]; {  
  int numtasks, rank, rc; 
  rc = MPI_Init(&argc,&argv);  
  if (rc != MPI_SUCCESS) {  
    printf ("Hiba az MPI program indításakor. Kilépes\n");  
    MPI_Abort(MPI_COMM_WORLD, rc);  
    } 
  MPI_Comm_size(MPI_COMM_WORLD,&numtasks); 
  MPI_Comm_rank(MPI_COMM_WORLD,&rank); 
  printf ("Taszkok száma= %d Saját rangom= %d\n", numtasks,rank); 
  /******* munkát végző kód *******/ 
  MPI_Finalize();  
  } 

4.5. Pont-pont kommunikációs rutinok

4.5.1. Pont-pont műveletek típusai

Az MPI-ben a pont-pont műveletek alatt általában azt értjük, amikor két különböző MPI processz üzenetet küld egymásnak. Az egyik taszk a küldés műveletét végzi, a másik pedig ennek párjaként a fogadást. Különböző célokra különböző küldő és fogadó rutinok vannak. Például:

  • szinkronizált küldés,

  • blokkoló küldés / blokkoló fogadás,

  • nem-blokkoló küldés / nem-blokkoló fogadás,

  • pufferelt küldés,

  • kombinált küldés / fogadás,

  • ,,kész'' küldés (,,ready'' send).

Bármely küldő függvény párba állítható bármely fogadóval. Ezeken felül még az MPI-ben létezik több küldő-fogadó művelet, mint például: amikor a küldő bevárja üzenete megérkezését, vagy lekérdezi, megvizsgálja, hogy megérkezett-e.

4.5.2. Pufferelés

Egy tökéletes világban minden küldési művelet tökéletesen szinkronban volna a vele párban lévő fogadó művelettel. Ez az eset ritkán áll fenn. Valamely módon az MPI implementációnak képesnek kell lennie arra, hogy eltárolja az adatokat, amikor a két folyamat nincs szinkronban. Tekintsük az alábbi két esetet:

A küldés 5 másodperccel az előtt történik, hogy a fogadás készen állna - hol van az üzenet amíg várakozik?

Több üzenet is érkezik ugyanahhoz a folyamathoz, de az csak egyet tud fogadni egy időben - mi történik az üzenetekkel, melyek „feltorlódnak”?

Ezen esetekben az MPI implementációja - és nem a szabvány - határozza meg, hogy mi történik az adatokkal az ilyen esetekben. Általában egy rendszer puffer van fenntartva az átmeneti adatok tárolására.

A rendszer puffer:

  • A programozó számára teljesen transzparens, mivel az MPI könyvtár kezeli.

  • Egy véges erőforrás, melyet könnyen ki lehet meríteni.

  • Gyakran rejtélyes és rosszul dokumentált.

  • A fogadó, a küldő, vagy akár mindkét oldalon is létezhet.

  • Javíthatja a program teljesítményét, mivel így a küldés-fogadás műveletek aszinkron módon is végrehajtódhatnak.

A felhasználó által kezelt címteret (gyakorlatilag a program változói) alkalmazás puffernek nevezzük. Az MPI támogatja a felhasználó által pufferelt küldést is.

4.5.3. Blokkoló kontra nem-blokkoló

A legtöbb MPI pont-pont kommunikációs rutin használható blokkoló vagy nem-blokkoló módon is.

Blokkoló:

  • Egy blokkoló küldési függvény csak azután fog visszatérni, ha már biztonságos az alkalmazás puffer (a küldési adatok) módosítása újrafelhasználás céljából. A biztonságos azt jelenti, hogy a módosítások nem lesznek kihatással a fogadónak szánt adatokra. A biztonságosság azonban nem jelenti azt, hogy az adat valójában meg is érkezett - elképzelhető, hogy épp a rendszer pufferbe került.

  • Egy blokkoló küldés lehet szinkronos, ami azt jelenti, hogy a fogadó taszkkal kézfogás zajlik a biztonságos küldés lebonyolítását illetően.

  • Egy blokkoló küldés lehet aszinkronos, amennyiben egy rendszer puffert használunk, hogy tároljuk az adatokat a későbbi küldéshez.

  • Egy blokkoló fogadás csak azután tér vissza, ha az adat már megérkezett, és a program számára készen áll a feldolgozásra.

Nem-blokkoló:

  • A nem-blokkoló küldés és fogadás műveletek hasonlóan viselkednek - szinte azonnal visszatérnek. Nem várnak semmilyen kommunikációs folyamat befejeződésére, mint például az üzenet másolására az alkalmazás pufferből a rendszer pufferbe, vagy az üzenet tényleges megérkezésére.

  • A nem-blokkoló műveletek csupán „kérik” az MPI könyvtárt, hogy hajtsa végre azt, amikor képes rá. A felhasználó nem tudja megjósolni, hogy ez mikor is következik be.

  • Egészen addig nem biztonságos az alkalmazás puffer (a saját változók) módosítása, amíg nem tudni biztosan, hogy a kért nem-blokkoló műveletet a könyvtár végrehajtotta. Erre a célra vannak „várakozó” függvények.

  • A nem-blokkoló kommunikációt főképp arra használjuk, hogy átlapoltan végezzünk számításokat és kommunikációt is, ezáltal kihasználva a lehetséges teljesítmény növekedést.

4.5.4. Sorrend és kiegyenlítettség

Sorrend:

  • Az MPI garantálja, hogy az üzenetek nem fogják megelőzni egymást.

  • Ha egy küldő két üzenetet (Üzenet 1 és Üzenet 2) egymást követően küldi ugyanarra a címre, a vele párban lévő fogadónak, akkor a fogadás művelete az 1-es üzenetet a 2-es üzenet előtt fogja megkapni.

  • Ha egy fogadó két fogadás műveletet végez (Fogadás 1 és Fogadás 2) egymás után, amely ugyanazt az üzenetet keresi, akkor az 1-es fogadás a 2-es fogadás művelet előtt fogja az üzenetet megkapni.

  • A sorrend szabályok nem érvényesek, ha több szálon folynak kommunikációs műveletek.

Kiegyenlítettség:

  • Az MPI nem garantálja a kiegyenlítettséget - a programozó dolga, hogy meggátolja a ,,kiéhezést''.

  • Például: a 0-ás taszk üzenetet küld a 2-es számú taszknak. Ezzel egy időben az 1-es számú taszk is üzenetet küld, amelyet a 2-es tud fogadni. A két küldés közül ekkor csak az egyik lehet sikeres. A következő ábra ezt szemlélteti:

4.5.5. MPI üzenetátadó rutinok argumentumai

Az MPI pont-pont kommunikációs rutinok argumentum listája általában a következő formátumú:

Puffer (buffer):

A program (alkalmazás) cím területén belül a küldeni vagy fogadni kívánt adatot kijelölő részt. A legtöbb esetben ez egyszerűen a küldendő/fogadandó változó neve. C programokban ezt cím szerint kell átadni (&var1).

Adatok száma (count):

Jelöli, hogy az adott típusból hány adatelemet szeretnénk küldeni/fogadni.

Adattípus (type):

Portabilitási szempontokból az MPI előre definiál bizonyos elemi adattípusokat. Az alábbi táblázat a szabvány által előírtakat tartalmazza:

Megjegyzések:

  • A programozók létrehozhatják a saját adattípusaikat is (lásd a 4.7. alfejezetet).

  • Az MPI_BYTE és MPI_PACKED típusoknak nincs hagyományos megfelelője C-ben.

  • Az MPI szabvány tartalmaz egy opcionális adattípust is, az MPI_LONG_LONG_INT típust.

  • Bizonyos implementációk tartalmazhatnak ezen felül más egyedi típusokat is, melyeket érdemes áttanulmányozni az MPI header állományban.

Cél (destination):

Ez az argumentum a küldő függvényekben adja meg azt a processzt, melynek az üzenetet kézbesíteni kell. A fogadó folyamat rangját kell megadni.

Forrás (source):

Ez a paraméter a fogadó függvények számára jelzi, hogy mely folyamattól vár üzenetet. A küldő processz rangját kell megadni. Használható az MPI_ANY_SOURCE értékkel, amely bármely taszktól fogadható üzenet.

Címke (tag):

Tetszőleges nemnegatív egész szám, melyet a programozó jelöl ki abból a célból, hogy egyedi módon azonosíthasson egy üzenetet. A küldés és fogadás műveleteknél az üzenetcímkéknek párban kell lennie, meg kell egyeznie. Üzenetek fogadása esetén használható az MPI_ANY_TAG érték, mellyel bármilyen üzenet fogadható, címkétől függetlenül. A szabvány garantálja, hogy a 0-32767 intervallumból bármilyen egész érték használható címkeként, de a legtöbb implementációban ettől sokkal nagyobb tartomány is elérhető.

Kommunikátor (comm):

Jelzi a kommunikációs kontextust, vagyis azt a processz halmazt, ahol a forrás és cél mezők helyesek. Hacsak a programozó explicit módon nem hoz létre új kommunikátorokat, akkor az előre definiált MPI_COMM_WORLD az általánosan használt.

Státusz (status):

Egy fogadás művelet esetén jelzi az üzenet forrását és címkéjét. C-ben ez egy mutató egy előre definiált MPI_Status struktúrára.

Kérelem (request):

Nem-blokkoló küldés és fogadás műveletek által használatos. Tekintve hogy a nem-blokkoló műveletek még az előtt visszatérhetnek mielőtt a rendszer puffer terület lefoglalásra kerülne, így a rendszer kijelöl egy egyedi „kérelem azonosítót”. A programozó ezt a rendszer által adott „leírót” később felhasználhatja (egy várakozás műveletnél), hogy megállapítsa a nem-blokkoló művelet sikerességét. C-ben ez egy mutató egy előre definiált MPI_Request struktúrára.

4.5.6. Blokkoló üzenetátadó függvények

A leggyakrabban használt blokkoló MPI üzenet átadó rutinok leírása a következő:

MPI_Send:

Alapvető blokkoló küldő függvény. Csak azután tér vissza, ha az alkalmazás puffer a küldő folyamatban már szabadon újrafelhasználható. Fontos említeni, hogy a függvény különböző rendszereken különböző módon lehet megvalósítva. Az MPI szabvány megengedi egy rendszer puffer használatát is, de nem kötelező. Bizonyos implementációk szinkronos küldést használhatnak az alapvető blokkoló küldés megvalósítására.

Szintaxis:

MPI_Send (&buf,count,datatype,dest,tag,comm)

MPI_Recv:

Fogad egy üzenetet és blokkol egészen addig, amíg a kért adat készen nem áll az alkalmazás pufferben a fogadó folyamatban.

Szintaxis:

MPI_Recv (&buf,count,datatype,source,tag,comm,&status)

MPI_Ssend:

Szinkronos blokkoló küldés: küld egy üzenetet és blokkol, amíg az adat a küldő alkalmazás pufferében újrafelhasználhatóvá válik, valamint amíg a célfolyamat el nem kezdte fogadni az üzenetet.

Szintaxis:

MPI_Ssend (&buf,count,datatype,dest,tag,comm)

MPI_Bsend:

Pufferelet blokkoló küldés: lehetőséget ad a programozónak, hogy a szükséges memóriaterületet lefoglalja amibe az adatot át lehet másolni, amíg a kézbesítésre sor nem kerül. Elkerülhetőek vele a problémák, amelyek a nem elegendő rendszer puffer méretből adódnak. A függvény visszatér, miután az adat átmásolásra került az alkalmazás pufferből a lefoglalt küldési pufferbe. Az MPI_Buffer_attach függvénnyel együtt kell használni.

Szintaxis:

MPI_Bsend (&buf,count,datatype,dest,tag,comm)

MPI_Buffer_attach/MPI_Buffer_detach:

A programozó használja, hogy lefoglaljon/felszabadítson memóriaterületet az MPI_Bsend függvény számára. A méret paramétert bájtként kell értelmezni, nem pedig adatelem mértékként. Minden folyamat csak egy puffert hozhat létre. Megjegyezzük, hogy az IBM implementáció MPI_BSEND_OVERHEAD mennyiségű bájtot hozzátesz a lefoglalt pufferhez mint felesleg.

Szintaxis:

MPI_Buffer_attach (&buffer,size)

MPI_Buffer_detach (&buffer,size)

MPI_Rsend:

Blokkoló kész küldés. Csak abban az esetben szabad használni, ha a programozó biztos abban, hogy a párban lévő fogadás művelet már várakozik.

Szintaxis:

MPI_Rsend (&buf,count,datatype,dest,tag,comm)

MPI_Sendrecv:

Elküld egy üzenetet, és végez egy fogadás műveletet is mielőtt blokkolna. Addig blokkol, amíg a küldési puffer újrafelhasználhatóvá nem válik, valamint a fogadó alkalmazás puffer az üzenetet nem tartalmazza.

Szintaxis:

MPI_Sendrecv (&sendbuf,sendcount,sendtype,dest,sendtag, &recvbuf,recvcount,recvtype,source,recvtag, comm,&status)

MPI_Wait, MPI_Waitany, MPI_Waitall, MPI_Waitsome:

Az MPI_Wait blokkol amíg a megadott nem-blokkoló küldés vagy fogadás művelet be nem fejeződik. Több nem-blokkoló művelet kezelésére a programozó megadhatja hogy egy, vagy néhány művelet befejeződéséig várjon.

Szintaxis:

MPI_Wait (&request,&status)

MPI_Waitany (count,&array_of_requests,&index,&status)

MPI_Waitall (count,&array_of_requests,&array_of_statuses)

MPI_Waitsome (incount,&array_of_requests,&outcount, &array_of_offsets, &array_of_statuses)

MPI_Probe:

Végrehajt egy blokkoló tesztet egy üzenetre. Az MPI_ANY_SOURCE és MPI_ANY_TAG értékek megadhatók, hogy bármely feladótól vagy bármely címkéjű üzenetre teszteljünk. A visszaadott státusz struktúrában a tényleges forrás és címkét kapjuk meg az status.MPI_SOURCE és status.MPI_TAG mezőkben.

Szintaxis:

MPI_Probe (source,tag,comm,&status)

4.5.7. Példa: blokkoló üzenet küldő rutinok

A 0-ás taszk „megpingeli” az 1-es taszkot és megvárja a rá adott választ.

  #include "mpi.h" 
  #include <stdio.h> 
  int main(argc,argv) 
  int argc; 
  char *argv[]; { 
  int numtasks, rank, dest, source, rc, count, tag=1; 
  char inmsg, outmsg='x'; 
  MPI_Status Stat; 
  MPI_Init(&argc,&argv);  
  MPI_Comm_size(MPI_COMM_WORLD, &numtasks);  
  MPI_Comm_rank(MPI_COMM_WORLD, &rank); 
  if (rank == 0) {  
    dest = 1; 
    source = 1; 
    rc = MPI_Send(&outmsg, 1, MPI_CHAR, dest, tag, MPI_COMM_WORLD); 
    rc = MPI_Recv(&inmsg, 1, MPI_CHAR, source, tag, MPI_COMM_WORLD, &Stat);  
    } 
  else if (rank == 1) { 
    dest = 0;  
    source = 0; 
    rc = MPI_Recv(&inmsg, 1, MPI_CHAR, source, tag, MPI_COMM_WORLD, &Stat); 
    rc = MPI_Send(&outmsg, 1, MPI_CHAR, dest, tag, MPI_COMM_WORLD);  
    } 
  rc = MPI_Get_count(&Stat, MPI_CHAR, &count); 
  printf("Task %d: Received %d char(s) from task %d with tag %d \n", rank, count, Stat.MPI_SOURCE, Stat.MPI_TAG); 
  MPI_Finalize();  
  } 

4.5.8. Nem-blokkoló üzenetátadó rutinok

A leggyakrabban használt nem-blokkoló MPI üzenet átadó rutinok leírása az alábbiak:

MPI_Isend:

Azonosít egy memóriaterületet, amely küldési pufferként fog szolgálni. A feldolgozás azonnal folytatódik, anélkül hogy megvárná míg az üzenet átmásolódik az alkalmazás pufferből. Egy kommunikációs kérés leírót kapunk vissza, amely a folyamatban lévő üzenet státuszának kezelésére szolgál. A program nem módosíthatja az alkalmazás puffert amíg az MPI_Wait vagy MPI_Test függvényekkel meg nem bizonyosodott róla, hogy a nem-blokkoló művelet véget ért.

Szintaxis:

MPI_Isend (&buf,count,datatype,dest,tag,comm,&request)

MPI_Irecv:

Azonosít egy memóriaterületet, amely fogadási pufferként fog szolgálni. A feldolgozás azonnal folytatódik anélkül, hogy megvárná az üzenet fogadásának és alkalmazásának pufferbe történő másolását. Egy kommunikációs kérelem azonosítót kapunk vissza, amely a folyamatban lévő üzenet státuszának lekérdezésére szolgál. A programnak az MPI_Wait vagy MPI_Test függvényeket kell meghívnia, hogy meghatározza a nem-blokkoló fogadás műveletének sikerességét, azaz a kért üzenet elérhető-e az alkalmazás pufferben.

Szintaxis:

MPI_Irecv (&buf,count,datatype,source,tag,comm,&request)

MPI_Issend:

Nem-blokkoló szinkronos küldés. Hasonló az MPI_Isend()-hez, kivéve hogy az MPI_Wait() vagy MPI_Test() azt jelzi, hogy a cél processz megkapta-e az üzenetet.

Szintaxis:

MPI_Issend (&buf,count,datatype,dest,tag,comm,&request)

MPI_Ibsend:

Nem-blokkoló pufferelt küldés. Hasonló az MPI_Bsend()-hez, kivéve hogy MPI_Wait() or MPI_Test() azt jelzi, hogy a cél processz megkapta-e az üzenetet. Az MPI_Buffer_attach függvénnyel együtt kell használni.

Szintaxis:

MPI_Ibsend (&buf,count,datatype,dest,tag,comm,&request)

MPI_Irsend:

Nem-blokkoló kész küldés. Hasonló az MPI_Rsend()-hez, kivéve hogy MPI_Wait() or MPI_Test() azt jelzi, hogy a cél processz megkapta-e az üzenetet. Kizárólag akkor szabad használni, ha a programozó biztos benne, hogy a párban lévő fogadás művelet már várakozik.

Szintaxis:

MPI_Irsend (&buf,count,datatype,dest,tag,comm,&request)

MPI_Test, MPI_Testany, MPI_Testall, MPI_Testsome:

Az MPI ellenőrzi a státuszát a megadott nem-blokkoló küldés vagy fogadás műveletnek. A „flag” paraméterben logikai igaz (1) értéket kapunk, ha a művelet befejeződött és logikai hamisat (0) ha nem. Többszörös nem-blokkoló műveletet megadása esetén megadható, hogy bármely, az összes vagy néhány művelet befejezése jelentsen sikerességet.

Szintaxis:

MPI_Test (&request,&flag,&status),

MPI_Testany (count,&array_of_requests,&index,&flag,&status),

MPI_Testall (count,&array_of_requests,&flag,&array_of_statuses),

MPI_Testsome (incount,&array_of_requests,&outcount, &array_of_offsets, &array_of_statuses)

MPI_Iprobe:

Végrehajt egy nem-blokkoló tesztet egy üzenetre. Az MPI_ANY_SOURCE és MPI_ANY_TAG értékeket használhatjuk hogy bármely feladótól, vagy bármely címkéjű üzenetre teszteljünk. A „flag” paraméter logikai igaz (1) ha az üzenet megérkezett, és logikai hamis (0) ha nem. A visszaadott státusz struktúrában a tényleges feladót és címkét kaphatjuk meg status.MPI_SOURCE és status.MPI_TAG értékek formájában.

Szintaxis:

MPI_Iprobe (source,tag,comm,&flag,&status)

4.5.9. Példa: nem-blokkoló üzenet átadó rutinokra

A következő példaprogramban a legközelebbi szomszéddal való üzenetváltás történik gyűrű topológiában.

  #include "mpi.h" 
  #include <stdio.h> 
  int main(argc,argv) int argc; 
  char *argv[]; { 
  int numtasks, rank, next, prev, buf[2], tag1=1, tag2=2;  
  MPI_Request reqs[4]; 
  MPI_Status stats[4]; 
  MPI_Init(&argc,&argv); 
  MPI_Comm_size(MPI_COMM_WORLD, &numtasks); 
  MPI_Comm_rank(MPI_COMM_WORLD, &rank); 
  prev = rank-1;  
  next = rank+1;  
  if (rank == 0) prev = numtasks - 1;  
  if (rank == (numtasks - 1)) next = 0; 
  MPI_Irecv(&buf[0], 1, MPI_INT, prev, tag1, MPI_COMM_WORLD, &reqs[0]); 
  MPI_Irecv(&buf[1], 1, MPI_INT, next, tag2, MPI_COMM_WORLD, &reqs[1]); 
  MPI_Isend(&rank, 1, MPI_INT, prev, tag2, MPI_COMM_WORLD, &reqs[2]); 
  MPI_Isend(&rank, 1, MPI_INT, next, tag1, MPI_COMM_WORLD, &reqs[3]);  
    { némi munkát végzünk } 
  MPI_Waitall(4, reqs, stats); 
  MPI_Finalize();  
  } 

4.6. Kollektív kommunikációs rutinok

4.6.1. Minden vagy semmi

A kollektív kommunikációban egy kommunikátor hatáskörén belül minden processznek részt kell vennie. Minden processz alapértelmezés szerint tagja az MPI_COMM_WORLD kommunikátornak. A programozó felelőssége a biztosítéka annak, hogy a kommunikátoron belül minden processz részt vegyen valamelyik kollektív műveletben.

Kollektív műveletek típusai:

  • Szinkronizálás - A folyamatok várnak, amíg egy csoport minden tagja el nem érte a szinkronizációs pontot.

  • Adatmozgatás - Szétküldés, szétdarabolás/összegyűjtés, mindenki mindenkinek.

  • Kollektív számítás - A csoport egy tagja adatokat gyűjt be a többitől és ezeken műveleteket végez (minimum, maximum, összeadás, szorzás, stb.).

Programozói megfontolások és korlátozások:

A kollektív műveletek mind blokkolóak. Továbbá az ilyen műveletek nem fogadnak üzenet címke paramétereket. A kollektív műveletek a folyamatok részhalmazain belül úgy valósulnak meg, hogy először a részhalmazokat új csoportokra darabolják, majd az új csoportokat új kommunikátorokhoz csatolják. Megszorítás a kollektív kommunikációban továbbá, hogy csak az MPI előre definiált típusai használhatók, a származtatott MPI típusok nem.

4.6.2. Rutinok

MPI_Barrier:

Egy barrier szinkronizációt hoz létre a csoporton belül. Minden folyamat, amely eléri az MPI_Barrier hívást blokkol, egészen addig amíg minden egyes folyamat a csoportban el nem éri ugyanazt az MPI_Barrier hívást.

Szintaxis:

MPI_Barrier (comm)

MPI_Bcast:

Szétsugároz (szétküld) egy üzenetet a „root” rangú processzből a csoport minden más processzének.

Szintaxis:

MPI_Bcast (&buffer,count,datatype,root,comm)

MPI_Scatter:

Szétoszt különböző üzeneteket egy adott forrásból minden folyamatnak a csoportban.

Szintaxis:

MPI_Scatter (&sendbuf,sendcnt,sendtype,&recvbuf, recvcnt,recvtype,root,comm)

MPI_Gather:

Összegyűjt különböző üzeneteket minden taszktól a csoportban és egyetlen célfolyamatnak küldi. Az MPI_Scatter művelet megfordítása.

Szintaxis:

MPI_Gather (&sendbuf,sendcnt,sendtype,&recvbuf, recvcount,recvtype,root,comm)

MPI_Allgather:

Adatok összefűzése a csoport minden tagjához. Gyakorlatilag minden folyamat a csoportban végrehajt egy egy-mind szétküldő műveletet.

Szintaxis:

MPI_Allgather (&sendbuf,sendcount,sendtype,&recvbuf, recvcount,recvtype,comm)

MPI_Reduce:

Egy redukciós műveletet hajt végre a csoport minden tagján és az eredményt egy folyamatba teszi.

Szintaxis:

MPI_Reduce (&sendbuf,&recvbuf,count,datatype,op,root,comm)

Az előre definiált MPI redukciós műveletek az alábbi táblázatban láthatók. Továbbá a felhasználó létrehozhat saját redukciós függvényt is az MPI_Op_create függvénnyel.

MPI_Allreduce:

Végrehajt egy redukciós műveletet minden folyamaton, és az eredményt a csoport minden tagjának elküldi. Ez ekvivalens egy MPI_Reduce után meghívott MPI_Bcast művelettel.

Szintaxis:

MPI_Allreduce (&sendbuf,&recvbuf,count,datatype,op,comm)

MPI_Reduce_scatter:

Először egy elemenkénti redukciót hajt végre egy vektoron a csoport minden tagjának felhasználásával. Ezek után az eredményvektort felbontja diszjunkt elemekre, melyeket minden folyamat között szétoszt. Ekvivalens az MPI_Reduce után meghívott MPI_Scatter művelettel.

Szintaxis:

MPI_Reduce_scatter (&sendbuf,&recvbuf,recvcount,datatype,op,comm)

MPI_Alltoall:

Minden folyamat a csoportban végrehajt egy szétszórás műveletet, elküldve egy üzenetet a csoport minden tagjának index szerinti sorrendben.

Szintaxis:

MPI_Alltoall (&sendbuf,sendcount,sendtype,&recvbuf,recvcnt,recvtype,comm)

MPI_Scan:

Végrehajt egy letapogatás műveletet, tekintettel egy redukciós műveletre a csoport tagjai közt.

Szintaxis:

MPI_Scan (&sendbuf,&recvbuf,count,datatype,op,comm)

4.6.3. Példa: kollektív kommunikációs függvények

A következő program végrehajt egy szétosztás (scatter) műveletet egy tömb sorain.

  #include "mpi.h" 
  #include <stdio.h> 
  #define SIZE 4 
  int main(argc,argv) int argc; 
  char *argv[]; { 
  int numtasks, rank, sendcount, recvcount, source; 
  float sendbuf[SIZE][SIZE] = {  
    {1.0, 2.0, 3.0, 4.0},  
    {5.0, 6.0, 7.0, 8.0},  
    {9.0, 10.0, 11.0, 12.0},  
    {13.0, 14.0, 15.0, 16.0} }; 
  float recvbuf[SIZE]; 
  MPI_Init(&argc,&argv);  
  MPI_Comm_rank(MPI_COMM_WORLD, &rank); 
  MPI_Comm_size(MPI_COMM_WORLD, &numtasks); 
  if (numtasks == SIZE) {  
    source = 1; 
    sendcount = SIZE; 
    recvcount = SIZE;  
    MPI_Scatter(sendbuf,sendcount,MPI_FLOAT,recvbuf,recvcount, MPI_FLOAT,source,MPI_COMM_WORLD); 
  printf("rank= %d Results: %f %f %f %f\n",rank,recvbuf[0], recvbuf[1],recvbuf[2],recvbuf[3]);  
  } else 
  printf("Must specify %d processors. Terminating.\n",SIZE); 
  MPI_Finalize();  
  } 

A mintaprogram kimenete:

  rank= 0 Results: 1.000000 2.000000 3.000000 4.000000 
  rank= 1 Results: 5.000000 6.000000 7.000000 8.000000  
  rank= 2 Results: 9.000000 10.000000 11.000000 12.000000  
  rank= 3 Results: 13.000000 14.000000 15.000000 16.000000  

4.7. Származtatott adattípusok

A korábbiakban láttuk, hogy az MPI saját adattípusokat definiál. Ezen felül azonban az MPI lehetőséget biztosít arra, hogy saját adatstruktúrákat hozzunk létre a primitív adattípusok szekvenciájaként. Az ilyen, felhasználó által létrehozott adattípusokat származtatott adattípusoknak nevezzük.

A primitív adattípusok folytonosak. A származtatott adattípusok megengedik, hogy egyszerű és kényelmes módon adhassunk meg nem folytonos adattípusokat, ugyanakkor úgy kezeljük őket mintha azok volnának.

Az MPI több metódust is biztosít az alábbi származtatott adattípusok létrehozására:

  • Folytonos

  • Vektor

  • Indexelt

  • Struktúra

4.7.1. Származtatott adattípusok függvényei

MPI_Type_contiguous:

A legegyszerűbb konstruktor. Új adattípust hoz létre úgy, hogy „count” darabszámú másolatot készít egy létező adattípusból.

Szintaxis:

MPI_Type_contiguous (count,oldtype,&newtype)

MPI_Type_vector, MPI_Type_hvector:

Hasonló a folytonos típushoz, de megenged szabályos réseket (lépésközöket) az elemek közt. AZ MPI_Type_hvector azonos az MPI_Type_vector függvénnyel, kivéve hogy a lépésközt bájtokban kell megadni.

Szintaxis:

MPI_Type_vector (count,blocklength,stride,oldtype,&newtype)

MPI_Type_indexed, MPI_Type_hindexed:

Az input adattípus méretezésének tömbje, amely az új adattípus térképeként fog szolgálni. Az MPI_Type_hindexed azonos az MPI_Type_indexed függvénnyel, kivéve hogy az eltolásokat bájtokban kell megadni.

Szintaxis:

MPI_Type_indexed (count,blocklens[],offsets[],old_type,&newtype)

MPI_Type_struct:

Egy új adattípus jön létre, pontosan a komponens adattípusokból feltérképezett, leírt módon.

Szintaxis:

MPI_Type_struct (count,blocklens[],offsets[],old_types,&newtype)

MPI_Type_extent:

Visszaadja a megadott adattípus méretét bájtokban kifejezve. Olyan MPI függvényekben hasznos, amelyek igénylik valamely lépésköz megadását bájtokban.

Szintaxis:

MPI_Type_extent (datatype,&extent)

MPI_Type_commit:

Elfogadtat egy új adattípust a rendszerrel. Minden felhasználó által létrehozott (származtatott) típushoz szükséges.

Szintaxis:

MPI_Type_commit (&datatype)

MPI_Type_free:

Felszabadítja a megadott típusú objektumot. Ez a függvény különösen fontos ahhoz, hogy megakadályozzuk a memória kimerülését abban az esetben, ha sok adatelemet hozunk létre, például egy ciklusban.

Szintaxis:

MPI_Type_free (&datatype)

4.7.2. Példák: származtatott adattípusok

Folytonos adattípus:

A program létrehoz egy adattípust, amely egy tömb egy sorát jelképezi és szétoszt egy-egy sort minden folyamatnak.

  #include "mpi.h" 
  #include <stdio.h> 
  #define SIZE 4 
  int main(argc,argv) 
  int argc; char *argv[]; {  
    int numtasks, rank, source=0, dest, tag=1, i; 
    float a[SIZE][SIZE] = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0};  
    float b[SIZE]; 
    MPI_Status stat; 
    MPI_Datatype rowtype; 
    MPI_Init(&argc,&argv); 
    MPI_Comm_rank(MPI_COMM_WORLD, &rank); 
    MPI_Comm_size(MPI_COMM_WORLD, &numtasks); 
    MPI_Type_contiguous(SIZE, MPI_FLOAT, &rowtype); 
    MPI_Type_commit(&rowtype); 
    if (numtasks == SIZE) {  
       if (rank == 0) {  
          for (i=0; i<numtasks; i++)  
             MPI_Send(&a[i][0], 1, rowtype, i, tag, MPI_COMM_WORLD);  
       } 
          MPI_Recv(b, SIZE, MPI_FLOAT, source, tag, MPI_COMM_WORLD, &stat);  
          printf("rank= %d b= %3.1f %3.1f %3.1f %3.1f\n", rank,b[0],b[1],b[2],b[3]);  
    } 
    else printf("Must specify %d processors. Terminating.\n",SIZE); 
    MPI_Type_free(&rowtype);  
    MPI_Finalize();  
  } 

A mintaprogram kimenete:

  rank= 0 b= 1.0 2.0 3.0 4.0 
  rank= 1 b= 5.0 6.0 7.0 8.0 
  rank= 2 b= 9.0 10.0 11.0 12.0 
  rank= 3 b= 13.0 14.0 15.0 16.0 

Vektor adattípus:

Létrehoz egy adattípust, amely egy tömb egy oszlopát reprezentálja, és szétoszt minden folyamatnak egy-egy oszlopot.

  #include "mpi.h" 
  #include <stdio.h> 
  #define SIZE 4 
  int main(argc,argv)  
  int argc; 
  char *argv[]; {  
    int numtasks, rank, source=0, dest, tag=1, i; 
    float a[SIZE][SIZE] = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0}; 
    float b[SIZE]; 
    MPI_Status stat; 
    MPI_Datatype columntype; 
    MPI_Init(&argc,&argv);  
    MPI_Comm_rank(MPI_COMM_WORLD, &rank); 
    MPI_Comm_size(MPI_COMM_WORLD, &numtasks);  
    MPI_Type_vector(SIZE, 1, SIZE, MPI_FLOAT, &columntype);  
    MPI_Type_commit(&columntype); 
    if (numtasks == SIZE) {  
       if (rank == 0) {  
          for (i=0; i<numtasks; i++)  
             MPI_Send(&a[0][i], 1, columntype, i, tag, MPI_COMM_WORLD);  
       }  
       MPI_Recv(b, SIZE, MPI_FLOAT, source, tag, MPI_COMM_WORLD, &stat); 
       printf("rank= %d b= %3.1f %3.1f %3.1f %3.1f\n", rank,b[0],b[1],b[2],b[3]);  
    }  
    else  
       printf("Must specify %d processors. Terminating.\n",SIZE);  
    MPI_Type_free(&columntype); 
    MPI_Finalize(); 
  }  

A mintaprogram kimenete:

  rank= 0 b= 1.0 5.0 9.0 13.0 
  rank= 1 b= 2.0 6.0 10.0 14.0 
  rank= 2 b= 3.0 7.0 11.0 15.0 
  rank= 3 b= 4.0 8.0 12.0 16.0  

Indexelt adattípus:

Létrehoz egy adattípust úgy, hogy kiválogat egy tömbből változó méretű részeket és szétosztja ezeket minden folyamatnak.

  #include "mpi.h" 
  #include <stdio.h> 
  #define NELEMENTS 6 
  int main(argc,argv)  
  int argc; char *argv[]; {  
    int numtasks, rank, source=0, dest, tag=1, i;  
    int blocklengths[2], displacements[2];  
    float a[16] = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0};  
    float b[NELEMENTS]; 
    MPI_Status stat;  
    MPI_Datatype indextype; 
    MPI_Init(&argc,&argv);  
    MPI_Comm_rank(MPI_COMM_WORLD, &rank);  
    MPI_Comm_size(MPI_COMM_WORLD, &numtasks); 
    blocklengths[0] = 4;  
    blocklengths[1] = 2; 
    displacements[0] = 5;  
    displacements[1] = 12;  
    MPI_Type_indexed(2, blocklengths, displacements, MPI_FLOAT, &indextype);  
    MPI_Type_commit(&indextype); 
    if (rank == 0) {  
       for (i=0; i<numtasks; i++)  
          MPI_Send(a, 1, indextype, i, tag, MPI_COMM_WORLD);  
    }  
    MPI_Recv(b, NELEMENTS, MPI_FLOAT, source, tag, MPI_COMM_WORLD, &stat); 
    printf("rank= %d b= %3.1f %3.1f %3.1f %3.1f %3.1f %3.1f\n", rank,b[0],b[1],b[2],b[3],b[4],b[5]); 
    MPI_Type_free(&indextype);  
    MPI_Finalize();  
  } 

A mintaprogram kimente:

  rank= 0 b= 6.0 7.0 8.0 9.0 13.0 14.0 
  rank= 1 b= 6.0 7.0 8.0 9.0 13.0 14.0 
  rank= 2 b= 6.0 7.0 8.0 9.0 13.0 14.0 
  rank= 3 b= 6.0 7.0 8.0 9.0 13.0 14.0 

Struktúra adattípus:

Létrehoz egy adattípust, amely egy részecskét reprezentál és szétosztja ilyen típusú részecskék egy tömbjét minden folyamatnak.

  #include "mpi.h" 
  #include <stdio.h> 
  #define NELEM 25 
  int main(argc,argv)  
  int argc; char *argv[]; {  
    int numtasks, rank, source=0, dest, tag=1, i; 
    typedef struct {  
       float x, y, z;  
       float velocity;  
       int n, type;  
    } Particle;  
    Particle p[NELEM], particles[NELEM];  
    MPI_Datatype particletype, oldtypes[2];  
    int blockcounts[2]; 
    /* MPI_Aint type used to be consistent with syntax of */  
    /* MPI_Type_extent routine */  
    MPI_Aint offsets[2], extent; 
    MPI_Status stat; 
    MPI_Init(&argc,&argv);  
    MPI_Comm_rank(MPI_COMM_WORLD, &rank);  
    MPI_Comm_size(MPI_COMM_WORLD, &numtasks);  
    /* Setup description of the 4 MPI_FLOAT fields x, y, z, velocity */  
    offsets[0] = 0; 
    oldtypes[0] = MPI_FLOAT; 
    blockcounts[0] = 4; 
    /* Setup description of the 2 MPI_INT fields n, type */  
    /* Need to first figure offset by getting size of MPI_FLOAT */  
    MPI_Type_extent(MPI_FLOAT, &extent);  
    offsets[1] = 4 * extent; 
    oldtypes[1] = MPI_INT; 
    blockcounts[1] = 2; 
    /* Now define structured type and commit it */  
    MPI_Type_struct(2, blockcounts, offsets, oldtypes, &particletype); 
    MPI_Type_commit(&particletype); 
    /* Initialize the particle array and then send it to each task */ 
    if (rank == 0) {  
       for (i=0; i<NELEM; i++) {  
          particles[i].x = i * 1.0;  
          particles[i].y = i * -1.0;  
          particles[i].z = i * 1.0;  
          particles[i].velocity = 0.25; 
          particles[i].n = i;  
          particles[i].type = i % 2;  
       }  
       for (i=0; i<numtasks; i++)  
          MPI_Send(particles, NELEM, particletype, i, tag, MPI_COMM_WORLD);  
    }  
    MPI_Recv(p, NELEM, particletype, source, tag, MPI_COMM_WORLD, &stat); 
    /* Print a sample of what was received */  
    printf("rank= %d %3.2f %3.2f %3.2f %3.2f %d %d\n", rank,p[3].x, p[3].y,p[3].z,p[3].velocity,p[3].n,p[3].type); 
    MPI_Type_free(&particletype);  
    MPI_Finalize(); 
  } 

A mintaprogram kimente:

  rank= 0 3.00 -3.00 3.00 0.25 3 1 
  rank= 2 3.00 -3.00 3.00 0.25 3 1 
  rank= 1 3.00 -3.00 3.00 0.25 3 1 
  rank= 3 3.00 -3.00 3.00 0.25 3 1 

4.8. Csoport és kommunikátor kezelő rutinok

4.8.1. Csoportok kontra kommunikátorok

Egy csoport a processzek egy rendezett halmaza. Minden folyamat egy csoportban rendelkezik egy egyedi egész ranggal. A rangok nullától kezdődnek és -ig tartanak, ahol a processzek száma a csoportban. Az MPI-ben a csoport egy bizonyos objektumként tárolódik a rendszer memóriában. A programozó számára kizárólag egy „leírón” (handle) keresztül érhető el. Egy csoport mindig társul egy kommunikátor objektumhoz.

A kommunikátor magába foglalja a processzek csoportját amelyek kommunikálhatnak egymással. Minden MPI üzenet számára meg kell adni egy kommunikátort. Leegyszerűsítve a kommunikátor egy extra „címke” amelyet az MPI hívásokban meg kell adni. Akárcsak a csoportok a kommunikátorok is objektumok a rendszer memóriában, és a programozó számára leírókon keresztül elérhetők. Például a leíró, amely minden folyamatot magába foglal az MPI_COMM_WORLD.

A programozó szemszögéből nézve a csoport és a kommunikátor egy és ugyanaz. A csoport függvények leginkább arra használatosak, hogy megadjuk mely processzekből hozzunk létre egy kommunikátort.

A fő céljai a kommunikátoroknak és csoportoknak:

  • A folyamatok funkció szerint bonthatók csoportokba.

  • Elérhetővé válik a kollektív kommunikáció összefüggő folyamatok egy részhalmaza közt.

  • Az alapját adja a felhasználó által megadott virtuális topológiák megvalósításának.

  • Biztonságos kommunikációt biztosít.

Programozói megfontolások és korlátozások:

  • A csoportok/kommunikátorok dinamikusak - a program futása során létrehozhatók illetve megszüntethetők.

  • Egy processz egyszerre több csoportnak/kommunikátornak is tagja lehet. Minden csoportban/kommunikátorban egyedi rangjuk lesz.

  • Az MPI több mint 40 függvényt biztosít, amelyek a csoportokhoz, kommunikátorokhoz és virtuális topológiákhoz kapcsolódnak.

  • Tipikus felhasználás:

    • Lekérdezzük a globális MPI_COMM_WORLD csoport leíróját az MPI_Comm_group függvénnyel.

    • Létrehozunk egy új csoportot a globális csoport részhalmazaként az MPI_Group_incl függvénnyel.

    • Létrehozunk egy új kommunikátort az új csoportnak az MPI_Comm_create()-el.

    • Meghatározzuk az új rangot a kommunikátorban az MPI_Comm_rank használatával.

    • Kommunikálunk bármely MPI üzenetátadó rutin segítségével.

    • Amikor végeztünk felszabadítjuk az új kommunikátort és csoportot (opcionális) az MPI_Comm_free és MPI_Group_free függvényekkel.

Az alábbi ábra szemlélteti a csoportok használatát:

Az alábbi példában létrehozunk két különböző processz csoportot, hogy szeperált kollektív kommunikációs üzeneteket válthassunk. Szükséges továbbá új kommunikátort is létrehozni.

  #include "mpi.h" 
  #include <stdio.h> 
  #define NPROCS 8 
  int main(argc,argv)  
  int argc; char *argv[]; {  
    int rank, new_rank, sendbuf, recvbuf, numtasks, ranks1[4]={0,1,2,3}, ranks2[4]={4,5,6,7};  
    MPI_Group orig_group, new_group; 
    MPI_Comm new_comm; 
    MPI_Init(&argc,&argv); 
    MPI_Comm_rank(MPI_COMM_WORLD, &rank); 
    MPI_Comm_size(MPI_COMM_WORLD, &numtasks); 
    if (numtasks != NPROCS) {  
       printf("Must specify MP_PROCS= %d. Terminating.\n",NPROCS);  
       MPI_Finalize();  
       exit(0);  
    } 
    sendbuf = rank; 
    /* Extract the original group handle */ 
    MPI_Comm_group(MPI_COMM_WORLD, &orig_group); 
    /* Divide tasks into two distinct groups based upon rank */  
    if (rank < NPROCS/2) {  
       MPI_Group_incl(orig_group, NPROCS/2, ranks1, &new_group); 
    } 
    else { 
       MPI_Group_incl(orig_group, NPROCS/2, ranks2, &new_group);  
    } 
    /* Create new new communicator and then perform collective communications */ 
    MPI_Comm_create(MPI_COMM_WORLD, new_group, &new_comm); 
    MPI_Allreduce(&sendbuf, &recvbuf, 1, MPI_INT, MPI_SUM, new_comm); 
    MPI_Group_rank (new_group, &new_rank); 
    printf("rank= %d newrank= %d recvbuf= %d\n",rank,new_rank,recvbuf); 
    MPI_Finalize();  
  } 

A mintaprogram kimenete:

  rank= 7 newrank= 3 recvbuf= 22 
  rank= 0 newrank= 0 recvbuf= 6  
  rank= 1 newrank= 1 recvbuf= 6  
  rank= 2 newrank= 2 recvbuf= 6  
  rank= 6 newrank= 2 recvbuf= 22 
  rank= 3 newrank= 3 recvbuf= 6  
  rank= 4 newrank= 0 recvbuf= 22  
  rank= 5 newrank= 1 recvbuf= 22 

4.9. Virtuális topológiák

Az MPI fogalmai szerint egy virtuális topológia egy a processzek geometriai „alakzatba” történő feltérképezése/rendezése. A két fő topológia típus, melyet az MPI támogat az a descartes-i (rács) és a gráf típus. Az MPI topológiák azonban csak virtuálisak és könnyen meglehet, hogy semmilyen kapcsolat nincs a fizikai struktúra és a párhuzamos gép processz topológiája között. Ezek a virtuális topológiák az MPI kommunikátorokra és csoportokra épülnek. Továbbá az alkalmazás fejlesztőjének kell őket „leprogramoznia”.

Miért érdemes ezeket használnunk?

Kényelmi szempontok:

  • A virtuális topológiák hasznosak lehetnek olyan alkalmazások számára, amelyek egy sajátos kommunikációs mintát használnak - olyan mintákat, amelyek illeszkednek egy MPI topológia szerkezetére.

  • Például a rács topológia kényelmes lehet egy alkalmazás számára, amely a 4 legközelebbi szomszédjával kommunikál és rács alapú adatokon dolgozik.

Kommunikációs hatékonyság:

  • Bizonyos hardver architektúrák kevésbé hatékonyan kezelhetik a kommunikációt egymástól távolabb lévő „csomópontok” esetében.

  • Egy adott megvalósítás optimalizálhatja a folyamatok feltérképezését az adott párhuzamos gép fizikai jellemzői alapján.

  • A processzek MPI virtuális topológiába való rendezése teljes egészében az MPI implementációtól függ és akár teljes mértékben figyelmen kívül is maradhat.

4.1. Példa. Példa

A folyamatok egy egyszerű virtuális rács szerkezetbe történő rendezése látható az ábrán:

Példaprogram:

Létrehozunk egy 4x4-es rács topológiát 16 processzorból és minden processz kicseréli a rangját a 4 szomszédjával.

  #include "mpi.h" 
  #include <stdio.h> 
  #define SIZE 16 
  #define UP 0 
  #define DOWN 1 
  #define LEFT 2 
  #define RIGHT 3 
  int main(argc,argv)  
  int argc;  
  char *argv[]; { 
    int numtasks, rank, source, dest, outbuf, i, tag=1, 
    inbuf[4]={MPI_PROC_NULL,MPI_PROC_NULL,MPI_PROC_NULL,MPI_PROC_NULL,},  
    nbrs[4], dims[2]={4,4}, periods[2]={0,0}, reorder=0, coords[2]; 
    MPI_Request reqs[8];  
    MPI_Status stats[8];  
    MPI_Comm cartcomm; 
    MPI_Init(&argc,&argv);  
    MPI_Comm_size(MPI_COMM_WORLD, &numtasks); 
    if (numtasks == SIZE) { 
       MPI_Cart_create(MPI_COMM_WORLD, 2, dims, periods, reorder, &cartcomm); 
       MPI_Comm_rank(cartcomm, &rank); 
       MPI_Cart_coords(cartcomm, rank, 2, coords); 
       MPI_Cart_shift(cartcomm, 0, 1, &nbrs[UP], &nbrs[DOWN]); 
       MPI_Cart_shift(cartcomm, 1, 1, &nbrs[LEFT], &nbrs[RIGHT]); 
       outbuf = rank; 
       for (i=0; i<4; i++) { 
          dest = nbrs[i];  
          source = nbrs[i]; 
          MPI_Isend(&outbuf, 1, MPI_INT, dest, tag, MPI_COMM_WORLD, &reqs[i]);  
          MPI_Irecv(&inbuf[i], 1, MPI_INT, source, tag, MPI_COMM_WORLD, &reqs[i+4]); 
       } 
       MPI_Waitall(8, reqs, stats);  
       printf("rank= %d coords= %d %d neighbors(u,d,l,r)= %d %d %d %d\n", rank,coords[0],coords[1],nbrs[UP],nbrs[DOWN],nbrs[LEFT], nbrs[RIGHT]);  
       printf("rank= %d inbuf(u,d,l,r)= %d %d %d %d\n", rank,inbuf[UP],inbuf[DOWN],inbuf[LEFT],inbuf[RIGHT]);  
    } 
    else printf("Must specify %d processors. Terminating.\n",SIZE);  
    MPI_Finalize(); 
  }  

4.10. Példaprogramok

A programok fordítása és futtatása a következőképpen történhet LINUX operációs rendszerben. A programokat (pl. munka.c-t) a

mpicc munka.c -o munka

parancssorral fordítjuk le.

A futtatás a (pl. 4 taszk létrehozásával)

mpirun btl = ^openib,udapl -np 4 munka

módon történhet. A btl = ^openib,udapl opcionális rész kikapcsolja a felesleges figyelmeztetések képernyőn történő megjelenését, míg az -np 4 opció adja a 4 taszk indítását (programnak megfelelően lehet nagyobb érték is).

A következő alszakaszokban olyan programokat tekintünk, melyek könnyebb/kényelmesebb fordításához készült egy Makefile állomány. Ennek használata a következő (pl. az mpi_array.c program esetén):

make -f Makefile.MPI.c mpi_array,

ahol a megfelelő bejegyzés alapján kerül fordításra az mpi_array.c forrás.

A Makefile állomány a következő:

  ##############################################################################  
  # FILE: Makefile.MPI.c  
  # DESCRIPTION:  
  # Makefile for all MPI C Language example codes  
  # AUTHOR: Blaise Barney  
  # LAST REVISED: 01/23/09  
  ############################################################################### 
  #Uncomment your choice of compiler  
  CC = mpicc  
  #CC = mpigcc  
  #CC = mpipathcc  
  #CC = mpipgcc 
  #Add your choice of flags  
  FLAGS = -O1 
  all: mpi_array \  
  mpi_heat2D \  
  mpi_prime \ 
  mpi_wave 
  clean: /bin/rm -rf \ 
  initial.dat \  
  final.dat \  
  mpi_array \ 
  mpi_heat2D \  
  mpi_prime \  
  mpi_wave \  
  *.o  
  mpi_array: mpi_array.c  
    $(CC) $(FLAGS) mpi_array.c -o mpi_array 
  mpi_heat2D: mpi_heat2D.c draw_heat.c  
    $(CC) $(FLAGS) mpi_heat2D.c draw_heat.c -o mpi_heat2D -lX11 
  mpi_prime: mpi_prime.c  
    $(CC) $(FLAGS) mpi_prime.c -o mpi_prime -lm 
  mpi_wave: mpi_wave.c draw_wave.c  
    $(CC) $(FLAGS) mpi_wave.c draw_wave.c -o mpi_wave -lX11 -lm 

4.10.1. MPI array (mpi_array.c)

Ez a program egy egyszerű adatfelosztást szemléltet. A fő (szülő) taszk először inicializál egy tömböt, majd egyenlő részeket oszt szét a tömbből minden más taszknak. Miután a többi taszk megkapja a saját részét a tömbből végrehajtanak egy összeadás műveletet minden tömbelemen. Továbbá nyilvántartanak egy összeget a saját tömbrészükről. A fő taszk hasonlóképpen végez a saját részével. Ahogy az összes nem fő (gyermek) taszk végez a saját tömb darabjával, a frissített részét a tömbnek visszaküldik a mesternek (szülőnek). Egy MPI kollektív kommunikációs hívás segítségével történik a darabok összegyűjtése. Végül a mester taszk kiírja a végső tömb darabjait és a globális összegét minden egyes tömbelemnek. Az MPI taszkok száma 4-el oszthatónak kell lennie.

Az mpi_array.c programforrás a következő:

  /**************************************************************************** 
  * FÁJL: mpi_array.c  
  * RÖVID LEÍRÁS:  
  * MPI Példa - Tömb Értékadás - C Verzió 
  * MEGJEGYZÉS: az MPI taszkok száma 4-el osztható kell legyen.  
  ****************************************************************************/ 
  #include "mpi.h" 
  #include <stdio.h> 
  #include <stdlib.h> 
  #define ARRAYSIZE 16000000 
  #define MASTER 0 
  float data[ARRAYSIZE]; 
  int main (int argc, char *argv[]) {  
    int numtasks, taskid, rc, dest, offset, i, j, tag1, tag2, source, chunksize;  
    float mysum, sum;  
    float update(int myoffset, int chunk, int myid);  
    MPI_Status status; 
    /***** Inicializáció *****/  
    MPI_Init(&argc, &argv); 
    MPI_Comm_size(MPI_COMM_WORLD, &numtasks);  
    if (numtasks % 4 != 0) {  
       printf("Kilépés. Az MPI taszkok számának 4-gyel oszthatónak kell lennie.\n");  
       MPI_Abort(MPI_COMM_WORLD, rc);  
       exit(0);  
    } 
    MPI_Comm_rank(MPI_COMM_WORLD,&taskid);  
    printf ("A(z) %d MPI taszk elindult...\n", taskid);  
    chunksize = (ARRAYSIZE / numtasks); 
    tag2 = 1; 
    tag1 = 2; 
    /***** Csak a mester taszk ******/  
    if (taskid == MASTER){ 
       /* A tömb inicializációja */  
       sum = 0;  
       for(i=0; i<ARRAYSIZE; i++) {  
          data[i] = i * 1.0;  
          sum = sum + data[i];  
       } 
       printf("Inicializált tömb összeg = %e\n",sum); 
       /* A tömb megfelelő részeinek elküldése a többi taszknak - a mester megtartja az első részt */  
       offset = chunksize;  
       for (dest=1; dest<numtasks; dest++) {  
          MPI_Send(&offset, 1, MPI_INT, dest, tag1, MPI_COMM_WORLD); 
          MPI_Send(&data[offset], chunksize, MPI_FLOAT, dest, tag2, MPI_COMM_WORLD); 
          printf("Sent %d elements to task %d offset= %d\n",chunksize,dest,offset); 
          offset = offset + chunksize; 
       } 
       /* A mester elvégzi a munka ráeső részét. */  
       offset = 0; 
       mysum = update(offset, chunksize, taskid); 
       /* Várakozás a többi taszk eredményének fogadására. */  
       for (i=1; i<numtasks; i++) {  
          source = i; 
          MPI_Recv(&offset, 1, MPI_INT, source, tag1, MPI_COMM_WORLD, &status);  
          MPI_Recv(&data[offset], chunksize, MPI_FLOAT, source, tag2, MPI_COMM_WORLD, &status);  
       } 
       /* A végeredmény meghatározása és a minta eredmények kiíratása. */  
       MPI_Reduce(&mysum, &sum, 1, MPI_FLOAT, MPI_SUM, MASTER, MPI_COMM_WORLD);  
       printf("Minta eredmények: \n");  
       offset = 0; 
       for (i=0; i<numtasks; i++) { 
          for (j=0; j<5; j++) printf(" %e",data[offset+j]); 
          printf("\n"); 
          offset = offset + chunksize;  
       } 
       printf("*** A végső összeg= %e ***\n",sum); 
    } /* A mester szakasz vége. */ 
    /***** Csak a nem-mester taszkok *****/ 
    if (taskid > MASTER) { 
       /* A saját tömbrész fogadása a mester taszktól. */  
       source = MASTER; 
       MPI_Recv(&offset, 1, MPI_INT, source, tag1, MPI_COMM_WORLD, &status); 
       MPI_Recv(&data[offset], chunksize, MPI_FLOAT, source, tag2, MPI_COMM_WORLD, &status); 
       mysum = update(offset, chunksize, taskid); 
       /* Az eredmény visszaküldése a mester taszknak. */  
       dest = MASTER; 
       MPI_Send(&offset, 1, MPI_INT, dest, tag1, MPI_COMM_WORLD); 
       MPI_Send(&data[offset], chunksize, MPI_FLOAT, MASTER, tag2, MPI_COMM_WORLD); 
       MPI_Reduce(&mysum, &sum, 1, MPI_FLOAT, MPI_SUM, MASTER, MPI_COMM_WORLD); 
    } /* A nem-mester szakasz vége. */ 
    MPI_Finalize(); 
  } /* A main vége. */ 
  float update(int myoffset, int chunk, int myid) {  
    int i;  
    float mysum; /* A saját tömbelemekre vonatkozó összeadás elvégzése és a saját összeg megtartása. */ 
    mysum = 0; 
    for(i=myoffset; i < myoffset + chunk; i++) { 
       data[i] = data[i] + i * 1.0; 
  mysum = mysum + data[i];  
    } 
    printf("A(z) %d taszk saját összeg = %e\n",myid,mysum);  
    return(mysum);  
  } 

A program eredményeképpen kapjuk a következő kimenetet:

4.10.2. Hővezetés egyenlete (mpi_heat2D.c)

Ez a példa egy egyszerűsített két-dimenziós hővezetési egyenlet tartomány felosztásán alapszik. A kezdeti hőmérséklet a tartomány közepén magas, a peremeken pedig nulla. A peremeken a nulla érték a szimuláció teljes ideje alatt meg van tartva. Az időben történő lépések során két tartományt tartalmazó tömb kerül felhasználásra, ahol a tartományok a régi és az új adatok között alternálnak. Ebben a párhuzamosított változatban a rácsot a mester folyamat osztja fel és osztja szét a sorok szerint dolgozó processzeknek. Minden egyes időbeni osztáspontban a dolgozó folyamat a határponti adatokat kicseréli a szomszédaival, mivel egy rácspont aktuális hőmérséklete az előző időpontbeli hőmérséklettől, valamint a szomszédok pontjainak adataitól is függ (lásd még a 2.7. szakaszt). Az összes időbeli osztáspont végeztével a dolgozó visszaküldi az eredményeit a mesternek.

Két adatállomány az eredmény: egy kezdeti adathalmaz és a végső adathalmaz, melyeket a draw_heat.c program jelenít meg a mpi_heat2D.c hívása által.

Az mpi_heat2D.c forrása a következő:

  /****************************************************************************  
  * FÁJL: mpi_heat2D.c  
  * EGYÉB FÁJLOK: draw_heat.c  
  * LEÍRÁS: * HEAT2D Példa - Párhuzamosított C Verzió  
  ****************************************************************************/  
  #include "mpi.h" 
  #include <stdio.h> 
  #include <stdlib.h> 
  extern void draw_heat(int nx, int ny); /* X eljárás a gráf létrehozásához */ 
  #define NXPROB 20 /* a rács x tengelye */  
  #define NYPROB 20 /* a rács y tengelye */  
  #define STEPS 100 /* az időlépések száma */  
  #define MAXWORKER 8 /* a dolgozó taszkok maximális száma */  
  #define MINWORKER 3 /* a dolgozó taszkok minimális száma */  
  #define BEGIN 1 /* üzenet címke */  
  #define LTAG 2 /* üzenet címke */  
  #define RTAG 3 /* üzenet címke */  
  #define NONE 0 /* a nincs szomszéd jelzése */  
  #define DONE 4 /* üzenet címke */ 
  #define MASTER 0 /* az első processz taszk azonosítója */ 
  struct Parms {  
    float cx; 
    float cy;  
  } parms = {0.1, 0.1}; 
  int main (int argc, char *argv[]) {  
    void inidat(), prtdat(), update();  
    float u[2][NXPROB][NYPROB]; /* tömb a rácshoz */  
    int taskid, /* a saját taszk ID */ 
    numworkers, /* a dolgozó processzek száma */  
    numtasks, /* a taszkok száma */ 
    averow,rows,offset,extra, /* az adatsorok küldéséhez */  
    dest, source, /* az üzenet küldéshez-fogadáshoz */  
    left,right, /* a szomszéd taszkok */  
    msgtype, /* az üzenet típusokhoz */  
    rc,start,end, /* vegyes */  
    i,ix,iy,iz,it; /* ciklus változók */  
    MPI_Status status; 
    /* Először határozzuk meg a saját taszk ID-t és mennyi taszk fut. */  
    MPI_Init(&argc,&argv); 
    MPI_Comm_size(MPI_COMM_WORLD,&numtasks); 
    MPI_Comm_rank(MPI_COMM_WORLD,&taskid); 
    numworkers = numtasks-1; 
    if (taskid == MASTER) {  
       /************************* a mester kód *******************************/  
       /* A numworkers értékének ellenőrzése - kilépés, ha nincs benne az intervallumban. */  
       if ((numworkers > MAXWORKER) || (numworkers < MINWORKER)) {  
          printf("Hiba: a taszkok számának %d és %d között kell lennie.\n", MINWORKER+1,MAXWORKER+1);  
          printf("Kilépés...\n");  
          MPI_Abort(MPI_COMM_WORLD, rc);  
          exit(1);  
       }  
       printf ("Indítása az mpi_heat2D-nek %d dolgozó taszkkal.\n", numworkers); 
       /* A rács inicializálása. */  
       printf("A rács mérete: X= %d Y= %d Időlépések = %d\n",NXPROB,NYPROB,STEPS);  
       printf("A rács inicializációja és a az initial.dat állomány írása...\n");  
       inidat(NXPROB, NYPROB, u); 
       prtdat(NXPROB, NYPROB, u, "initial.dat"); 
       /* A munka elosztása a dolgozók között. Először meghatározni, hogy mennyi sort kell elküldeni és mit kell tenni az extra sorokkal. */  
       averow = NXPROB/numworkers; 
       extra = NXPROB%numworkers; 
       offset = 0; 
       for (i=1; i<=numworkers; i++) {  
          rows = (i <= extra) ? averow+1 : averow;  
          /* Minden egyes dolgozónak be kell mutatni a szomszédait, mivel adatokat kell cserélniük egymást között. */  
          if (i == 1) left = NONE;  
          else left = i - 1;  
          if (i == numworkers) right = NONE; 
          else right = i + 1;  
          /* Az indulási információ elküldése az összes dolgozónak. */  
          dest = i;  
          MPI_Send(&offset, 1, MPI_INT, dest, BEGIN, MPI_COMM_WORLD);  
          MPI_Send(&rows, 1, MPI_INT, dest, BEGIN, MPI_COMM_WORLD);  
          MPI_Send(&left, 1, MPI_INT, dest, BEGIN, MPI_COMM_WORLD);  
          MPI_Send(&right, 1, MPI_INT, dest, BEGIN, MPI_COMM_WORLD);  
          MPI_Send(&u[0][offset][0], rows*NYPROB, MPI_FLOAT, dest, BEGIN, MPI_COMM_WORLD);  
          printf("Sent to task %d: rows= %d offset= %d ",dest,rows,offset); 
          printf("left= %d right= %d\n",left,right);  
          offset = offset + rows; 
       }  
       /* Várakozás az összes dolgozótól érkező eredményre. */ 
       for (i=1; i<=numworkers; i++) {  
          source = i;  
          msgtype = DONE; 
          MPI_Recv(&offset, 1, MPI_INT, source, msgtype, MPI_COMM_WORLD, &status); 
          MPI_Recv(&rows, 1, MPI_INT, source, msgtype, MPI_COMM_WORLD, &status); 
          MPI_Recv(&u[0][offset][0], rows*NYPROB, MPI_FLOAT, source, msgtype, MPI_COMM_WORLD, &status);  
       } 
       /* A végeredmény kiíratása, az X gráf meghívása és az MPI lezárása. */  
       printf("A final.dat állomány kiíratása és a gráf generálása ...\n");  
       prtdat(NXPROB, NYPROB, &u[0][0][0], "final.dat"); 
       printf("A MORE gomb megnyomásával látható a kezdeti/végső állapot.\n"); 
       printf("Az EXIT gomb megnyomásával kilépünk a programból.\n"); 
       draw_heat(NXPROB,NYPROB); 
       MPI_Finalize();  
    } /* A mester kód vége. */ 
    /************************* a dolgozók kódja **********************************/  
    if (taskid != MASTER) {  
       /* Minden inicializációja - beleértve a peremeket is - zérusra. */  
       for (iz=0; iz<2; iz++)  
          for (ix=0; ix<NXPROB; ix++) 
             for (iy=0; iy<NYPROB; iy++)  
                u[iz][ix][iy] = 0.0; 
       /* A saját offset, sor, szomszédok és rácsrész fogadása a mestertől. */  
       source = MASTER; 
       msgtype = BEGIN; 
       MPI_Recv(&offset, 1, MPI_INT, source, msgtype, MPI_COMM_WORLD, &status); 
       MPI_Recv(&rows, 1, MPI_INT, source, msgtype, MPI_COMM_WORLD, &status); 
       MPI_Recv(&left, 1, MPI_INT, source, msgtype, MPI_COMM_WORLD, &status); 
       MPI_Recv(&right, 1, MPI_INT, source, msgtype, MPI_COMM_WORLD, &status); 
       MPI_Recv(&u[0][offset][0], rows*NYPROB, MPI_FLOAT, source, msgtype, MPI_COMM_WORLD, &status); 
       /* A peremelemek meghatározása. Az első és utolsó oszlopok szükségesek. Nyilvánvalóan a 0. sor nem tud cserélni a 0-1. sorral. Hasonlóan az utolsó sor nem tud cserélni az utolsó+1. sorral */  
       start=offset; 
       end=offset+rows-1; 
       if (offset==0) start=1;  
       if ((offset+rows)==NXPROB) end--;  
       printf("taszk=%d start=%d end=%d\n",taskid,start,end); 
       /* A STEPS iteráció kezdése. A perem soroknak kommunikálni kell a szomszédokkal. Ha az első vagy az utolsó rácssort kapjuk, akkor csak egy szomszéddal kell kommunikáni. */  
       printf("A(z) %d taszk fogadta a munkát. Az időlépések elkezdése...\n",taskid);  
       iz = 0; 
       for (it = 1; it <= STEPS; it++) { 
          if (left != NONE) {  
             MPI_Send(&u[iz][offset][0], NYPROB, MPI_FLOAT, left, RTAG, MPI_COMM_WORLD); 
             source = left;  
             msgtype = LTAG; 
             MPI_Recv(&u[iz][offset-1][0], NYPROB, MPI_FLOAT, source, msgtype, MPI_COMM_WORLD, &status);  
          } 
          if (right != NONE) {  
             MPI_Send(&u[iz][offset+rows-1][0], NYPROB, MPI_FLOAT, right, LTAG, MPI_COMM_WORLD); 
             source = right; 
             msgtype = RTAG; 
             MPI_Recv(&u[iz][offset+rows][0], NYPROB, MPI_FLOAT, source, msgtype, MPI_COMM_WORLD, &status);  
          }  
          /* Az update meghívása, hogy frissítsük a rácspontok értékeit. */  
          update(start,end,NYPROB,&u[iz][0][0],&u[1-iz][0][0]);  
          iz = 1 - iz;  
       } 
       /* Végül a saját végeredmény részek visszaküldése a mesternek. */  
       MPI_Send(&offset, 1, MPI_INT, MASTER, DONE, MPI_COMM_WORLD);  
       MPI_Send(&rows, 1, MPI_INT, MASTER, DONE, MPI_COMM_WORLD); 
       MPI_Send(&u[iz][offset][0], rows*NYPROB, MPI_FLOAT, MASTER, DONE, MPI_COMM_WORLD);  
       MPI_Finalize(); 
    } 
  } 
  /**************************************************************************  
  * alprogram update  
  ****************************************************************************/  
  void update(int start, int end, int ny, float *u1, float *u2) { 
    int ix, iy;  
    for (ix = start; ix <= end; ix++)  
       for (iy = 1; iy <= ny-2; iy++) *(u2+ix*ny+iy) = *(u1+ix*ny+iy) + parms.cx * (*(u1+(ix+1)*ny+iy) + *(u1+(ix-1)*ny+iy) - 2.0 * *(u1+ix*ny+iy)) + parms.cy * (*(u1+ix*ny+iy+1) + *(u1+ix*ny+iy-1) - 2.0 * *(u1+ix*ny+iy));  
  } 
  /***************************************************************************** 
  * alprogram inidat  
  *****************************************************************************/  
  void inidat(int nx, int ny, float *u) {  
    int ix, iy; 
    for (ix = 0; ix <= nx-1; ix++)  
       for (iy = 0; iy <= ny-1; iy++) *(u+ix*ny+iy) = (float)(ix * (nx - ix - 1) * iy * (ny - iy - 1));  
  } 
  /**************************************************************************  
  * alprogram prtdat 
  **************************************************************************/  
  void prtdat(int nx, int ny, float *u1, char *fnam) {  
    int ix, iy;  
    FILE *fp; 
    fp = fopen(fnam, "w"); 
    for (iy = ny-1; iy >= 0; iy--) {  
       for (ix = 0; ix <= nx-1; ix++) {  
          fprintf(fp, "%6.1f", *(u1+ix*ny+iy));  
          if (ix != nx-1) fprintf(fp, " ");  
          else fprintf(fp, "\n");  
       }  
    } 
    fclose(fp);  
  } 

Az draw_heat.c forrása a következő:

  /***************************************************************************  
  * FÁJL: draw_heat.c  
  * LEÍRÁS: 
  * A heat2D példaállományokból hívják. Ez a kód nincs dokumentálva. Egyszerűen egy grafikus interfészt ad a heat2D példa kódjainak.  
  ***************************************************************************/ 
  #include <stdio.h> 
  #include <stdlib.h> 
  #include <string.h> 
  #include <X11/Xlib.h> 
  #include <X11/Xutil.h> 
  #define HEIGHT 750  
  #define WIDTH 750  
  #define NCOLORS 255 
  typedef struct {  
    Window window;  
    XSizeHints hints;  
    XColor backcolor; 
    XColor bordcolor; 
    int bordwidth; 
  } MYWINDOW; 
  char title[] = {"draw_heat"}, moreword[] = {"More"}, exitword[] = {"Exit"}, text[10]; 
  void draw_heat(int nx, int ny) { 
    float scale, point, high, *a1, *a2, coloratio = 65535.0 / 255.0;  
    int i,j,k,x,y,done, myscreen, rectheight, rectwidth,toggle;  
    FILE *initial, *final; 
    MYWINDOW base,more,quit;  
    Font font,font2; 
    GC itemgc,textgc,rectgc[NCOLORS],linegc; 
    XColor rectcolor,red,yellow,blue,green,black,white;  
    XEvent myevent;  
    Colormap cmap;  
    KeySym mykey;  
    Display *mydisp; 
    /* Az rgb értékek beállítása a színekhez. */  
    yellow.red= (int) (255 * coloratio);  
    yellow.green= (int) (255 * coloratio); 
    yellow.blue= (int) (0 * coloratio); 
    green.red= (int) (0 * coloratio);  
    green.green= (int) (255 * coloratio);  
    green.blue= (int) (0 * coloratio); 
    black.red= (int) (0 * coloratio);  
    black.green= (int) (0 * coloratio);  
    black.blue= (int) (0 * coloratio); 
    white.red= (int) (255 * coloratio);  
    white.green= (int) (255 * coloratio);  
    white.blue= (int) (255 * coloratio); 
    mydisp = XOpenDisplay("");  
    if (!mydisp) {  
    fprintf (stderr, "Hé! Vagy nem megy az X vagy valami más baj van.\n");  
    fprintf (stderr, "Nem lehet megmutatni a gráfot.\n");  
    exit(1);  
    }  
    myscreen = DefaultScreen(mydisp);  
    cmap = DefaultColormap (mydisp, myscreen);  
    XAllocColor (mydisp, cmap, &yellow);  
    XAllocColor (mydisp, cmap, &black);  
    XAllocColor (mydisp, cmap, &green);  
    XAllocColor (mydisp, cmap, &white);  
    /* az ablak pozíciójának és méretének inicializálása */  
    base.hints.x = 50; 
    base.hints.y = 50;  
    base.hints.width = WIDTH;  
    base.hints.height = HEIGHT; 
    base.hints.flags = PPosition | PSize;  
    base.bordwidth = 5; 
    /* ablak létrehozása */ 
    base.window = XCreateSimpleWindow (mydisp, DefaultRootWindow (mydisp), base.hints.x, base.hints.y, base.hints.width, base.hints.height, base.bordwidth, black.pixel, black.pixel);  
    XSetStandardProperties (mydisp, base.window, title, title, None, NULL, 0, &base.hints); 
    /* kilépés az ablak pozícióból és méretből */  
    quit.hints.x = 5;  
    quit.hints.y = HEIGHT-75; 
    quit.hints.width = 70; 
    quit.hints.height = 30; 
    quit.hints.flags = PPosition | PSize;  
    quit.bordwidth = 5; 
    quit.window = XCreateSimpleWindow (mydisp, base.window, quit.hints.x, quit.hints.y, quit.hints.width, quit.hints.height, quit.bordwidth, green.pixel, yellow.pixel);  
    XSetStandardProperties (mydisp, quit.window, exitword, exitword, None, NULL, 0, &quit.hints);  
    /* további ablak pozíció és méret */  
    more.hints.x = WIDTH-85;  
    more.hints.y = HEIGHT-75;  
    more.hints.width = 70;  
    more.hints.height = 30; 
    more.hints.flags = PPosition | PSize;  
    more.bordwidth = 5; 
    more.window = XCreateSimpleWindow (mydisp, base.window, more.hints.x, more.hints.y, more.hints.width, more.hints.height, more.bordwidth, green.pixel, yellow.pixel);  
    XSetStandardProperties (mydisp, more.window, moreword, moreword, None, NULL, 0, &more.hints); 
    /* A fontok betöltése */  
    font = XLoadFont (mydisp, "fixed");  
    font2 = XLoadFont (mydisp, "fixed"); 
    /* GC létrehozása és inicializációja */  
    textgc = XCreateGC (mydisp, base.window, 0,0); 
    XSetFont (mydisp, textgc, font); 
    XSetForeground (mydisp, textgc, green.pixel); 
    linegc = XCreateGC (mydisp, base.window, 0,0); 
    XSetForeground (mydisp, linegc, white.pixel); 
    itemgc = XCreateGC (mydisp, quit.window, 0,0); 
    XSetFont (mydisp, itemgc, font2); 
    XSetForeground (mydisp, itemgc, black.pixel); 
    /* a vörös színskála árnyékának létrehozása */  
    for (k=0;k<NCOLORS;k++) {  
       rectgc[k] = XCreateGC (mydisp, base.window, 0,0); 
       rectcolor.red= (int) (k * coloratio);  
       rectcolor.green= (int) (0 * coloratio); 
       rectcolor.blue= (int) (0 * coloratio);  
       XAllocColor (mydisp, cmap, &rectcolor);  
       XSetForeground (mydisp, rectgc[k], rectcolor.pixel);  
    } 
    /* A program eseményvezérelt, az XSelectInput híváshalmazok, melyek megszakítások minden egyes ablaknak. */  
    XSelectInput (mydisp, base.window, ButtonPressMask | KeyPressMask | ExposureMask);  
    XSelectInput (mydisp, quit.window, ButtonPressMask | KeyPressMask | ExposureMask); XSelectInput (mydisp, more.window, ButtonPressMask | KeyPressMask | ExposureMask); 
    /* Ablak leképezése - megengedi az ablak megjelenését. */  
    XMapRaised (mydisp, base.window);  
    XMapSubwindows (mydisp, base.window); 
    /* a beolvasni kívánt adathoz szükséges terület létrehozása */  
    a1 = (float *) malloc(nx*ny*sizeof(float));  
    a2 = (float *) malloc(nx*ny*sizeof(float)); 
    /* A heat2D futása során létrehozott két adatállomány beolvasása. A legnagyobb értékbeolvasást is meghatározza. */ 
    high = 0.0;  
    initial = fopen("initial.dat","r");  
    for (k=0;k<nx*ny;k++) {  
       fscanf(initial,"%f",a1+k);  
       if ( *(a1+k) > high) high = *(a1+k);  
    }  
    fclose(initial); 
    final = fopen("final.dat","r");  
    for (k=0;k<nx*ny;k++) fscanf(final,"%f",a2+k); 
    fclose(final); 
    /* A skála téglalap mérete és színe. */  
    rectwidth = WIDTH/ny;  
    rectheight = HEIGHT/nx; 
    scale = ((float)NCOLORS) / high; 
    /* A fő esemény ciklus - kilépés, ha a felhasználó megnyomja az "exit" gombot. */  
    done = 0;  
    toggle = 0;  
    while (! done) {  
       XNextEvent (mydisp, &myevent);  
       /* Olvassa a következő eseményt. */  
       switch (myevent.type) {  
       case Expose:  
          if (myevent.xexpose.count == 0) {  
             if (myevent.xexpose.window == base.window) {  
                for (x=0;x<nx;x++) {  
                   for (y=0;y<ny;y++) {  
                      if (toggle == 0) point = scale * *(a1+x*ny+y);  
                      else point = scale * *(a2+x*ny+y);  
                      k = (int) point;  
                      if (k >= NCOLORS) k = NCOLORS-1;  
                      XFillRectangle (mydisp, base.window, rectgc[k], y*rectwidth, x*rectheight, rectwidth, rectheight);  
                   }  
                }  
                for (x=0;x<nx;x++) XDrawLine (mydisp, base.window, linegc, 1,x*rectheight,WIDTH, x*rectheight);  
                for (y=0;y<ny;y++) XDrawLine (mydisp, base.window, linegc, y*rectwidth,1, y*rectwidth,HEIGHT);  
                XDrawString (mydisp, base.window, textgc,325,30,"Heat2D",6);  
                if (toggle == 0) XDrawString (mydisp, base.window, textgc,325,735,"INITIAL",7);  
                else XDrawString (mydisp,base.window,textgc,330,735,"FINAL",5);  
             } 
  else if (myevent.xexpose.window == quit.window) XDrawString (mydisp, quit.window, itemgc, 12,20, exitword, strlen(exitword)); 
  else if (myevent.xexpose.window == more.window) XDrawString (mydisp, more.window, itemgc, 12,20, moreword, strlen(moreword));  
          } /* az Expose eset */  
          break; 
       case ButtonPress:  
          if (myevent.xbutton.window == quit.window) done = 1;  
          else if (myevent.xbutton.window == more.window) {  
             if (toggle==0) toggle = 1;  
             else if (toggle==1) toggle = 0;  
                for (x=0;x<nx;x++) {  
                   for (y=0;y<ny;y++) { 
                      if (toggle==0) point = scale * *(a1+x*ny+y); 
                      else point = scale * *(a2+x*ny+y);  
                      k = (int) point;  
                      if (k >= NCOLORS) k = NCOLORS-1;  
                      XFillRectangle (mydisp, base.window, rectgc[k], y*rectwidth, x*rectheight, rectwidth, rectheight);  
                   } 
                } 
                for (x=0;x<nx;x++) XDrawLine (mydisp, base.window, linegc, 1,x*rectheight,WIDTH, x*rectheight); 
                for (y=0;y<ny;y++) XDrawLine (mydisp, base.window, linegc, y*rectwidth,1, y*rectwidth,HEIGHT);  
                XDrawString (mydisp, base.window, textgc,325,30,"heat2D",6);  
                if (toggle==0) XDrawString (mydisp, base.window, textgc,325,735,"INITIAL",7);  
                else XDrawString (mydisp,base.window,textgc,330,735,"FINAL",5);  
          } 
          <break; 
       case MappingNotify:  
          break; 
       } 
    }  
    XDestroyWindow (mydisp, base.window);  
    XCloseDisplay (mydisp);  
  } 

A program futása során (a X gráf megjelenítése előtt) a következőket kapjuk:

A program futása során elkészülő ablak megjelenését a következő animáció mutatja be: mpi.heat2D.swf

4.10.3. Hullámegyenlet (mpi_wave.c)

Ez a program megvalósít egy egyidejű hullám egyenletet, amely a Fox: Problémák megoldása párhuzamos processzorokon (1988) (eredeti címe: Solving Problems on Concurrent Processors, ) című könyvében (első kötet, 5. fejezet) található. Egy rezgő húrt pontokká bontunk fel. Minden egyes processzor bizonyos számú pont amplitúdójának frissítéséért felelős. Minden egyes iterációban az egyes processzorok a határpontokat a legközelebbi szomszédaival kicserélik. Ez a változat alacsony szintű küldés és fogadás műveleteket használ a határpontok kicserélésekor. A program elkészít az eredmények alapján egy gráfot is (lásd később a draw_wave.c programot is).

A mpi_wave.c program forrása a következő:

  /*************************************************************************** 
  * FÁJL: mpi_wave.c  
  * EGYÉB FÁJLOK: draw_wave.c  
  * RÖVID LEÍRÁS:  
  * MPI Egyidejű Hullám Egyenlet - C Verzió  
  * Pont-Pont Kommunikációs Példa  
  ***************************************************************************/  
  #include "mpi.h" 
  #include <stdio.h> 
  #include <stdlib.h> 
  #include <math.h> 
  #define MASTER 0  
  #define TPOINTS 800  
  #define MAXSTEPS 10000  
  #define PI 3.14159265 
  int RtoL = 10;  
  int LtoR = 20; 
  int OUT1 = 30; 
  int OUT2 = 40; 
  void init_master(void); 
  void init_workers(void); 
  void init_line(void); 
  void update (int left, int right); 
  void output_master(void); 
  void output_workers(void); 
  extern void draw_wave(double *); 
  int taskid, /* taszk ID */  
  numtasks, /* a processzek száma */  
  nsteps, /* az időlépések száma */  
  npoints, /* ezen processzor által kezelt pontok száma */  
  first; /* az első pont indexe, melyet ez a processzor kezel */  
  double etime, /* az eltelt idő másodpercekben */  
  values[TPOINTS+2], /* az értékek a t időpillanatban */  
  oldval[TPOINTS+2], /* az értékek a (t-dt) időpillanatban */  
  newval[TPOINTS+2]; /* az értékek a (t+dt) időpillanatban */ 
  /* ------------------------------------------------------------------------  
  * A mester megkapja az időlépések számát a felhasználótól bemeneti értékként és szétküldi azt.  
  * ------------------------------------------------------------------------ */  
  void init_master(void) {  
    char tchar[8]; 
    /* Az időlépések számának beállítása, kiíratása és szétküldése. */  
    nsteps = 0;  
    while ((nsteps < 1) || (nsteps > MAXSTEPS)) {  
       printf("Adja meg a időlépések számát (1-%d): \n",MAXSTEPS);  
       scanf("%s", tchar);  
       nsteps = atoi(tchar); 
       if ((nsteps < 1) || (nsteps > MAXSTEPS)) printf("Enter value between 1 and %d\n", MAXSTEPS);  
    } 
    MPI_Bcast(&nsteps, 1, MPI_INT, MASTER, MPI_COMM_WORLD);  
  } 
  /* -------------------------------------------------------------------------  
  * A dolgozók fogadják az időlépések számát bemeneti értékként a mestertől. 
  * -------------------------------------------------------------------------*/  
  void init_workers(void) {  
    MPI_Bcast(&nsteps, 1, MPI_INT, MASTER, MPI_COMM_WORLD); 
  } 
  /* ------------------------------------------------------------------------  
  * Minden processz inicializálja a pontokat az egyesen.  
  * --------------------------------------------------------------------- */  
  void init_line(void) {  
    int nmin, nleft, npts, i, j, k; double x, fac; 
    /* A szinusz görbén levő kezdeti értékek kiszámítása. */  
    nmin = TPOINTS/numtasks;  
    nleft = TPOINTS%numtasks; 
    fac = 2.0 * PI; 
    for (i = 0, k = 0; i < numtasks; i++) {  
       npts = (i < nleft) ? nmin + 1 : nmin; 
       if (taskid == i) {  
          first = k + 1; 
          npoints = npts; 
          printf ("task=%3d first point=%5d npoints=%4d\n", taskid, first, npts);  
          for (j = 1; j <= npts; j++, k++) {  
             x = (double)k/(double)(TPOINTS - 1); 
             values[j] = sin (fac * x);  
          } 
       }  
       else k += npts;  
    }  
    for (i = 1; i <= npoints; i++) oldval[i] = values[i];  
  } 
  /* -------------------------------------------------------------------------  
  * Minden processz megfelelő időpontokban frissíti a pontjait.  
  * -------------------------------------------------------------------------*/  
  void update(int left, int right) {  
    int i, j;  
    double dtime, c, dx, tau, sqtau; 
    MPI_Status status; 
    dtime = 0.3;  
    c = 1.0; 
    dx = 1.0;  
    tau = (c * dtime / dx); 
    sqtau = tau * tau; 
    /* Az értékek frissítése minden pontnál a húr mentén. */  
    for (i = 1; i <= nsteps; i++) {  
       /* Adatcsere a bal oldali szomszéddal. */  
       if (first != 1) {  
          MPI_Send(&values[1], 1, MPI_DOUBLE, left, RtoL, MPI_COMM_WORLD);  
          MPI_Recv(&values[0], 1, MPI_DOUBLE, left, LtoR, MPI_COMM_WORLD, &status);  
       } 
       /* Adatcsere a jobb oldali szomszéddal. */  
       if (first + npoints -1 != TPOINTS) {  
          MPI_Send(&values[npoints], 1, MPI_DOUBLE, right, LtoR, MPI_COMM_WORLD); 
          MPI_Recv(&values[npoints+1], 1, MPI_DOUBLE, right, RtoL, MPI_COMM_WORLD, &status);  
       } 
       /* A pontok frissítése az egyenes mentén. */  
       for (j = 1; j <= npoints; j++) {  
          /* Globális végpontok. */  
          if ((first + j - 1 == 1) || (first + j - 1 == TPOINTS)) newval[j] = 0.0;  
          else  
             /* Használjuk a hullámegyenletet a pontok frissítéséhez. */  
             newval[j] = (2.0 * values[j]) - oldval[j] + (sqtau * (values[j-1] - (2.0 * values[j]) + values[j+1]));  
          } 
          for (j = 1; j <= npoints; j++) { 
             oldval[j] = values[j];  
             values[j] = newval[j];  
          }  
    }  
  } 
  /* ------------------------------------------------------------------------  
  * A mester fogadja az eredményeket a dolgozóktól és kiíratja azokat.  
  * ------------------------------------------------------------------------ */  
  void output_master(void) {  
    int i, j, source, start, npts, buffer[2];  
    double results[TPOINTS]; 
    MPI_Status status;  
    /* A dolgozók eredményeinek tárolása egy eredmény tömbben. */  
    for (i = 1; i < numtasks; i++) {  
       /* Fogadása az első pontnak, a pontok számának és az eredménynek. */ 
       MPI_Recv(buffer, 2, MPI_INT, i, OUT1, MPI_COMM_WORLD, &status); 
       start = buffer[0]; 
       npts = buffer[1]; 
       MPI_Recv(&results[start-1], npts, MPI_DOUBLE, i, OUT2, MPI_COMM_WORLD, &status);  
    } 
    /* A mester eredményeinek tárolása az eredmény tömbben. */  
    for (i = first; i < first + npoints; i++) results[i-1] = values[i]; 
    j = 0; printf("***************************************************************\n");  
    printf("A végső amplitúdó értékek az összes pontra %d lépés után:\n",nsteps);  
    for (i = 0; i < TPOINTS; i++) { 
       printf("%6.2f ", results[i]);  
       j = j++;  
       if (j == 10) {  
          printf("\n");  
          j = 0;  
       }  
    } 
    printf("***************************************************************\n");  
    printf("\nA gráf rajzolása...\n");  
    printf("Az EXIT gomb megnyomásával vagy a CTRL-C-vel léphet ki.\n"); 
    /* Az eredmények megjelenítése a draw_wave eljárással */  
    draw_wave(&results[0]);  
  } 
  /* -------------------------------------------------------------------------  
  * A dolgozók elküldik a frissített értékeket a mesternek.  
  * -------------------------------------------------------------------------*/  
  void output_workers(void) {  
    int buffer[2];  
    MPI_Status status; 
    /* Küldése az első pontnak, a pontok számának és az eredménynek a mesterhez. */  
    buffer[0] = first;  
    buffer[1] = npoints; 
    MPI_Send(&buffer, 2, MPI_INT, MASTER, OUT1, MPI_COMM_WORLD);  
    MPI_Send(&values[1], npoints, MPI_DOUBLE, MASTER, OUT2, MPI_COMM_WORLD);  
  } 
  /* ------------------------------------------------------------------------  
  * Fő program  
  * ------------------------------------------------------------------------ */ 
  int main (int argc, char *argv[]) {  
    int left, right, rc; 
    /* Az MPI inicializálása. */ 
    MPI_Init(&argc,&argv); 
    MPI_Comm_rank(MPI_COMM_WORLD,&taskid); 
    MPI_Comm_size(MPI_COMM_WORLD,&numtasks); 
    if (numtasks < 2) {  
       printf("Hiba: az MPI taszkok száma %d\n",numtasks);  
       printf("Szükség van legalább 2 taszkra! Kilépés...\n");  
       MPI_Abort(MPI_COMM_WORLD, rc);  
       exit(0);  
    } 
    /* A bal és jobb oldali szomszédok meghatározása. */  
    if (taskid == numtasks-1) right = 0;  
    else right = taskid + 1; 
    if (taskid == 0) left = numtasks - 1;  
    else left = taskid - 1; 
    /* A program paraméterek elérése és a hullám értékek inicializációja. */  
    if (taskid == MASTER) {  
       printf ("Az mpi_wave indítása felhasználva %d taszkot.\n", numtasks);  
       printf ("Használjunk %d pontot a vibráló húron.\n", TPOINTS);  
       init_master();  
    } 
    else init_workers(); 
    init_line(); 
    /* Az értékek frissítése az egyenes mentén nstep időlépés esetén. */  
    update(left, right); 
    /* A mester összegyűjti az eredményeket a dolgozóktól és kiíratja. */  
    if (taskid == MASTER) output_master();  
    else output_workers(); 
    MPI_Finalize();  
    return 0;  
  } 

A draw_wave.c program forrása:

  /***************************************************************************  
  * FÁJL: draw_wave.c  
  * LEÍRÁS:  
  * Az mpi_wave függvényei hívják meg, hogy az eredményről egy grafikont rajzoljon.  
  ***************************************************************************/  
  #include <stdio.h> 
  #include <stdlib.h> 
  #include <string.h> 
  #include <X11/Xlib.h> 
  #include <X11/Xutil.h> 
  #define HEIGHT 500 
  #define WIDTH 1000 
  typedef struct {  
    Window window;  
    XSizeHints hints;  
    XColor backcolor;  
    XColor bordcolor;  
    int bordwidth; 
  } MYWINDOW; 
  char baseword[] = {"draw_wave"}, exitword[] = {"Exit"}, text[10]; 
  void draw_wave(double * results) { 
    float scale, point, coloratio = 65535.0 / 255.0; 
    int i,j,k,y, zeroaxis, done, myscreen, points[WIDTH]; 
    MYWINDOW base, quit;  
    Font font,font2;  
    GC itemgc,textgc,pointgc,linegc;  
    XColor red,yellow,blue,green,black,white;  
    XEvent myevent;  
    Colormap cmap;  
    KeySym mykey;  
    Display *mydisp; 
    /* Az rgb értékek beállítása a színekhez. */  
    red.red= (int) (255 * coloratio);  
    red.green= (int) (0 * coloratio);  
    red.blue = (int) (0 * coloratio); 
    yellow.red= (int) (255 * coloratio);  
    yellow.green= (int) (255 * coloratio);  
    yellow.blue= (int) (0 * coloratio); 
    blue.red= (int) (0 * coloratio);  
    blue.green= (int) (0 * coloratio);  
    blue.blue= (int) (255 * coloratio); 
    green.red= (int) (0 * coloratio);  
    green.green= (int) (255 * coloratio); 
    green.blue= (int) (0 * coloratio); 
    black.red= (int) (0 * coloratio);  
    black.green= (int) (0 * coloratio);  
    black.blue= (int) (0 * coloratio); 
    white.red= (int) (255 * coloratio);  
    white.green= (int) (255 * coloratio) ; 
    white.blue= (int) (255 * coloratio); 
    mydisp = XOpenDisplay("");  
    if (!mydisp) {  
       fprintf (stderr, "Hé, vagy nem megy az X vagy más probléma van.\n");  
       fprintf (stderr, "Nem lehet megjeleníteni a gráfot.\n");  
       exit(1);  
    }  
    myscreen = DefaultScreen(mydisp);  
    cmap = DefaultColormap (mydisp, myscreen);  
    XAllocColor (mydisp, cmap, &red);  
    XAllocColor (mydisp, cmap, &yellow);  
    XAllocColor (mydisp, cmap, &blue);  
    XAllocColor (mydisp, cmap, &black);  
    XAllocColor (mydisp, cmap, &green);  
    XAllocColor (mydisp, cmap, &white);  
    /* Beállítás az ablak létrehozásához. */  
    /* XCreateSimpleWindow sok attribútum esetén az alapértelmezett értékeket használja, ezáltal egyszerűsítve a programozó munkáját sok esetben. */ 
    /* az alap ablak pozíciója és mérete */  
    base.hints.x = 50; 
    base.hints.y = 50; 
    base.hints.width = WIDTH; 
    base.hints.height = HEIGHT; 
    base.hints.flags = PPosition | PSize;  
    base.bordwidth = 5; 
    /* ablak létrehozása */  
    base.window = XCreateSimpleWindow (mydisp, DefaultRootWindow (mydisp), base.hints.x, base.hints.y, base.hints.width, base.hints.height, base.bordwidth, black.pixel, black.pixel);  
    XSetStandardProperties (mydisp, base.window, baseword, baseword, None, NULL, 0, &base.hints); 
    /* kilépés az ablak pozícióból és méretből */  
    quit.hints.x = 5;  
    quit.hints.y = 450;  
    quit.hints.width = 70;  
    quit.hints.height = 30;  
    quit.hints.flags = PPosition | PSize;  
    quit.bordwidth = 5; 
    quit.window = XCreateSimpleWindow (mydisp, base.window, quit.hints.x, quit.hints.y, quit.hints.width, quit.hints.height, quit.bordwidth, green.pixel, yellow.pixel);  
    XSetStandardProperties (mydisp, quit.window, exitword, exitword, None, NULL, 0, &quit.hints); 
    /* Fontok betöltése. */  
    font = XLoadFont (mydisp, "fixed");  
    font2 = XLoadFont (mydisp, "fixed"); 
    /* GC létrehozása éa inicializáció. */  
    textgc = XCreateGC (mydisp, base.window, 0,0); XSetFont (mydisp, textgc, font); 
    XSetForeground (mydisp, textgc, white.pixel); 
    linegc = XCreateGC (mydisp, base.window, 0,0);  
    XSetForeground (mydisp, linegc, white.pixel); 
    itemgc = XCreateGC (mydisp, quit.window, 0,0);  
    XSetFont (mydisp, itemgc, font2);  
    XSetForeground (mydisp, itemgc, black.pixel); 
    pointgc = XCreateGC (mydisp, base.window, 0,0);  
    XSetForeground (mydisp, pointgc, green.pixel); 
    /* A program eseményvezérelt, az XSelectInput híváshalmazok, melyek megszakítások minden egyes ablaknak. */  
    XSelectInput (mydisp, base.window, ButtonPressMask | KeyPressMask | ExposureMask);  
    XSelectInput (mydisp, quit.window, ButtonPressMask | KeyPressMask | ExposureMask); 
    /* ablak leképezés - ez engedi az ablak megjelenítését. */  
    XMapRaised (mydisp, base.window);  
    XMapSubwindows (mydisp, base.window); 
    /* Skáláz minden adat pontot. */  
    zeroaxis = HEIGHT/2;  
    scale = (float)zeroaxis;  
    for (j=0;j<WIDTH;j++) points[j] = zeroaxis - (int)(results[j] * scale); 
    /* A fő esemény ciklus - kilép, amikor a felhasználó megnyomja "exit" gombot. */  
    done = 0; 
    while (! done) {  
       XNextEvent (mydisp, &myevent);  
       /* Olvassa a következő eseményt. */  
       switch (myevent.type) {  
       case Expose:  
          if (myevent.xexpose.count == 0) {  
             if (myevent.xexpose.window == base.window) {  
                XDrawString (mydisp, base.window, textgc, 775, 30, "Wave",4);  
                XDrawLine (mydisp, base.window, linegc, 1,zeroaxis,WIDTH, zeroaxis);  
                for (j=1; j<WIDTH; j++) XDrawPoint (mydisp, base.window, pointgc, j, points[j-1]);  
             } 
             else if (myevent.xexpose.window == quit.window) {  
                XDrawString (mydisp, quit.window, itemgc, 12,20, exitword, strlen(exitword));  
             } 
          }  
       /* Expose eset*/  
       break; 
       case ButtonPress:  
          if (myevent.xbutton.window == quit.window) done = 1;  
          break; 
       case KeyPress:  
          break; 
       case MappingNotify:  
          break; 
       } 
    }  
    XDestroyWindow (mydisp, base.window);  
    XCloseDisplay (mydisp);  
  } 

A program futása során a következő eredményt kapjuk (részlet), ahol a lépések száma 5000:

A program futása során elkészülő ablak megjelenését a következő animáció mutatja be: mpi.wave.swf

4.10.4. Prímgenerálás (mpi_prime.c)

A program prímszámokat generál. Minden folyamat egyenlő részt vesz ki a munkából, véve minden -edik számot, ahol az a lépésköz, melyet úgy számolunk ki: , vagyis a páros számokat automatikusan átugorjuk. A lépésközt használó módszert számok folytonos blokkjai miatt preferáljuk, ugyanis a nagyobb számok több számítást igényelnek, és emiatt terhelési kiegyensúlyozatlansághoz vezethetnek. Kollektív kommunikációval csak két adatelemet küldünk: a megtalált prímek számát, és a legnagyobb prímet.

Az mpi_prime.c programforrás a következő:

  /******************************************************************************  
  * FÁJL: mpi_prime.c  
  * LEÍRÁS:  
  * Prímszámokat generál.  
  ******************************************************************************/  
  #include "mpi.h" 
  #include <stdio.h> 
  #include <stdlib.h> 
  #include <math.h> 
  #define LIMIT 2500000 /* Növeljük meg ezt, ha több prímet akarunk találni. */  
  #define FIRST 0 /* Az első taszk rangja. */ 
  int isprime(int n) {  
    int i,squareroot;  
    if (n>10) { 
       squareroot = (int) sqrt(n);  
       for (i=3; i<=squareroot; i=i+2)  
          if ((n%i)==0) return 0;  
          return 1;  
       }  
       /* Feltételezzük, hogy az első négy prímet már meghatározták. */  
    else return 0;  
  } 
  int main (int argc, char *argv[]) {  
    int ntasks, /* az összes taszk száma egy partícióban */  
    rank, /* taszk azonosító */  
    n, /* ciklus változó */  
    pc, /* prím számláló */  
    pcsum, /* a prímek száma, melyeket az összes taszk talált */  
    foundone, /* a legnagyobb újabb prím */  
    maxprime, /* a legnagyobb megtalált prím */  
    mystart, /* ahol a számítást el kell kezdeni */  
    stride; /* minden n-dik szám meghatározása */ 
    double start_time,end_time;  
    MPI_Init(&argc,&argv);  
    MPI_Comm_rank(MPI_COMM_WORLD,&rank);  
    MPI_Comm_size(MPI_COMM_WORLD,&ntasks);  
    if (((ntasks%2) !=0) || ((LIMIT%ntasks) !=0)) {  
       printf("Sajnos ez a feladat páros számú taszkot igényel.\n");  
       printf("Osztható legyen 2-vel a %d. Próbálja 4-et vagy a 8-at.\n",LIMIT);  
       MPI_Finalize();  
       exit(0);  
    } 
    start_time = MPI_Wtime();  
    /* A kezdési idő inicializálása. */  
    mystart = (rank*2)+1;  
    /* A saját kezdési pont meghatározása - páratlan számnak kell lennie. */  
    stride = ntasks*2;  
    /* A lépés meghatározása, kihagyva a páros számokat. */  
    pc=0; /* Í prím számláló inicializálása. */  
    foundone = 0;  
    /******************** a 0 rangú taszk hajtja végre ezt a részt ********************/  
    if (rank == FIRST) {  
       printf("%d taszk használata, hogy megtaláljuk a %d számokat\n",ntasks,LIMIT);  
       pc = 4; /* Feltételezzük, hogy az első négy prím ismert. */  
       for (n=mystart; n<=LIMIT; n=n+stride) {  
          if (isprime(n)) {  
             pc++;  
             foundone = n;  
          }  
       }  
       MPI_Reduce(&pc,&pcsum,1,MPI_INT,MPI_SUM,FIRST,MPI_COMM_WORLD);         MPI_Reduce(&foundone,&maxprime,1,MPI_INT,MPI_MAX,FIRST,MPI_COMM_WORLD); 
       end_time=MPI_Wtime();  
       printf("Kész. A legnagyobb prím %d Az összes prím %d\n",maxprime,pcsum);  
       printf("A futási idő: %.2lf seconds\n",end_time-start_time);  
    } 
    /******************** minden más taszk ezt a részt hajtja végre ***********************/  
    if (rank > FIRST) {  
       for (n=mystart; n<=LIMIT; n=n+stride) {  
          if (isprime(n)) {  
             pc++;  
             foundone = n;  
          }  
       }  
       MPI_Reduce(&pc,&pcsum,1,MPI_INT,MPI_SUM,FIRST,MPI_COMM_WORLD);         MPI_Reduce(&foundone,&maxprime,1,MPI_INT,MPI_MAX,FIRST,MPI_COMM_WORLD);  
    } 
    MPI_Finalize();  
  } 

A program eredményét a következő ábra mutatja:

Feladatok:

1. Készítsük el a Multi-Pascal rendszerben megismert rekurzív sorozat elemeit meghatározó (Fibonacci) programot az MPI eljárások felhasználásával!

2. Készítsük el a Multi-Pascal rendszerben megismert mátrixszorzás (ParallelMatrixMultiply) programot az MPI eljárások felhasználásával!

3. Készítsük el a Multi-Pascal rendszerben megismert négyzetgyökvonás (ParallelSqroot) programot az MPI eljárások felhasználásával!

4. Készítsük el a PVM rendszerben megismert forkjoin (forkjoin.c) programot az MPI eljárások felhasználásával!

5. fejezet - JCluster

5.1. Bevezetés

A Jcluster eszközrendszerben kifejlesztésre került egy olyan algoritmus, amelynek segítségével ütemezni lehet a feladatokat és elosztani a terhelést a teljes klaszterban. A kommunikáció hatékonyságához az UDP protokolt használták a TCP helyett, és implementálásra került egy megbízható, magasszintű üzenet kezelés a többszálú kommunkációhoz. Egy egyszerű PVM és MPI típusú üzenet kezelési interface is létrehozásra került a könnyebb programozás érdekében.

Voltaképpen a Jcluster a PVM/MPI java megfelelőjének is tekinthető. Például megtalálható benne az összes szükséges PVM-ből jól ismert parancs Jcluster-es megfelelője.

Elérhetősége és letöltése:

A Jcluster honelapon például jcluster-1.0.5.tar.gz néven érhető el ill. tölthető le. Platformfüggetlen.

5.1.1. Telepítés, futtatás

I. A Jcluster telepítése nagyon egyszerű. Először ki kell tömöríteni a letöltött anyagot egy jegyzékbe. Ezzel a következő állományokat és jegyzékeket kapjuk:

Jegyzékek (directories):

conf - a Jcluster eszközrendszer konfigurációs jegyzéke;

docs - a dokumentációs jegyzék;

lib - a fordításhoz szükséges definíciós állomány, a jcluster.jar jegyzéke;

src - a Jcluster eszközrendszer forráskódja;

userApps- az alkalmazások jegyzéke, melyeket a felhasználó hozott létre.

Állományok (files in WINDOWS):

console.bat - a console program indításához szükséges állomány;

start.bat - a démon program indításához szükséges;

close.bat - a démon program leállítására szolgál.

Állományok (files in LINUX):

console.sh - a console program indításához szükséges állomány;

start.sh - a démon program indításához szükséges;

close.sh - a démon program leállítására szolgál;

II. A Jcluster eszközrendszer futtatása nagyon egyszerűen történik. Előtte viszont ellenőrizni kell, hogy a megfelelő JDK verzió fel van-e telepítve a számítógépre (legalább 1.4.1-es verzió) és a PATH-ba bekerült-e (ha nem, akkor nekünk kell berakni) a „%JAVA_HOME%/bin” kód.

WINDOWS esetén a start.bat-ot, LINUX esetén a start.sh-t kell futtatni, és ha a „Initialization finished” mondatot látjuk az ablakban, akkor az indítás sikeres volt. A következő ábra ezt mutatja:

Egy másik gépre történő telepítés és futtatás után a gépek „egymásra találnak”, azaz automatikusan megtalálják egymást a hálózaton (feltéve, ha ez külön nics tiltva). Ez voltaképpen a PVM-ben is megtalálható add parancsnak felel meg (a PVM add parancsának használata „nehézkesebb”). A csatlakozást a következő ábrán tekinthetjük meg:

5.1.2. Alkalmazások indítása a Jclusterben

A kiadott verzióhoz tartozik az királynő probléma ( queen problem). Ez a program a „\userApps\test” jegyzékben található. A futtatáshoz először indítsuk el a console.bat (console.sh) programot (a korábban elindított ablakot NE zárjuk be). Ekkor kapjuk a következőt:

Írjuk be a „Jc:>” prompt után az „sp test.Queen 13”-t, majd nyomjunk egy ENTER-t a futtatáshoz.

Korábban a démon indításakor használt ablakban/konzolban jelennek meg az információk.

A fentiek részletes magyarázata a következő. A konzolban a következő parancsokat használhatjuk:

Az sp parancs:

sp <alkalmazás> [opció],

ahol az

<alkalmazás> - az alkalmazás neve, melyben benne van a csomag neve,

[opció] - egy sztring argumentum, amelyet az alkalmazás fog kezelni. Ezt a parancsot PVM típusú üzenetátadásban használhatjuk.

Az spm parancs:

spm <alkalmazás> <szám> [opció],

ahol az

<alkalmazás> - az alkalmazás neve, melyben benne van a csomag neve,

<szám> - az MPI típusú alkalmazások száma

[opció] - egy sztring argumentum, amelyet az alkalmazás fog kezelni. Ezt a parancsot MPI típusú üzenetátadásban használhatjuk.

Az ls parancs:

ls,

ez a parancs szolgál a számítógép processz információinak kiíratására.

Az lsall parancs:

lsall,

ez a parancs szolgál az összes számítógép processz információinak kiíratására.

A quit parancs:

quit,

amely bezárja a „console” ablakot, de a „démon”-t nem állítja le.

Az exitd parancs:

exitd,

amely bezárja a „démon” és a „console” ablakokat.

A kill parancs:

kill <taskId>,

<taskId> - a processz azonosító, melyet az ls paranccsal érhetünk el..

Ennek a segítségével zárhatunk be egy processzt.

A killall parancs:

killall,

amely bezárja/terminálja az összes processzt az összes gépen.

A reload parancs:

reload,

amely felszabadítja az összes „cache” elérhetőséget.

5.2. Alkalmazások készítése

5.2.1. PVM típusú alkalmazások

Ismét az királynő problémát használjuk, hogy bemutassuk a PVM típusú üzenetátadást (a teljes programforrrást az alszakasz végén adjuk meg).

Az eredeti probléma a hagyományos sakktáblából indult ki.

A nyolckirálynő-probléma egy sakkfeladvány, lényege a következő: hogyan lehet 8 királynőt úgy elhelyezni egy -as sakktáblán, hogy a sakk szabályai szerint ne üssék egymást. Ehhez a királynő/vezér lépési lehetőségeinek ismeretében az kell, hogy ne legyen két bábu azonos sorban, oszlopban vagy átlóban.

A nyolckirálynő-probléma egy példa az ennél általánosabb „ királynő problémára”, ami azt a kérdést veti fel, hányféleképpen lehet lerakni darab királynőt egy -es táblán.

Története

A kérdést először Max Bezzel vetette fel 1848-ban. Az évek során sok matematikus - többek között Gauss és Georg Cantor - foglalkozott vele (és az általánosított -királynő-problémán).

Az első megoldást Franz Nauck adta 1850-ben.

1874-ben S. Gunther determinánsok használatával adott egy eljárást, amivel lerakhatóak a bábuk. Később ezt J. W. L. Glaisher finomította.

Edsger Dijkstra 1972-ben arra használta ezt a problémát, hogy bemutassa a strukturált programozás előnyeit, erejét. Publikált egy részletes leírást a backtrack algoritmusról.

Megoldás

A megoldás nehezen számítható ki, mivel a bábuknak összesen különböző lerakása létezik, de ebből csak 92 felel meg az királynő probléma szabályainak. Ez igen nagy számítási időt jelent. Az összes lehetséges lerakások száma, és ezáltal a számításhoz szükséges idő csökkenthető azáltal, hogy bevezetünk egy olyan szabályt, miszerint minden sorban (vagy oszlopban) csak egy-egy bábu állhat. Így a vizsgálandó lerakások száma csupán (16884-szer kevesebb). Ez -ra kezelhető, de megengedhetetlenül nagy például -ra.

Algoritmizálási szempontból a bábuk helyét érdemes tömbként kezelni: mivel minden sorban csak egyetlen bábu állhat, ezért elég a sorokat megszámozni (1-től -ig), majd darab számot lejegyezni aszerint, hogy az -edik sorban hányadik helyen áll bábu.

A következő algoritmus, ami egy - a probléma szabályainak megfelelő - tömböt ad eredményül ( és esetekre):

  • Osszuk el -et 12-vel. Jegyezzük le a maradékot.

  • Írjuk le egy listába 2-től -ig a páros számokat növekvő sorrendben.

  • Ha a maradék 3 vagy 9, akkor a 2-es számot vigyük át a lista végére.

  • Írjuk a lista végére 1-től -ig a páratlan számokat növekvő sorrentben, de ha a maradék 8, akkor páronként cseréljük fel őket (például 3, 1, 7, 5, 11, 9, …).

  • Ha a maradék 2, akkor cseréljük ki az 1-et és 3-at, valamint tegyük az 5-öt a lista végére.

  • Ha a maradék 3 vagy 9, akkor tegyük az 1-et és 3-at a lista végére (ebben a sorrendben).

Végül tegyük le a bábukat a sakktáblára: az első sorba oda, ahova a lista első száma mutatja; a második sorba oda, ahová a lista második száma mutatja…

Néhány eredmény

14 királynő: 2, 4, 6, 8, 10, 12, 14, 3, 1, 7, 9, 11, 13, 5,

15 királynő: 4, 6, 8, 10, 12, 14, 2, 5, 7, 9, 11, 13, 15, 1, 3,

20 királynő: 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 3, 1, 7, 5, 11, 9, 15, 13, 19, 17.

További részletekért lásd még a Nyolckirálynő-probléma vagy az N-Queens Problem lapokat.

A programozáshoz egyszerű eszközöket használunk, csupán négy osztályt érintünk. Az egyik ilyen osztály a JTasklet.class, amely az alaposztály, ugyanis minden alkalmazás ebből származtatható (örökíthető-inheritance) és implementálja a public void work(); eljárást.

A programban a következő forrásrészlet felel meg ennek:

  package test; 
  import jcluster.*; 
  public class Queen extends JTasklet{  
    public Queen() {  
  } 
  int num = 12; //alapértelmezett érték  
  int layer = 12;  
  int count = 0; 
  public void work(){  
    if (env.pvm_parent () == -1){ 
       parent();  
    } else {  
       children();  
    }  
  }  

A JTasklet.class osztályban van egy env nevű változó, amely a JEnvironment.class példánya. Ez egy nagyon fontos osztály, ugyanis minden PVM típusú eljárás ebben van. Például a fenti env.pvm_parent () eljárás adja meg a szülő processz azonosítóját. (Egy processz nem rendelkezik szülővel, ha az eljárás -1-et ad vissza, azaz ő a szülő.)

Egy másik osztály a JMessage.class. Ez puffereli az üzeneteket, amelyeket küldünk vagy fogadunk.

A programban a következő forrásrészletben találjuk meg a fent leírtakat:

  public void parent(){ 
    String arg = env.getArg ();  
    if (arg != null){ 
       try{  
          num = new Integer(arg).intValue(); 
          if ((num-layer)>3) layer = num -3;  
       }catch(Exception e){ 
          System.out.println (e.toString ()); 
       } 
    } 
    long starttime = System.currentTimeMillis ();  
    int[] initPerm = new int[num]; 
    JMessage[] mesArray = new JMessage[num]; 
    try{  
       for (int i=0;i<num;i++){ 
          initPerm[0] = i;  
          mesArray[i] = new JMessage(env); 
          mesArray[i].pack (num); 
          mesArray[i].pack (layer); 
          mesArray[i].pack (num - 1);  
          mesArray[i].pack( initPerm,0,1);  
       } 
       System.out.println ("begin spawn children...");  
       env.pvm_spawn ("test.Queen",num,mesArray);  
       int number = 0;  
       for (int i=0;i<num;i++){  
          JMessage m = env.pvm_recv (); 
          number = number + m.upkint ();  
       } 
       long endtime = System.currentTimeMillis (); 
       System.out.println("Computing finished."); 
       System.out.print("Using time: "); 
       System.out.println (endtime-starttime+"ms"); 
       System.out.print("Solution number of "+num+"*"+num+" Queen problem is "); 
       System.out.println (number+".");  
    }catch(JException e){ 
       System.out.println (e.toString ());  
    }  
  } 
  public void children() {  
    try{  
       JMessage m = env.pvm_getInitMsg (); 
       if (m!=null){  
          int n = m.upkint (); 
          int s = m.upkint (); 
          int l = m.upkint (); 
          int[] pos = new int[n]; 
          m.unpack (pos,0,n-l);  
          if (l > s){  
             JMessage[] mesArray = new JMessage[n]; 
             int j = 0; 
             for (int i=0;i<n;i++){ 
                pos[n-l] = i; 
                if (isValid(n-l+1,pos)){  
                   mesArray[j] = new JMessage(env);  
                   mesArray[j].pack (n); 
                   mesArray[j].pack (s); 
                   mesArray[j].pack (l-1); 
                   mesArray[j].pack( pos,0,n-l+1); 
                   j++; 
                }  
             }  
             env.pvm_spawn ("test.Queen",j,mesArray);  
             int number = 0;  
             for (int i=0;i<j;i++){ 
                JMessage mm = env.pvm_recv (); 
                number = number + mm.upkint (); 
             } 
             JMessage mes = new JMessage(env); 
             mes.pack (number); 
             env.pvm_send(mes,env.pvm_parent() ); 
          } else {  
             subwork(n,l,pos); 
             JMessage mes = new JMessage(env); 
             mes.pack (count); 
             env.pvm_send(mes,env.pvm_parent() ); 
          }  
       }else{  
          System.out.println ("Error: no initMessage");  
       }  
    }catch(JException e){  
       System.out.println (e.toString ());  
    } 
  } 

Az üzenet küldéséhez a new JMessage(env); használhatjuk. A new JMessage(); alak tiltott. A fogadott üzenetet a JMessage m = env.pvm_recv ();-vel kezelhetjük. Az utolsó osztály a JException.class. Nem rendelkezik speciális paranccsal, ez pontosan az Exception.class osztálynak felel meg.

A teljes programforrás a következő:

  package test; 
  import jcluster.JTasklet ;  
  import jcluster.JException ; 
  import jcluster.JEnvironment ; 
  import jcluster.JMessage ; 
  public class Queen extends JTasklet{  
    public Queen() {  
  } 
    int num = 12; //alapértelmezett érték  
    int layer = 12;  
    int count = 0; 
    public void work(){  
       if (env.pvm_parent () == -1){ 
          parent();  
       } else {  
          children();  
       }  
    }  
    public void parent(){ 
       String arg = env.getArg ();  
       if (arg != null){ 
          try{  
             num = new Integer(arg).intValue(); 
             if ((num-layer)>3) layer = num -3;  
          }catch(Exception e){ 
             System.out.println (e.toString ()); 
          } 
       } 
       long starttime = System.currentTimeMillis ();  
       int[] initPerm = new int[num]; 
       JMessage[] mesArray = new JMessage[num]; 
       try{  
          for (int i=0;i<num;i++){ 
             initPerm[0] = i;  
             mesArray[i] = new JMessage(env); 
             mesArray[i].pack (num); 
             mesArray[i].pack (layer); 
             mesArray[i].pack (num - 1);  
             mesArray[i].pack( initPerm,0,1);  
          } 
          System.out.println ("begin spawn children...");  
          env.pvm_spawn ("test.Queen",num,mesArray);  
          int number = 0;  
          for (int i=0;i<num;i++){  
             JMessage m = env.pvm_recv (); 
             number = number + m.upkint ();  
          } 
          long endtime = System.currentTimeMillis (); 
          System.out.println("Computing finished."); 
          System.out.print("Using time: "); 
          System.out.println (endtime-starttime+"ms"); 
          System.out.print("Solution number of "+num+"*"+num+" Queen problem is "); 
          System.out.println (number+".");  
       }catch(JException e){ 
          System.out.println (e.toString ());  
       }  
    } 
    public void children() {  
       try{  
          JMessage m = env.pvm_getInitMsg (); 
          if (m!=null){  
             int n = m.upkint (); 
             int s = m.upkint (); 
             int l = m.upkint (); 
             int[] pos = new int[n]; 
             m.unpack (pos,0,n-l);  
             if (l > s){  
                JMessage[] mesArray = new JMessage[n]; 
                int j = 0; 
                for (int i=0;i<n;i++){ 
                   pos[n-l] = i; 
                   if (isValid(n-l+1,pos)){  
                      mesArray[j] = new JMessage(env);  
                      mesArray[j].pack (n); 
                      mesArray[j].pack (s); 
                      mesArray[j].pack (l-1); 
                      mesArray[j].pack( pos,0,n-l+1); 
                      j++; 
                   }  
                }  
                env.pvm_spawn ("test.Queen",j,mesArray);  
                int number = 0;  
                for (int i=0;i<j;i++){ 
                   JMessage mm = env.pvm_recv (); 
                   number = number + mm.upkint (); 
                } 
                JMessage mes = new JMessage(env); 
                mes.pack (number); 
                env.pvm_send(mes,env.pvm_parent() ); 
             } else {  
                subwork(n,l,pos); 
                JMessage mes = new JMessage(env); 
                mes.pack (count); 
                env.pvm_send(mes,env.pvm_parent() ); 
             }  
          }else{  
             System.out.println ("Error: no initMessage");  
          }  
       }catch(JException e){  
          System.out.println (e.toString ());  
       } 
    } 
    public void subwork(int n, int m,int[] perm){ 
       if (m!=1){ 
          for (int i=0;i<n;i++){ 
             perm[n-m] = i; 
             if (isValid(n-m+1,perm)){ 
                subwork(n,m-1,perm);  
             }  
          } 
       }else{ 
          for (int i=0;i<n;i++){ 
             perm[n-m] = i; 
             if (isValid(n-m+1,perm)){  
                count++; 
                return;  
             } 
          } 
       } 
    } 
    public boolean isValid(int n1,int[] perm1){  
       boolean res = true; 
       for (int i=0;i<(n1-1);i++){ 
          if (perm1[i]==perm1[n1-1]){  
             res = false; 
             break;  
          }  
       } 
       for (int i=0;i<(n1-1);i++){  
          if ( (n1-1-i) == Math.abs(perm1[n1-1]-perm1[i])){  
             res = false;  
             break;  
          } 
       }  
       return res;  
    } 
  } 

5.2.2. MPI típusú alkalmazások

A következő sorokban egy MPI típusú tesztalkalmazás prograforrása van megadva. A további részleteket a Jcluster leírásában olvashatunk pl. az MPI-es megoldásokról vagy az egyéb osztályokról.

A program forrása:

  package test; 
  import jcluster.JTasklet;  
  import jcluster.JException; 
  import jcluster.JEnvironment; 
  import jcluster.JMessage;  
  import jcluster.op.*; 
  public class TestMPI extends JTasklet{ 
    public TestMPI() {  
    } 
    public void work(){ 
       System.out.println ("Begin to test reduce..."); 
       testReduce(); 
       try{ 
          env.MPI_barrier ();  
       }catch(JException e){ 
          System.out.println (e.toString ());  
       } 
       System.out.println ("Begin to test allGather..."); 
       testAllGather(); 
       try{ 
          env.MPI_barrier ();  
       }catch(JException e){  
          System.out.println (e.toString ());  
       } 
       System.out.println ("Begin to test alltoAll..."); 
       testAlltoAll(); 
    } 
    public void testReduce() {  
       try {  
          int MAXLEN = 10000;  
          int root, out[], in[], i, j, k;  
          int myself, tasks; 
          out = new int[MAXLEN];  
          in = new int[MAXLEN]; 
          myself = env.MPI_rank();  
          tasks = env.MPI_size();  
          root = tasks/2; 
          for(j = 1; j <= MAXLEN; j*=10) { 
             for(i = 0; i < j; i++) {  
                out[i] = i;  
             } 
             env.MPI_reduce (out, 0, in, 0, j, env.INT , env.SUM , root); 
             if(myself == root) { 
                for(k=0;k<j;k++) { 
                   if(in[k] != k*tasks) {  
                      System.out.println("bad answer ("+ in[k] +") at index "+ k +" of "+ j +" (should be "+ k*tasks +")");  
                      break; 
                   } 
                }  
             }  
             if (myself == root) System.out.println (j); } 
             env.MPI_barrier (); 
             if (myself == 0) {  
                System.out.println("TEST REDUCE COMPLETE");  
             } 
       } catch (JException me) {  
          System.out.println (me.toString ());  
       }  
    } 
    public void testAllGather() {  
       try {  
          int MAXLEN = 100;  
          int root, out[], in[], i, j, k; 
          int myself,tasks; 
          out = new int[10000]; 
          in = new int[10000]; 
          myself = env.MPI_rank(); 
          tasks = env.MPI_size(); 
          root = 0;  
             for(j = 1; j <= MAXLEN; j *= 10) {  
                for(i = 0; i < j; i++) {  
                   out[i] = myself;  
                } 
                env.MPI_allGather(out, 0, j, env.INT, in, 0, j, env.INT);  
                for(i = 0; i < tasks; i++) { 
                   for(k = 0; k < j; k++) {  
                      if ( in[k + i * j] != i) {  
                         System.out.println("bad answer ("+ in[k] +") at index "+ (k + i * j) +" of "+ (j * tasks) +" (should be "+ i +")"); 
                         break; 
                      }  
                   }  
                }  
                if (myself == root) System.out.println (j);  
             }  
          env.MPI_barrier ();  
          if (myself == 0) {  
             System.out.println("TEST AllGather COMPLETE");  
          }  
       } catch (Exception e) {  
          e.printStackTrace();  
       }  
    }  
    public void testAlltoAll() {  
       try {  
          int MAXLEN = 100;  
          int ARRAYSIZE = 10000; 
          int out[], in[], i, j, k;  
          int myself, tasks; 
          in = new int[ARRAYSIZE];  
          out = new int[ARRAYSIZE]; 
          myself = env.MPI_rank();  
          tasks = env.MPI_size(); 
          for(j = 1; j <= MAXLEN; j *= 10) {  
             for(i = 0; i < j * tasks; i++) {  
                out[i] = myself; 
             } 
             env.MPI_allToAll (out, 0, j, env.INT, in, 0, j, env.INT); 
             for(i = 0; i < tasks; i++) {  
                for(k = 0; k < j; k++) {  
                   if(in[k + i * j] != i) {  
                      System.out.println("bad answer ("+ in[ k + i * j] +") at index " + (k + i * j) +" of "+ (j * tasks) +" (should be "+ i +")");  
                      break;  
                   }  
                }  
             }  
             if (myself == 0) System.out.println(j); }  
             env.MPI_barrier();  
             if (myself == 0) {  
                System.out.println("TEST AlltoAll COMPLETE");  
             } 
       } catch (JException me) {  
          me.printStackTrace();  
       }  
    } 
  } 

5.2.3. „Hello World” program

A programok fordításához szokásos java szerkesztőt használhatunk, pl. az eclipse-t. A fordításhoz szükséges a megfelelő jcluster.jar betöltése.

A hello program során egy egyszerű üzenetküldés/fogadás zajlik le. A létrehozott gyermek processz által elküldött „Hello World” szöveg kerül fogadásra a szülő által. A programban viszonylag sok képernyőn megjelenő információ jön létre, amely tovább segíti a program működésének megértését.

A work indítja a programot, így az indítás egyszerűbb.

A program forrása a következő:

  import java.util.LinkedList;  
  import jcluster.*; 
  public class hello extends JTasklet { 
    public hello() { } 
    public void child() {  
       System.out.println(env.pvm_me() + " : child begin");  
       try {  
          env.pvm_joinGroup("test");  
          System.out.println(env.pvm_me() + " : child join group"); 
          env.pvm_barrier("test", 3);  
          System.out.println(env.pvm_me() + " : child barrier over"); 
          JMessage m = env.pvm_recvTag(9999); 
          System.out.println(env.pvm_me() + " : child recv over"); 
          System.out.println(env.pvm_me() + " : " + m.unpack()); 
          env.pvm_lvGroup("test");  
       } catch (JException e) {  
          System.out.println(e.toString());  
       }  
    } 
    public void parent() {  
       try {  
          System.out.println(env.pvm_me() + " : parent begin spawn"); 
          env.pvm_spawn("hello", 2); 
          env.pvm_joinGroup("test"); 
          System.out.println(env.pvm_me() + " : parent join group"); 
          env.pvm_barrier("test", 3); 
          System.out.println(env.pvm_me() + " : parent barrier over"); 
          JMessage m = new JMessage(env); 
          m.pack("Hello World! -by jcluster"); 
          System.out.println(env.pvm_me() + " : parent before bcast");  
          env.pvm_bcast("test", m); 
          System.out.println(env.pvm_me() + " : parent bcast over"); 
          env.pvm_lvGroup("test");  
       } catch (JException e) { 
          System.out.println(e.toString());  
       }  
    } 
    public void work() {  
       if (env.pvm_parent() == -1) {  
          parent(); 
       } else {  
          child();  
       }  
    }  
  } 

A futtatási eredmény a következő ábrán tekinthető meg:

A fenti ábrán jól láthatóak az egyes lépések jelzései ill. a szülő-gyermek csoportkapcsolódási lépései (belépés-join és elhagyás-leave). A program működéséről lásd még a következő animációt: hello.jcluster.swf

5.3. Példaprogramok

Ezen fejezet végén olyan programok forrását adjuk meg, melyek már más környezetben létrehozásra kerültek. A célunk az volt, hogy megmutassuk ezeknek a Jcluster-ben történő megvalósítását.

5.3.1. RankSort (Multi-Pascal - Jcluster)

A korábban Multi-Pascal környezetben elkészített Ranksort program (lásd még a 2.2.2. alszakaszt vagy a 2.1. példát) megvalósítása, melybe előre belekerültek azok az adatok, melyeket rendezni/rangsorolni kell.

A program forrása a következő:

  import jcluster.*; 
  public class RankSort extends JTasklet{ 
    public RankSort() {} 
    double[] tomb= {3,4.87,-5,6,7,-8.45,9,3.75,4,6,-2.46,3,4,6.75,7,8,9,3,-2,1.01,1,1,0,3,-5,6,7,2,4,2};  
    int n = tomb.length;  
    double[] rendezett= new double[n]; 
    public void work(){ 
       if (env.pvm_parent() == -1){ 
          parent();  
       } else {  
          children();  
       }  
    }  
    public void parent(){ 
       try {  
          System.out.println(env.pvm_me() + " : parent begin spawn"); 
          env.pvm_spawn("RankSort", n); //n db gyermek processzt indít  
          env.pvm_joinGroup("test");  
          env.pvm_barrier("test", n+1); //vár, amíg n+1 db processz lesz a csoportban  
          for(int i=0; i<n; i++){  
             JMessage mes = new JMessage(env);  
             mes.pack(i);  
             env.pvm_send(mes, i+1); //elküldi az i+1.processznek az i indexet  
          }  
             int szamlalo=n;  
             while (szamlalo!=0) {  
                JMessage m=env.pvm_recv(); //jött egy üzenet  
                int rang=m.upkint(); 
                int eredeti_index=m.upkint()-1;  
                rendezett[rang]=tomb[eredeti_index];  
                szamlalo--;  
             } 
             System.out.println("rendezett tömb: "); 
             for (int i=0; i<n;i++){  
                System.out.println(this.rendezett[i]); 
             }  
             System.out.println("szülő kilép");  
             env.pvm_lvGroup("test");  
       } catch (Exception e){ }  
    }  
    public void children(){  
       try{  
          env.pvm_joinGroup("test");  
          env.pvm_barrier("test", n+1);  
          int src;  
          JMessage m=env.pvm_recv(); // vár amíg a szülőtől üzenetet kap  
          src=m.upkint();  
          double testval=tomb[src];  
          int j=src;  
          int rank=0; 
          do {  
             j=(j+1) % n;  
             if (testval >= tomb[j]) {  
                if ( (testval==tomb[j])&&(j<src) ){  
                   rank++;  
                }  
                if (testval>tomb[j]){  
                   rank++; 
                }  
             }  
          } while(j!=src);  
          JMessage m2= new JMessage(env);  
          m2.pack(rank);  
          m2.pack(env.pvm_me()); 
          env.pvm_send(m2, env.pvm_parent()); //küld a szülőnek üzenetet 
          env.pvm_lvGroup("test");  
       } catch(JException e){  
          System.out.println (e.toString ());  
       } 
    }  
  } 

5.3.2. Mmult (Multi-Pascal - Jcluster)

Ez a program a Multi-Pascal-ban megismert mátrixszorzási algoritmust használja fel (lásd még a 2.2.5. alszakaszt vagy a 2.4. ábrát), azaz egy Jcluster-es implemetálásnak tekinthető. Ebben először létrehozásra kerül két mátrix, majd elindul a párhuzamos eljárás a szorzat meghatározásához. A képernyőn megjelenik a két mátrix és a végeredmény is.

A program forrása a következő:

  import jcluster.*; 
  import java.util.*; 
  public class MMult extends JTasklet {  
    Random r = new Random();  
    static int n=4, m=5, k=6;  
    static int[][] A= new int[n][m];  
    static int[][] B=new int[m][k];  
    int[][] C=new int[n][k]; 
    public MMult(){ }  
    public void Matrixok(){  
       //két véletlen mátrix létrehozása  
       for (int i=0; i<n; i++){  
          for (int j=0;j<m; j++){  
             A[i][j]=(int)r.nextInt()/100000000;  
          }  
       } 
       for (int i=0; i<m; i++){  
          for (int j=0;j<k; j++){ 
             B[i][j]=(int)r.nextInt()/100000000;  
          } 
       }  
       // Kiíratás  
       System.out.print("A= \n"); 
       for (int i=0; i<n; i++){  
          for (int j=0;j<m; j++){  
             System.out.print(A[i][j]+" ");  
          }  
          System.out.print("\n");  
       }  
       System.out.print("B= \n"); 
       for (int i=0; i<m; i++){  
          for (int j=0;j<k; j++){  
             System.out.print(B[i][j]+" ");  
          } 
          System.out.print("\n");  
       } 
    }  
    public void work(){  
       if (env.pvm_parent() == -1){ 
          parent();  
       } else { 
          children(); 
       } 
    } 
    public void parent(){  
       try { 
          Matrixok(); 
          env.pvm_spawn("MMult", n);  
          env.pvm_joinGroup("szorzo_csoport"); 
          env.pvm_barrier("szorzo_csoport", n+1); 
          int[] B_oszlopfolytonos= new int[k*m]; 
          for(int j=0; j<k; j++){  
             for(int i=0; i< m; i++ ){  
                B_oszlopfolytonos[j*m+i]=B[i][j];  
             } 
          } 
          for(int i=0; i<n; i++){  
             JMessage mes = new JMessage(env);  
             int[] A_sora= new int[m];  
             for (int j=0; j<m; j++){  
                A_sora[j]=A[i][j];  
             } 
             mes.pack(A_sora);  
             env.pvm_send(mes, i+1);  
             //elküldi az i+1.processznek az A i-edik sorát és a B mátrixot  
          } 
          env.pvm_barrier("szorzo_csoport", n+1); 
          JMessage mes2=new JMessage(env); 
          mes2.pack(B_oszlopfolytonos); 
          env.pvm_bcast("szorzo_csoport", mes2); 
          int szamlalo=n*k;  
          while (szamlalo!=0) {  
             JMessage m=env.pvm_recv();  
             //jött egy üzenet  
             int i=m.upkint();  
             int j=m.upkint();  
             int ertek=m.upkint();  
             C[i][j]=ertek;  
             szamlalo--; 
          }  
          System.out.print("A*B= \n"); <(/81>
          for (int i=0; i<n; i++){  
             for (int j=0;j<m; j++){ 
                System.out.print(C[i][j]+" "); 
             } 
             System.out.print("\n");  
          }  
          env.pvm_lvGroup("szorzo_csoport");  
       } catch (JException e){  
       } 
    } 
    public void children(){  
       try{ 
          env.pvm_joinGroup("szorzo_csoport"); 
          env.pvm_barrier("szorzo_csoport",n+1); 
          JMessage uzenet=env.pvm_recv(); 
          int[] A_sora=new int[m]; 
          uzenet.unpack(A_sora,0,m); 
          env.pvm_barrier("szorzo_csoport", n+1);
          JMessage uzenet2=env.pvm_recv();  
          int[] B_of=new int[m*k]; 
          uzenet2.unpack(B_of,0,m*k); 
          for (int i=0; i< k ; i++){  
             int sum=0;  
             for(int j=0; j< m; j++){  
                sum=sum+A_sora[j]*B_of[i*m+j]; 
             } 
             JMessage m2= new JMessage(env); 
             m2.pack(env.pvm_me()-1); 
             m2.pack(i); 
             System.out.println(env.pvm_me()-1+" "+i +" : " + sum);  
             m2.pack(sum); 
             env.pvm_send(m2, env.pvm_parent()); 
          } 
          env.pvm_lvGroup("szorzo_csoport");  
       } catch(JException e){ 
  System.out.println (e.toString ());  
       } 
    } 
  } 

A program futtatása után a következőket kapjuk:

A program futását megtekinthetjük a következő animációban is: mmult.jcluster.swf.

5.3.3. Mmult2 (PVM - Jcluster)

Az utolsó példaprogram esetén a PVM rendszerben megismert mátrixszorzás Jcluster-es változatát adjuk meg. Ebben az esetben is véletlenszerűen létrehozott mátrix és egységmátrix szorzását végezzük el.

A program forrása a következő:

  import jcluster.*; 
  import java.util.*; 
  public class MMult2 extends JTasklet { 
    Random r = new Random();  
    static int m=3;  
    static int blocksize=2;  
    static int n=blocksize*m; 
    static int blokkhossz=blocksize*blocksize; 
    static int[][] A= new int[n][n]; 
    static int[][] B=new int[n][n];  
    int[][] C=new int[n][n];  
    final int ATAG=2; 
    final int BTAG=2; 
    final int DIMTAG=2; 
    public MMult2(){ }  
    public void Matrixok(){  
       //két mátrix létrehozása, A véletlen, B egységmátrix  
       for (int i=0; i<n; i++){  
          for (int j=0;j<n; j++){ 
             A[i][j]=(int)r.nextInt()/100000000;  
             if (i!=j){  
                B[i][j]=0;  
             } else B[i][j]=1;  
          }  
       }  
       // Kiiratás  
       System.out.print("A= \n"); 
       for (int i=0; i<n; i++){ 
          for (int j=0;j<n; j++){ 
             System.out.print(A[i][j]+" "); 
          } 
          System.out.print("\n");  
       } 
       System.out.print("B= \n"); 
       for (int i=0; i<n; i++){ 
          for (int j=0;j<n; j++){ 
             System.out.print(B[i][j]+" ");  
          } 
          System.out.print("\n");  
       }  
    } 
    public void work(){  
       if (env.pvm_parent() == -1){ 
          parent(); 
       } else {  
          children(); 
       } 
    } 
    public void parent(){ 
       try { 
          Matrixok(); 
          env.pvm_spawn("MMult2", m*m); 
          env.pvm_joinGroup("szorzo_csoport"); 
          env.pvm_barrier("szorzo_csoport", m*m+1); 
          System.out.println("parent barrier"); 
          int[] A_oszlopfolytonos= new int[n*n]; 
          int[] B_oszlopfolytonos= new int[n*n]; 
          int[] C_oszlopfolytonos= new int[n*n]; 
          for(int j=0; j<n; j++){ 
             for(int i=0; i< n; i++ ){ 
                A_oszlopfolytonos[j*n+i]=A[i][j]; 
                B_oszlopfolytonos[j*n+i]=B[i][j]; 
             } 
          } 
          System.out.println("oszlopfolytonos tárolások kész"); 
          for(int j=0; j<m; j++){ 
             for (int i=0; i<m;i++){ 
                int[] A_blokk= new int[blokkhossz]; 
                int[] B_blokk= new int[blokkhossz]; 
                for (int k2=0; k2<blocksize; k2++){ 
                   for (int k1=0;k1<blocksize;k1++){ 
                      A_blokk[k2*blocksize+k1]=A_oszlopfolytonos[(j*blocksize+k2)*n+i*blocksize+k1]; 
                      B_blokk[k2*blocksize+k1]=B_oszlopfolytonos[(j*blocksize+k2)*n+i*blocksize+k1];  
                   }  
                } 
                JMessage mes1 = new JMessage(env); 
                mes1.pack(A_blokk); 
                env.pvm_send(mes1,i*m+j+1,i*m+j+1); 
                JMessage mes2 = new JMessage(env);  
                mes2.pack(B_blokk);  
                env.pvm_send(mes2, i*m+j+1, i*m+j+1); 
                //elküldi az összes processznek az A és B mátrixot  
             }  
          } 
          System.out.println("blokkok elküldve");  
          env.pvm_barrier("szorzo_csoport", m*m+1); 
          int szamlalo=m*m; 
          while (szamlalo!=0) { 
             //hány processznek kell még befejeznie 
             JMessage m=env.pvm_recv();  
             //jött egy üzenet  
             int i=m.upkint(); 
             int j=m.upkint(); 
             int[] C_blokk_resz=new int[blokkhossz]; 
             m.unpack(C_blokk_resz,0,blokkhossz); 
             for (int k2=0; k2<blocksize; k2++){ 
                for (int k1=0;k1<blocksize;k1++){ 
                   C_oszlopfolytonos[(j*blocksize+k2)*n+i*blocksize+k1]= C_blokk_resz[k2*blocksize+k1]; 
                } 
             } 
             szamlalo--; 
          }  
          for (int j=0; j<n; j++){ 
             for (int i=0; i< n; i++ ){ 
                C[i][j]=C_oszlopfolytonos[j*n+i];  
             } 
          } 
          System.out.print("A*B= \n"); 
          for (int i=0; i<n; i++){ 
             for (int j=0;j<n; j++){ 
                System.out.print(C[i][j]+" "); 
             } 
             System.out.print("\n");  
          } 
          env.pvm_lvGroup("szorzo_csoport"); 
       } catch (JException e){ }  
    } 
    public void children(){ 
       try{ 
          env.pvm_joinGroup("szorzo_csoport"); 
          System.out.println("child barrier"); 
          env.pvm_barrier("szorzo_csoport",m*m+1); 
          /*saját ID és a sor oszlop meghatározása*/ 
          int sajatID=env.pvm_me()-1; 
          int row=sajatID/m;  
          int col=sajatID %m; 
          /*szomszédok meghatározása*/ 
          int up; 
          if (row==0){  
             up=(m-1)*m+col+1;  
          } else up=(row-1)*m+col+1; 
          int down; 
          if (row==m-1){ 
             down=0+col+1; 
          } else down=(row+1)*m+col+1; 
          /*vesszük az inicializáló blokkokat*/ 
          JMessage uzenet1=env.pvm_recv(0, env.pvm_me());  
          int[] A_blokk=new int[blokkhossz];  
          int[] B_blokk=new int[blokkhossz];  
          uzenet1.unpack(A_blokk,0,blokkhossz); 
          JMessage uzenet2=env.pvm_recv(0, env.pvm_me());  
          uzenet2.unpack(B_blokk,0,blokkhossz); 
          env.pvm_barrier("szorzo_csoport", m*m+1);  
          int[] C_blokk=new int[blokkhossz]; 
          int[] C_blokk_uj=new int[blokkhossz]; 
          for (int blokkiteracio=0;blokkiteracio<m; blokkiteracio++){ 
             /*épp ő küldi az A blokkot */  
             if (col==(row+blokkiteracio)%m ) {  
                JMessage uzenet3=new JMessage(env); 
                uzenet3.pack(A_blokk); 
                for (int i=0; i<m;i++){ 
                   if (sajatID!=(sajatID/m)*m+i){ 
                      env.pvm_send(uzenet3, (sajatID/m)*m+i+1,ATAG); 
                      System.out.println("A blokk elküldve"+env.pvm_me()+" "+i);  
                   } 
                } 
                C_blokk_uj=MatrixSzorzas(A_blokk,B_blokk, blocksize );  
                System.out.println("uj blokk"+env.pvm_me());  
                for (int j=0; j<blocksize; j++){ 
                   for (int i=0;i<blocksize; i++){ 
                      System.out.print(C_blokk_uj[i*blocksize+j]+" "); 
                   } 
                   System.out.print("\n"); 
                }  
             } else{  
                int[] A_blokk_temp=new int[blokkhossz];  
                JMessage uzenet4=env.pvm_recv();  
                uzenet4.unpack(A_blokk_temp,0,blokkhossz);  
                System.out.println("A blokk vétele"+env.pvm_me()); 
                C_blokk_uj=MatrixSzorzas(A_blokk_temp,B_blokk, blocksize ); 
             } 
             env.pvm_barrier("szorzo_csoport", m*m);  
             /*B oszlopok forgatása*/ 
             JMessage uzenet5=new JMessage(env); 
             uzenet5.pack(B_blokk); 
             env.pvm_send(uzenet5, up,BTAG); 
             env.pvm_barrier("szorzo_csoport", m*m); 
             JMessage uzenet6=env.pvm_recv(down,BTAG); 
             uzenet6.unpack(B_blokk,0,blokkhossz); 
             for (int i=0; i<blokkhossz; i++){ 
                C_blokk[i]=C_blokk[i]+C_blokk_uj[i];  
             } 
          }  
          JMessage uzenet7= new JMessage(env); 
          uzenet7.pack(row); 
          uzenet7.pack(col); 
          uzenet7.pack(C_blokk); 
          env.pvm_send(uzenet7, env.pvm_parent()); 
          env.pvm_lvGroup("szorzo_csoport"); 
       } catch(JException e){  
          System.out.println (e.toString ()); 
       } 
    } 
    public int[] MatrixSzorzas(int[] a, int[] b, int length){  
       int[] c=new int[length*length]; 
       for (int j=0; j< length ; j++){ 
          for (int i=0; i< length; i++){ 
             for (int k=0; k< length; k++){  
                c[j*length+i]=c[j*length+i]+(a[k*length+i]*b[j*length+k]);  
             } 
          }   
       } 
       return c;  
    } 
  } 

A program futtatása után a következőket kapjuk:

A program futását megtekinthetjük a következő animációban is: mmult2.jcluster.swf.

Feladatok:

1. Készítsük el a Multi-Pascal rendszerben megismert rekurzív sorozat elemeit meghatározó (Fibonacci) programot a Jcluster felhasználásával!

2. Készítsük el a PVM rendszerben megismert hővezetés egyenletét megoldó (heat.c és heatslv.c) programot a Jcluster felhasználásával! Az adatok alapján készítsük el a grafikonokat is!

3. Készítsük el a Multi-Pascal rendszerben megismert négyzetgyökvonás (ParallelSqroot) programot a Jcluster felhasználásával!

4. Készítsük el a PVM rendszerben megismert forkjoin (forkjoin.c) programot a Jcluster felhasználásával!

Irodalomjegyzék

[1] Bruce P. Lester. The Art of Parallel Programming. Prentice Hall, Englewood Cliffs. 1995.

[2] Al Geist, Adam Beguelin, Jack Dongarra, Weicheng Jiang, Robert Manchek, Vaidy Sunderamarga. PVM: Parallel Virtual Machine. MIT Press. 1994.

[3] Iványi Antal,. Párhuzamos algoritmusok. ELTE Eötvös Kiadó, Budapest. 2010.