S-114.240 Laskennallisen tekniikan seminaari
Rinnakkaislaskenta: Jaetunmuistin koneet, säikeet
jouni.juujärvi@hut.fi
15.4.1999
Esitelmä perustuu Wilkinson & Allen, Parallel Programming kirjaan,
Mika Julkusen erinomaiseen graduun: Prosessit ja Säikeet rinnakkaisohjelmoinnissa,
www.numeric-quest.com/lang/multi-frame.html
web sivuun säieohjelmoinnista Linuxissa ja BeOS newsletter artikkeleihin.
Esitelmä on jaoteltu seuraavasti: Ensin luodaan nopea silmäys
yleisimpään moniprosessori konearkkitehtuuriin, sen jälkeen
käydään pikaisesti läpi säie ohjelmoinnin kehittyminen
rinnakkaisista prosesseista. Selvitään säie ohjelmointiin
liittyviä käsitteitä, käydään läpi säie
ohjelmointia Linuxissa ja lopuksi vertaillaan säieohjelmoinnin hyviä
ja huonoja puolia.
1. Jaetun muistin koneet
Rinnakkaislaskentajärjestelmät voi karkeasti jakaa kahteen eri
luokkaan. Jaetun muistin koneisiin ja viestinvälitystekniikkaan perustuviin
monen koneen järjestelmiin. Jaetun muistin koneissa kaikki prosessorit
voivat lukea ja kirjoittaa samaan muistiavaruuteen. Halvat ja yleisimmät
jaetun muistin koneet perustuvat samaan arkkitehtuuriin kuin yhden prosessorin
koneet. Tässä mallissa on yksi väylä jossa on kiini
kaikki prosessorit ja muisti (kuva 1).
Kuva 1. PC arkkitehtuuriin perustuva moniprosessori kone
Tämä arkkitehtuuri on toimiva ainoastaan kohtuullisen pienille
prosessori määrille luokkaa 4-8. Tämä johtuu siitä
että väylää pystyy käyttämään vain
yksi prosessori kerralla. Prosessorien määrän kasvaessa
väylä menee vähitellen tukkoon. Se miten nopeasti tämä
tapahtuu riippuu välimuistin määrästä prosessoreissa,
ajettavasta ohjelmasta ja väylän kapasiteetista. Intelin alustalla
tämä raja saavutetaan noin 4 prosessorin kohdalla.
PowerPC & Mac puolella ensivuonna ilmestyvät G4
prosessorit
ja Maxbus arkkitehtuurissa väylälle mahtuvien prosessorien määrän
pitäsi olla huomattavasti suurempi(www.macosrumors.com).
Siinä prosessorien välissä on prosessorin kanssa samalla
kellotaajuudella toimiva väylä. Sen lisäksi prosessorit
jakavat välimuistin keskenään jolloin prosessorien määrän
kasvaessa välimuistin määrä kasvaa myös. Tällöin
muistiin viittauksien määrä ei kasva yhtä nopeasti
kuin perinteisessä tekniikassa.
1.2 Ohjelmointi
Rinnakkaisten prosessien luominen on perinteisin tapa ohjelmalle hyödyntää
koneen useampaa prosessoria saman aikaisesti Unixissa. Tämä on
perinteisesti tapahtunut fork-join komentojen avulla missä prosessista
on muodostettu kopio. Äitiprosessi ja lapsiprosessi tunnistavat itsensä
fork kutsun paluuarvon avulla ja voivat sen jälkeen ryhtyä suorittamaan
omaa osaa ohjelmasta. Molemmat prosessit omistavat oman muistiavaruutensa
ja prosessien välinen kommunikointi tapahtuu yleensä putkien
ja socketien avulla. Näiden alustus hoidetaan yleensä ennen lapsiprosessin
luomista. Prosessien luominen vie paljon CPU aikaa koska siihen liittyy
paljon muitakin ominaisuuksia kuin pino ja rekisteriarvot. Samasta syystä
myös niiden tuhoaminen ja prosessista toiseen vaihtaminen on aika
raskaita operaatioita. Lisäksi niiden välinen kommunikointi on
hankalaa ja resursseja kuluttavaa.
Näitä ongelmia helpottamaan ja suoritusta nopeuttamaan kehitettiin
säikeet. Saman prosessin sisällä säikeet jakavat saman
muistiavaruuden ja kaiken muun paitsi pinon ja prosessorien rekisterien
arvon. Koska jokaisella säikeellä on oma pinonsa kaikki dynaamisesti
luodut oliot ovat kunkin säikeen omistuksessa. Pinojen sisältöön
tosin pääsee toisista säikeistä käsin myös
käsiksi jos säie välittää tarvittavat osoitteen
pinossa olevaa muuttujaansa. Säikeen prosessien välistä
eroa on kuvattu kuvasssa 2.
Kuva 2. Prosessien ja säikeiden välinen suhde
Säikeen luominen voi olla yli kymmenen kertaa nopeampi toimenpiden
kuin prosessin luominen. Uutta säiettä luotaessa saman prosessin
sisällä tarvitsee luoda vain uusi pino ja paikka säikeen
tarvitsemille prosessorin reikisteri arvoille. Näihin rekisteri arvoihin
talletetaan aina prosessorin tila keskeytyksen yhteydessä jotta suoritusta
voidaan jatkaa myöhemmin. Myös säikeestä toiseen säikeeseen
vaihtamisen pitäisi sujua nopeammin kuin prosessista toiseen. Saman
prosessin sisällä ei ytimen tarvitse ajettavan säikeen valitsemisen
jälkeen (ja eräiden muiden toimenpiteiden jälkeen) tehdä
muuta kuin ladata uudet rekisterit prosessoriin. Tämä nopeus
ero ei tosin ei päde Linuxissa jossa säikeet ja prosessit käsitellään
samalla schedulerilla ja säilytetään samassa taulussa. Linux
schedulerin toteutus on kuitenkin hyvin nopea (ja yksinkertainen) jolloin
tästä ei ole kovin suurta haittaa.
2. Säikeet
2.1 Määritelmiä
Silloin kun ohjelma tai kirjasto ei ole suunniteltu käytettäväksi
kuin yhden suoritettavan säikeen kanssa kutsutaan yksisäikeiseksi.
Tällaista kirjastoa voidaan käyttää monisäikeisestäkin
ohjelmasta, mutta silloin pitää huolehtia siitä että
vain yksi säie kerrallaan suorittaa kutsuja kirjastoon.
Ne ohjelmat joissa on useampi säikeitä rinnakkain suorituksessa
ja kirjastoja joita voi useampi säie suorittaa kutsutaan monisäikeisiksi.
Kirjastoja kutsutaan tällöin myös säie turvallisiksi.
Säikeet luodaan usein joko käyttäjätasolla tai ytimessä.
Käyttäjätasolla luodut säikeet jakavat tällöin
prosessin saaman CPU ajan. Niiden hyötynä on nopeampi vaihtaminen
kuin ytimen tason säikeillä, mutta haittana on useamman prosessorin
hyödyntämisen menettäminen ja jos joku säie jotuu odottamaan
I/O tapahtumaa niin silloin myös muut säikeet jäävät
odottamaan. Ytimentason säikeet ovat rinnakkaislaskentaan sopivia
koska saman prosessin ytimen tason säikeet voidaan allokoida eri prosessoreille.
Käyttäjätason säikeet eivät näy ytimelle
joten se ei osaa allokoida niitä eri prosessoreille.
Säieohjelmoinille on IEEEssä määritelty standardi
POSIX standardi 1003.1c jota useimmat Unix valmistajat noudattavat. Esim
-Sun Solaris 2.5, Digital Unix 4.0, Silicon Graphics Irix6, IBM, HP ja
Linux. Standardista tosin osa poikkeaa tai ei ole toteuttanut sitä
ihan kokonaan, mutta ohjelmat on kuitenkin hyvin siirrettävissä
alustalta toiselle rajapinnan perusteella.
Linux säiekirjastoja on useampia joista suurinosa toimii käyttäjätason
säikeillä. Tässä esitemässä kuitenkin keskitytään
kirjastoon nimeltä LinuxThreads mikä toimii ytimentason säikeillä
ja näin ollen soveltuu monen prosessorin samanaikaiseen hyödyntämiseen.
2.2 Ohjelmointi Linuxissa (Posix 1003.1c:ssä)
2.2.1 Luominen
Mikä tahansa säie voi luoda uuden säikeen funktio kutsulla
pthread_create.
Tällöin
uusi säie aloittaa suorittamaa kutsussa määriteltyä
funktiota rinnakkaisesti aikaisemnnin määritellyn funktion
kanssa.
Uutta säiettä luotaessa sille annetaan parametrina thread
attirbute. Se voi olla vasta luotu tai sitten samaa oliota voi käyttää
useamman kerran. tällöin täytyy huomata että kutusun
jälkeen tehdyt muutokset ei vaikuta jo luotuun säikeeseen. Jos
funktiokutsussa annetaan NULL pointteri olion sijasta niin silloin
säie luodaan oletusarvoilla.
Säikeellä on seuraavat ominaisuudet
-
Tila: Liitettävissä, irrallinen. Liitettävissä on oletus
arvoja jolloin säie sykronoidaan emosäikeen kanssa pthread_join
funktion avulla.
-
Schedulointi tapa: tavallinen (ei reaaliaikainen), reaaliaikainen round-robin
ja reaaliaikainen FIFO.
-
Schedulointi parametrit: Käytössä lähinnä prioriteetti
numero jolla on merkitystä vain reaaliaika tapaa noudattavilla säikeillä.
-
Periytyvyys: peritäänkö schedulointi tapa yms parametrit
vanhemmilta. Oletusarvo on että ei.
-
Toiminta alue: Järjestelmä laajuinen (oletus) tai prosessi laajuinen.
Prosessilaajuisuutta ei ole toteutettuna Linuxksissa koska sitä ei
voi toteuttaa ilman muutoksia ytimeen.
2.2.2 Schedulointi
Schedulointi on ytimen osa joka päättää mikä ajettavissa
oleva säie/prosessi suoritetaan CPUssa seuraavaksi. Linuxissa sama
scheduler käsittelee sekä säikeet että prosessit.
Scheduloija tarjoaa kolmea erillaista schedulointi tapaa, yhtä
tavallisille prosesseille ja kahta muuta reaaliaikaisille prosesseille.
Pysyvä prioriteetti arvo sched_priority on annettuna kullekkin
prosessille ja tätä arvoa voi muuttaa vain systeemikutsulla.
Tämä arvo on luku välillä 0 - 99. Scheduloija valitsee
ajovalmiista prosesseista suurimman luvun aina seuraavaksi ajettavaksi.
Schedulointi tapa vaikuttaa siihen miten uusi ajettava prosessi sijoitetaan
samanarvoisten prosessien listalla ja miten sitä liikutellaan tämän
listan sisällä.
SCHED_OTHER on oletus tapa jota suurin osa prosesseista
käyttää. Tällöin prioriteetti on asetettuna 0:n.
SCHED_FIFO
ja SCHED_RR on tarkoitettu vain reaaalikaisille prosesseille ja
niiden proriteeti arvot on välillä 1- 99. Vain ne prosessit
joita ajetaan superkäyttäjän oikeuksilla voivat saada
reaaliaika prosessin arvoja.
Linuxissa schedulointi parametreja voi lukea ja kirjoittaa funktio kutsuilla
pthread_setschedparam
ja pthread_getschedparam.
Kannattaa myös huomioida että Posix standardi ei takaa mitään
säikeiden suorituksen reiluudelle ja ei myöskään Linux
joka voi sopivissa olosuhteissa selvästi suosia joitain säikeitä
toisten kustannuksella.
2.2.3 Lopettaminen
Säikeiden ajaminen voidaan lopettaa usealla tavalla:
-
palaamalla normaalisti säie funktiosta return lauseella.
-
kutsumalla funktiota pthread_exit koodista käsin.
-
hyväksymällä toisesta säikeestä funktiolla pthread_cancel
lähetetty lopetuspyyntö.
-
reagoimalla koko prosessille lähetettyyn SIGKILL tai SIGINT
signaaliin.
-
suorittamalla joku exec alkuinen funktio kutsu jostain samaan prosessiin
kuuluvasta säikeestä. Koska linux toteutus poikkeaa tässä
posix standardista siinä että exec ei välttämättä
tapa kaikkia saman prosessin säikeitä kannattaa ennen exec
funktio-kutsua kutsua funktiota pthread_kill_other_threads_np.
2.2.3.1 Exit
Säie lopettaa oman toimintansa kutsumalla pthread_exit funktiota.
Tällöin kaikki säikeelle määritetyt siivous funktiot
(pthread_cleanup_push) suoritetaan käänteisessä järjestyksesta
aloittaen viimeiseksi talletetusta. Sen jälkeen suoritetaan lopeutus
funktion ei NULL arvoisille data alkioille jotka on luotu kutsulla pthread_create.
Lopuksi säikeen suoritus lopetetaan.
2.2.3.2 Cancellation
Mikä tahansa säie voi lähettää keskeytys pyynnön
toiselle säikeella jos se haluaa lopettaa toisen säikeen samalla
tavalla kuin keskeytettävästä olisi kutsuttu pthread_exit(PTHREAD_CANCELED)
fuktiota.
Keskeytettävä säie voi olla huomioimatta pyyntöä,
toteuttaa sen heti tai vasta kun se saavuttaa keskeytyspaikan riippuen
säikeen asetuksista:
-
Keskeytys tila.
-
Keskeytys pyynnön vastaus tyyppi (asetettavissa komennolla pthread_setcanceltype),
siirretty (oletus) tai välittömästi.
Keskeytyspisteet on paikkoja jossa tarkistetaan onko keskeytyspyyntöä
lähetetty säikeelle ja jos on niin se suoritetaan . Seuraavat
funktiot toimivat keskeytyspaikkoina Posix:ssa & Linuxissa:
-
pthread_join
-
pthread_cond_wait
-
pthread_cond_timedwait
-
pthread_testcancel
-
sem_wait
-
sigwait
Mikään muun säikeisiin liittyvä funktiokutsu ei Posixissa
voi olla keskeytys paikkana, mutta systeemikutsut ja niitä puolestaan
kutsuvat funktiokutsut voivat esim read, write, wait, fprintf. Näistä
mikään ei toimi keskeytyspaikkana toistaiseksi Linux:ssa koska
ydin ei tue tämän ominaisuuden toteuttamista.
2.2.3 Siivoaminen
Siivouskahvat on osittimia funktioihin joita kutsutaan siinä vaiheessa
kun säikeen suoritusta ollaan keskeyttämässä joko kutsumalla
pthread_exit
tai pthread_cancel funktiolla. Siivousfunktioita kutsutaan käänteisessä
järjestyksessä eli viimeksi asetettu kutsutaan ensin.
Siivouskahvojen tarkoituksena on vapauttaa ne resurssit joita säikeellä
on sillä hetkellä kun sitä ollaan keskeyttämässä.
Esim jos säie lopetetaan siinä vaiheessa kun sillä on mutex
lukittuna se jää pysyvästi lukkoon. Tämä voidaan
välttää parhaiten asettamalla siivouskahvaksi pthread_mutex_unlock
joka
vapauttaa lukon kutsuttaessa.
Siivouskahvoja asetetaan ja poistetaan kutsuilla pthread_cleanup_push
ja pthread_cleanup_pop. Nämä kutsut pitäisi aina
esiintyä pareina samalla tavalla kuin muistinvaraamis ja -vapauttamis
kutsut. Linuxissa tämä voidaan toteuttaa makrona jossa aaltosulun
alkuun tuleen push ja loppuun pop.
Linuxista löytyy myös Posix standardista poikkeavasti seuraavat
makrot:
-
pthread_cleanup_push_defer_np
-
pthread_cleanup_pop_defer_np
2.2.4 Synkronisointi
Koska säikeet jakavat paljon resursseja saman prosessin sisällä
niiden täytyy toimia yhteistyössä toistensa kanssa.
Tämä on ongelma silloin kun useampi säie samanaikaisesti
käsittelee jotain oliota muuttaen sen tilaa. Tällöin toiset
säikeet voivat muuttaa olion tilaa kesken ensimmäisen säikeen
suorittamista.
On tietenkin mahdollista että säikeet eivät käytä
mitään yhteisiä resursseja päällekkäin jolloin
synkronisointia ei tarvita. Tämä ei kuintenkaan toteudu usein
vaan yleensä joitan resursseja pitää jakaa. Synkronisointitapoja
on useita joita on selitettynä tarkemmin alla.
Liittäminen
Säie voi lopettaa toimintansa kunnes toinen säie on lopettanut
suorituksensa. Tämä tehdään funktiokutsulla pthread_join.
Jos toinen säie on määritelty liitettäväksi se
vapauttaa resurssinsa vasta tämän kutsun jälkeen.
Mutex
Mutex on lukko joka estää kahta säiettä pääsemästä
kriittiseen kohtaan samanaikaisesti. Sillä on kaksi tilaa. Lukitsematon
(jollon kukaan säie ei omista sitä) ja lukittu. Vain yksi säie
kerrallaan pystyy lukitsemaan sen. Säie joka yrittää saada
lukkoa joka on jo lukittuna jää odottamaan kunnes lukko avataan
sen varanneen säikeen toimesta. Lukko pitää alustaa ennen
kuin se voidaan ottaa käyttöön ja useapi säie ei saa
alustaa sitä samanaikaisesti. Alustusta ei myöskään
saa suorittaa kun se on käytössä toisen säikeen toimesta.
Linuxissa on käytössä kahdenlaisia Mutexeja.
-
Sandardin mukainen nopea mutex.
-
Rekursiivisesti kutsuttavissa oleva mutex.
Rekursiivisesti kutsuttavissa olevaa mutexia sama säie voi yrittää
lukita uudelleen jäämättä jumiin.
Mutexsia voi yrittää lukita kutsulla pthread_mutex_lock
ja sen voi vapauttaa kutsulla pthread_mutex_unlock. Rekursiivisessa
tapauksessa säikeen pitää vapauttaa lukko yhtämonta
kertaa kuin se on lukittuna. Säikeen lukitsemista voi yrittää
myös kutsulla pthread_mutex_trylock. Tämä kutsu ei
jää odottamaan jos säie on varattuna vaan kertoo lukituksen
onnistumisen paluuarvona.
Ehdot
Ehtojen avulla voi joku säie pysäyttää toimintansa
kunnes se uudelleen käynnistetään. Operaatiot voi jakaan
kahteen osaan:
-
Odotetaan kunnes saadaan uudelleen käynnistys käsky toisen säikeen
toimesta. Tällaisia kutsuja on kahdenlaisia pthread_cond_wait
ja pthread_cond_timedwait. Jälkimmäisessä kutsussa
asetetaan aikaraja mikä suostutaan odottamaan.
-
Käynnistetään pysähtynyt säie. Jos halutaan käynnistää
vain yksi ehtoa odottava säie niin silloin käytetään
funktiota pthread_cond_signal. Jos taas halutaan käynnistää
kaikki ehtoa odottavat säikeet niin silloin käytetään
funktiota pthread_cond_broadcast.
Yllämainittuja funktioita pitää käyttää lukkojen
avulla välttääkseen tilanteita joissa samaan aikaan kun
ensimmäinen säie valmistautuu odottamaan ehdon toteuttamista
toinen säie laukaisee ehdon jättäen ensimmäisen säikeen
odottamaan.
Kannattaa myös huomioida että jos vain yksi säie herätetään
ja odottamassa on useampia säikeitä niin ei ole mitään
määrättyä järjestystä säikeen valinnassa.
Ehto muuttujaa ei saa useampi säie alustaa samanaikaisesti ja sitä
ei myöskään luonnollisesti saa alustaa samalla kun joku
säie jo käyttää sitä. Alustus tehdään
funktiolla pthread_cond_init. Se saa argumentiksi Posix standardissa
määritellyn cond_attr mutta tällä argumentilla
ei ole mitään merkitystä Linux toteutuksessa. Ehto muuttuja
tuhotaan komennolla pthread_cond_destroy.
Tällöin ei tietenkään
mikään säie ei saa enään olla odottamassa ehdon
toteutumista. Linux toteutuskessa tosin ehtomuuttuja ei varaa mitään
resursseja joten sen tuhoaminen ei tee mitään muuta kuin tarkistaa
ettei mikään säie ole odottamassa sen toteutumista.
Readers/Writers Locks
Tämä käsite toimii vain Solaris-ympäristössä.
Se on hitaampi kuin mutex mutta toimii paremmin usean lukijan tapauksessa.
Tällöin se antaa monen lukijan päästä käsiksi
dataan jos kirjoittaja ei ole varannut sitä itselleen.
Semaphorit
Semaphorit on hyvin samankaisia mutex:n kanssa. Niitä on kahdenlaisia.
Binary semaphorit eroavat mutexista vain siinä että semaphorin
vapauttava säie voi olla eri kuin sen varannut. Counting semaphorin
avulla voidaan tehdä samoja asioita kuin ehtojen avulla mutta monesti
helpommin. Tosin pitää huomioida että mutexin ja ehtojen
avulla suojattu osa näkyy aaltosulkujen sisällä jolloin
rakenne on helppo hahmottaa. Semaphoreilla rakenne ei näy niin selvästi
ja niitä pystyy muutenkin käyttämään hyvin kontrolloimattomalla
tavalla josta on vaikea ottaa selvää. Semaphoreja käytetään
yleensä kun jotain resursseja on rajallinen määrä jolloin
jokainen varaus vähentää arvoa(semaphorin==vapaiden resurssien
määrä) yhdellä ja lopulta se säie joka yrittää
allokoida nollasta jää odottamaan semaphorin vapautumista. Koska
semaphorit on monimutkaisempia kuin mutex niin ne on myös vähän
raskaampia operaatioita.
Benaphorit
BeOS käyttöjärjestelmässä on keksitty uusi tapa
keventää binäärisemaphorien käyttöä.
Se vaatii käyttöjärjestelmältä tukea sen verran
että järjestelmän on tuettava atomista lisäys
ja arvon tarkistusfunktiota. Tämän funktion toteutus on huomattavasti
kevyempi kuin semaphorin varaamisen tarkistaminen. Toiminta idea on varata
semaphori etukäteen ja vain kilpailutilanteessa käyttää
varattua semaphoria. Alla toimintaa valaiseva koodi esimerkki:
while(true){
if(atomic_add(&g_lock,1)>0)
accuire_sem(g_ben);
do_critical_section();
if(atomic_add(&g_lock,-1)>1)
release_sem(g_ben);
}
BeOSssä atomic_add kestää 1/10 semaphorin tarkistus ajasta
joten jos usein toistuvassa varaustilanteessa jossa ei läheskään
aina ole kilpailutilanne ajan säästö voi olla huomattava.
Koska Linux on vapaasti koodattavissa on siinä mahdollista kirjoittaa
myös atomic_add funktio ja käyttää sen avulla myös
benaphoreja.
Monitorit
Javassa on pyritty helpottamaan rinnakkaisten säikeiden käyttöä.
Siinä suojataan tarvittava koodin osa kirjoittamalla funktion esittelyssä
avain sana syncronized. Tällöin vain yksi säie kerrallaan
pääsee suorittamaan funktiota. Monitoreja on myös käytössä
joissain muissa järkestelmissä mutta ne eivät ainakaan toistaiseksi
kuulu Posix 1003 standardiin eikä myöskään Linux kirjastoon.
Ne ilmeisesti kuuluvat ohjelmointi kielen tasolle joten jos niitä
haluttaisiin käyttää ne pitäisi sisällyttää
suoraan C++:n ja kääntäjiin.
2.2.5 Datan välitys & erikoisfunktiot
Koska säikeet jakavat saman muistiavaruuden niiden välinen datan
välitys ei ole ongelma. Jos säikeet haluavat muuttujia joilla
on eri arvot eri säikeillä ne luodaan erikoisfunktioilla. Funktiot
ovat seuraavat:
-
pthread_key_create, luodaan avain.
-
pthread_key_delete, t uhotaan avain (mutta ei avaimen viittaamaa
arvoa).
-
pthread_setspesific, talletetaan pointteri avaimeen.
-
pthread_getspesific. luetaan pointteri avaimesta.
Once
pthread_once funktio saa argumentteina funktio ja once_control
muuttuja. Funktio kutsuu saatua funktiota jos samalla once_control
muuttujalla ei pthread_once funktiota ole kutusuttu aikaisemmin.
Tämän kutsun tarkoituksena on helpottaa sellaisten muuttujien
alustamista mitkä on tarkoitettu alustettavaksi vain kerran.
2.2.6 Signaalit
Kekeytykset on tietokoneen laitetason tapahtumia jotka vaativat prosessoria
suorittamaan jonkun ennalta määrätyn keskeytys rutiinin.
Rutiinin suoritettuaan prosessori palaa suorittamaan keskenjäänyttä
ohjelmaa. Signaalit on keskeytysten vastineita ohjelma tasolla. Ne ovat
ohjelmatasolla syntyviä tapahtumia, jotka saavat vastaanottavan prosessin
suorittamaan käsittelijän signaalille ja suorituksen päätyttyä
palaamaan takaisin keskeytyneen prosessin suorittamiseen. Koska säikeet
jo antavat mahdollisuuden asynkroniseen toimintaan niin säie ohjelmassa
kannattaa luoda yksi säie sopivalla maskilla jonka tehtävänä
on käsitellä kaikki halutut signaalit. Tällöin säikeessa
kutsutaan sigwait funktiota ja jäädään odottamaan
niiden saapumista. Mitään erillisiä käsittelijä
funktioita ei kannata käyttää.
Signaalikäsittelijät
Näitä on olemassa vain yksi signaalityyppi/prosessi. Näiden
käyttöä ei suositella.
Maskit
Maskien avulla valitaan mitä signaaleja säie suostuu ottamaan
vastaan jos samalla prosessilla on useita säikeitä jotka suostuvat
käsittelemään signaalin niin silloin joku niistä saa
signaalin suoritettavakseen. Kuten yllä on mainittuna kannattaa maskata
kakki signaalit muilta paitsi signaalin käsittelijä säikeelta.
Turvallisuus
Kaikkia posix punktioita ei ole lupa kutsua signaalikäsittelijästä
käsin tämä johtuu siitä että funktion suoritus
saattoi olla kesken kun käsittelijää kutsuttiin. Tämän
takia signaalikäsittelijässä toiminta kannattaa minimoida
lähinnä jonkun flagin arvon muuttamiseksi tai/ja viestien lähettämiseksi
muille säikeille.
SIGUSR1 & 2
Nämä signaalit on varattuna Linuxkirjaston toteutukseen joten
niitä ei voi käyttää.
2.3 Suden kuopat ja muistettavaa
Deadlock
Deadlock syntyy kun säikeen toiminta estyy pysyvästi jonkun resurssin
odottamisen takia. Syitä tälläiseen on monia mutta yleisin
on lukon uudelleen lukitseminen saman funktion sisällä. Tällöin
yleensä funktio on jo lukinnut lukon ja kutsuu jotain toista funktiota
joka joko suoraan tai kutsumalla jotain kolmatta funktiota yrittää
lukita samaa lukkoa uudelleen. Ratkaisuna on välttää olion
ulkopuolelle suoritettavia kutsuja ja jos niitä tarvitseen niin tarvittaessa
vapauttaa lukko kutsun ajaksi ja lukita sitten uudelleen kun kutsu palaa.
Toinen hyvin yleinen deadlock tilanne syntyy siitä että säikeet
odottavat toisiltaan resurssien vapauttamista. Tähän on ratkaisuna
resurssien varaaminen aina samassa järjestyksessä. Jos tähän
sääntöön pitää tehdä poikkeuksia niin
silloin pitää käyttää pthread_mutex_trylock:ia
ja huomioida varaussäännön rikkominen.
Nälkiintyminen
Koska Linux (ja monet muutkin) scheduleri on toteutettu nopeana ja hyvin
yksinkertaisena palveluna se ei takaa mitään reiluudesta. Tämän
takia voi joku säie jäädä melkein aina odottamaan kun
joku toinen säie saa pitää lukkoa hallussaan hyvin pitkiä
aikoja. Tällöin yleensä lukkoa hallussa pitävä
säie kyllä vapauttaa lukon mutta ehtii varata sen melkein aina
uudelleen ennen kuin kukaan muu ehtii varata sen. Ongelma johtuu huonosti
suunnitellusta ohjelmasta ja jos tarvitaan takeita reiluudesta eri säikeiden
välillä niin silloin niitä pitää ajaa reaaliaika
secdulointi määrityksillä (round-robin tai FIFO).
Globaalit muuttujat
Yleensä globaaleja muuttujia ei ole suunniteltu säie turvallisiksi.
Esim ERRNO muuttuja osoittaa viimeksi tapahtuneen virheen syytä.
Säie ohjelmassa sen arvon voi muuttaa joku toinen säie ennen
kuin aikaisemman virheen luonut säie ehtii lukemaan arvon talteen.
Tämä ongelma on korjattu Linux Threads paketissa tekemällä
ERRNO:sta
säikeen sisäinen muuttuja.
Staattinen data
Jotkut ei säie turvalliset funktiot palauttavat osoittimia staattisiin
muuttujiin. Näiden arvoa voi toinen säie muuttaa ennen kuin ensimmäinen
lukee sen talteen. Tämä ongelma voidaan ratkaista mutexilla estämällä
muut kutsut kirjastoon/funktioon ennen arvon lukemista. Tai sitten voidaan
käyttää säie ystävällisiä versioista
jotka yleensä ovat _r loppuisia.
Lukitsemisesta
Lukitseminen tapahtuu joko funktio tai data tasolla. Funktio tasolla lukkoa
pidetään koko funktion suorituksen ajan. Data tasolla sitä
pitdetään vain sen aikaa kun yhteistä muuttujaa/resurssia
käsitellään. Data tason lukitseminen on hienonpi tasoista
ja rinnakkaisuuden puolesta suositeltavampaa, mutta toisaalta yleinen viisaus
on aloittaa ensin karkeampitasoisilla lukoilla tarkastella suorituskyvyn
perusteella missä tarvitaan hienojakoisempia lukkoja.
Muita yleisiä nyrkkisääntöjä on:
-
Älä pidä lukkoja hallussa yli hyvin pitkien operaatioiden
yli (Esim I /O). Tämä laskee suorituskykyä.
-
Älä pidä lukkoja jos kutsut olion ulkopuolisia funktioita.
-
Älä yritä tehdä erittäin rinnakkaista ja yli hienoa
koodia. Noudata KISS periaatetta aina kun voit.
-
Päätä lukkojen varausjärjestys ja toteuta sääntöä.
2.4 Esimerkkejä
Seuraava esimerkki laskeen summan 1:stä n:ään.
int a[array_size];
int gloabal_index=0;
int sum=0;
pthread_mutex_t mutex1;
void *slave(void *ignored){
int local_index, partial_sum=0;
do {
pthread_mutex_lock(&mutex1); //Luetaan seuraava
alkio listasta ja lisätään se paikalliseen summaan
local_index=global_index;
global_index++;
pthread_mutex_unlock(&mutex1);
if(local_index < array_size)
partial_sum+= *(a+ local_index);
}while (local_index<array_size)
pthread_mutex_lock(&mutex1);
sum +=partial_sum;
pthread_mutex_unlock(&mutex1);
return(); //Säie lopettaa toimintansa
}
main(){
int i;
pthread_t thread[10];
pthread_mutex_init(&mutex1,NULL); //Alustetaan mutex
for(i=0;i<array_size;i++) //Talletetaan arvot a:han
a[i]=i+1;
for(i=0;i<no_prosesses;i++) //Luodaan työn tekevät
säikeet
if(pthread_create(&thread[i],NULL,slave,NULL)=!0)
perror("Pthread_create fails");
for(i=0;i<no_prosesses;i++) //Odotetaan kunnes kaikki
säikeet on päässeet loppuun
if(pthread_join[i],NULL)!=0)
perror("Pthread_join fails");
printf("The sum of 1 to %i is %d\n",array_size,sum);
}
Lisää esimerkkejä löytyy Linkkilistassa mainitusta
www.numeric-quest.comin
linkistä.
3 Summa summarum
3.1 Säie ohjelmoinnin hyödyt ja haitat
-
+ Parantaan sovelluksen käyttöliittymän toiminnallisuutta.
Käyttöliittymän ei tarvitse mennä mykäksi jos
sovellus alkaa suorittamaan jotain aikaa vievää funktiokutsua.
-
+ Pystyy hyödyntämään aitoa rinnakkaisuutta moniprosessori
koneessa.
-
+ Käyttää koneen resursseja taloudellisesti. Säikeiden
välinen schedulointi on nopeampaa ja prosessin sisäisten säikeiden
yhteisestä muistista johtuen säikeiden välinen kommunikointi
on nopeampaa.
-
+ Parantaa ohjelman rakennetta. Tämä siinä tapauksessa että
ohjelmassa on kohtia jotka vaativat rinnakkaisuutta.
-
- Ohjelmointi haasteellisempaa kuin perinteinen. On paljon uusia tapoja
tehdä vaikeasti havaittavia ja ei helposti toistettavia bugeja.
3.2 Jaetun muistin koneet & säikeet / hajautetun muistin
koneet & sanomien lähettäminen
Se että kannattaako käyttää jaetun muistin koneita
ja säikeitä vaiko hajautetun muistin koneita ja sanomja kirjastoa
riippuu aika pitkälle ongelman luonteesta. Tällä hetkellä
halpoja jaetunmuistin koneita saa vain aika pienellä prosessorimäärällä
varustettuna.
2 prosesorinen PC tulee halvemmaksi kuin 2 erillisen koneen osto ja
niiden muodostaman verkon luominen. 4 prosessorinen PC ja 4 erillisen PC:n
hinnat liikkuvat samoissa. Jos taas tarvitaan enemmän laskenta tehoa
niin siinä vaiheessa pitänee käyttää jotain muuta
kuin PC alustaa ja hinnat nousevat jyrkästi.
Toisaalta jos ajatellaan säikeiden välistä kommunikointinopeutta
eri prosessorien välissä niin se on teoriassa sama kuin väylänopeus
joka on PCssä tällä hetkellä 100MHz x 32 bittiä.
Beowulfklusteri joka on kytketty 100Mhz Ethernet korteilla saavuttaa maksimissaan
(Ethrenet verkko saturoituu noin 20% kohdalla) 20MHz x 1bitin siirtonopeuden.
Sen siirtokapasiteetti on siis alle 1/150 osa prosessorien välisestä
kommunikointinopeudesta ja sen lisäksi kommunkointi nopeutta huonotavat
kaikki normaalit lähiverkkoon liittyvät viiveet ja epämääräisyydet.
Mikään ei tietenkään estä käyttämästä
molempia tapoja sekaisin. Toisaalta tulevaisuus riippuu varmasti voimakkaasti
myös siitä mihin suuntaan prosessoriteknologia tulevaisuudessa
suuntautuu. Tilanne muuttuu ainakin siinä tapauksessa selvästi
jos monen prosessorin tekeminen samalla piipalalle yleistyy ja moniprosessori
emolevyt yleistyvät. Tällähetkellä PC puolella taitaa
kyseessä olla 'muna kana' ongelma. Harva ohjelma on suunniteltu ajejttavaksi
moniprosessori koneessa, jolloin moniprosessori koneillakaan ei ole suurta
kysyntää. Toisaalta verkkoteknologia menee myös kokoajan
nopeasti eteenpäin joten tulevaisuudesta on vaikea ennustaa
mitään varmaa.
4. Liitteet
4.1 Linkit
Barry Wilkinson, Michael Allen, Parallel Programming : Techniques
and Applications Using Networked Workstations and Parallel Computers, Prentice
Hall, 1998
Mika Julkunen, Prosessit ja Säikeet Rinnakkaisohjelmoinnnissa Pro
Gradu -tutkijelma 12.2.1997 Helsingin Yliopisto.
www.be.com
www.numeric-quest.com/lang/multi-frame.html