keskiviikko 10. syyskuuta 2008

Monisäikeiset ohjelmat.





olen jo aiemmin blogissani esitellyt pthread kirjaston käyttöä säikeiden luontiin. Mainitsin myös semaforien ja mutexien käytön, sekä näytin muutaman esimerkin syntaxista. Nyt tarkoituksenani on kertoa hieman enemmän aiheeseen liittyvistä sudenkuopista.

Re-entrancy ja thread safety:

Pohditaampa hetkinen monisäikeisten ohjelmien toimintaa, ja muuttujien näkyvyyttä. Monisäie ohjelmoinnissahan useampi koodinpätkä voi olla suorituksessa yhtä aikaa. Pohditaanpa hetkinen mitä tämä merkitsee funktiotasolla. Tehdään jälleen ongelman ytimen selkeyttävä ajatusleikki. Kuvitellaanpa että meillä on funktio, joka keittää kahvia. Kahvinkeitin olkoon muuttuja jota funktiomme käyttää. Nyt meillä on kaksi vaihtoehtoa sille, miten kahvinkeittimemme sijoitamme. Voimme luoda kahvinkeittimen funktion sisällä, jolloin joka kerta kun funktiota kutsutaan, luodaan uusi kahvinkeitin. Tai sitten voimme määritellä kahvinkeittimemme globaalina muuttujana, jolloin kahvinkeittimiä on vain yksi, ja aina kun funktiota kutsutaan käytetään samaa kahvinkeitintä. Mititäänpä nyt hetki monisäikeistä ohjelmaa, kumpikohan kahvinkeitinmalli toimisi paremmin?

Nyt tekisi miei sanoa, että globaali keitin, kukas hullu sitä kahvia niin paljon keittää, ettei yksi keitin riitä? Mutta kun palaamme takaisin ohjelmointiin, huomaamme, että mikäli meillä on vain yksi keitin, ja mikäli kahvinkeittofunktiotamme kutsutaan monesta säikeestä, saattaa eteen tulla tilanne, että säie yksi on juuri keskellä kahvinkeittofunktiota, kun scheduleri päättää antaa ajoaikaa säie 2:lle. Nyt säie kaksi menee kahvinkeittofunktioon, ja ...

Huomaattehan riskin? on mahdollista, että säie yksi oli juuri täyttänyt kahvinkeittimen vesisäiliön, kun säie kaksi pääsi ajoon. Nyt säie kaksi kaataa säiliöön lisää vettä, ja keitin tulvii yli (kumpikin säiehän käytti samaa keitintä). Säie kaksi lisää kahvin, painaa virtanappulaa ja kaataa kahvin kuppeihin. Säie 2 jatkaa muissa tehtävissä, kunnes scheduleri antaa taas ajoaikaa säikeelle 1. Nyt säie yksi muistaa täyttäneensä veden, lisää kahvin ja painaa virtanappulaa. Mutta koska säie kaksi kävi välissä käyttämässä keitintä, vesisäiliö on nyt tyhjä, ja keitin palaa tuhkaksi...

Mietitäänpä miten ongelma voidaan välttää. Yksi keino on käyttää funktion sisäistä kahvinkeitin muuttujaa, jolloin aina kun keittofunktiota kutsutaan, kutsuja saa ihka oman keittimen käyttöönsä. Mutta aina tämä ei ole järkevää. Joskus on tarpeen käpistellä samaa muuttujaa/oliota/tms. useasta threadista. Nyt vaihtoehtona on käyttää edellä esiteltyjä semaforeja/mutexeja. Eli laitetaan keitin lukolliseen huoneeseen, johon on vain yksi avain. Nyt kun säie yksi ryhtyy kahvinkeittopuuhiin, se menee huoneeseen sisälle, ottaa avaimen lukosta, ja lukitsee oven perässään. Mikäli scheduleri antaa kesken kahvinkeiton ajoaikaa säikeelle 2, ja mikäli säie kaksi koettaa ryhtyä kahvinkeittoon, se törmää lukittuun oveen ja joutuu odottelemaan vuoroaan kunnes säie yksi on keittänyt kahvinsa. Säie yksi saa taas ajoaikaa, keittää kahvin valmiiksi, poistuu huoneesta ja jättää mennessään avaimen lukkoon. Nyt säie kaksi pääsee kokkailemaan, lukiten oven huoneeseen mennessään...

re-entrant ja thread safe ovat termejä jotka kuvaavat jonkin funktion soveltuvuutta monisäikeiseen ohjelmointiin. Termeillä on kuitenkin perustavaa laatua oleva ero. ohjelmointi, ja varsinkin C kun ovat ihmeitä täynnä, tuo edellinen lukkoesimerkki ei olekkaan aivan vedenpitävä. Eli lukkoesimerkki on esimerkki thread safe funktiosta, mutta re-entrant se ei ole.

re-entrant termi vaatii nimittäin, että funktio täyttää myös kvanttimekaniikan mukanaan tuomat kummallisuudet... Tai jotain :D Eli re-entrant funktio on sellainen, joka toimii myös tilanteessa jossa säie hyppää kesken jonkin funktion suorituksen suorittamaan samaa funktiota. Eli kahvinkeittoesimerkissämme säie yksi kesken kahvinkeiton tunneloituisi lukitun oven väärälle puolelle, ja pyrkisi uudelleen keittämään kahvia. Tämähän ei toimisi sillä avainhan jäi kahvinkeittohuoneeseen. Sen sijaan paikallista kahvinkeitintä käyttämällä tämä toimisi. Nyt kun säie yksi hyppäisi uudelleen funktion alkuun, se loisi itsellleen uuden keittimen, keittelisi kahvin ja palaisi sitten jatkamaan keittelyä sillä toisella keittimellä...

Mikä C:ssä siis saa moisia kvanttimekaanisia ilmiöitä aikaan?

No perus C:ssä oikeastaan vain rekursiiviset funktiot (eli funktiot jotka kutsuvat itseään). Rekursiivista funktiota rakentaessa ohjelmoija kuitenkin yleensä tietää tilanteen, ja osaa automaattisesti ottaa tällaisen tilanteen huomioon. Mutta tutkitaanpa hieman vaikka posix rajapintaa. (posix = portaple operating system standardi, joka määrittelee usita C-kielen funktioita ja metodeita. Tyypillisiä posix standardin täyttäviä käyttöjärjestelmiä ovat mm. linux ja unix). Posix määrittelee signaalit, joiden voidaan ajatella olevan ohjelmallisia keskeytyksiä. Eli säie määrittää funktion, jonka se suorittaa mikäli tietyntyyppinen keskeytys tulee. Nyt välittömästi kun tällainen signaali generoidaan, säie jättää sen hetkiset tehtävänsä, ja hyppää suorittamaan keskeytyksen käsittely funktiotaan.

Jos nyt jatketaan kahvinkeitto linjoilla, ja mietitään tilanne jossa olemme rekisteröineet kahvinkeittofunktion jonkin tietyn signaalin käsittely funktioksi. Sitten ohjelmamme huomaa janoisen vieraan, ja nostaa signaalin, jolloin säikeemme jolle kahvinkeittokäsittely oli määrätty, hyppää suorittamaan kahvinkeittofunktiota. Kuinka ollakkaan, yht-äkkiä, kesken kahvin keittelyn, ohjelmamme huomaa uuden janoisen vieraan. Keskeytys generoidaan, ja kahvinkeittokäsittelijämme jättää ensimmäisen keittokerran kesken, ja hyppää uudelleen keittofunktion alkuun. Mikäli meillä nyt on semafori tai mutexi suojaamassa globaalia keitintämme, saamme aikaan deadlockin, sillä semafori/mutex on varattuna (ovi on lukittuna), mutta säie jonka tulisi avata mutex/semafori, odottaa nyt kahvinkeittofunktion alussa, lukon takana... Ts. pattitilanne.

Eli, re-entrancy on tietyssä mielessä tiukempi vaatimus kuin thread safety. Ja keskeytysfunktioiden kanssa TULEE olla tarkkana.

Eli summa summarum, ongelmakohtia säikeiden kanssa:

1. saman resurssin käpistely useasta säikeestä. (globaali data, static muuttujat)
2. Deadlock. (Jonkin resurssin odottelu, kun vapauttaja odottelee myös).

Ja vielä.... STANDARDIKIRJASTON MUUTTUJAT EIVÄT PÄÄSÄÄNTÖISESTI OLE REENTRANTTEJA! Eli keskeytysfunktioissa tulisi välttää standardikirjaston funktioiden käyttöä! Myöskään thread safety ei standardikirjaston funktioille ole tavanomaista! Esim malloc ja free on yleensä toteutettu siten, että ne käpistelevät jotain globaalia resurssia jossa pidetään kirjaa varatuista muistiblockeista! Kuitenkin useista standardikirjaston muuttujista on tehty reentrantit/threadsafet versiot, jotka on myös tyypillisesti nimetty _r() ( esim strtok_r(). )

... Lisää ehkä taas joskus :)

Ei kommentteja: