TIES542 kevät 2009 Yhteismuistisamanaikaisuus Antti-Juhani Kaijanaho 9. maaliskuuta 2009 Tehtävät ovat samanaikaiset (engl. concurrent), jos ne etenevät yhtä aikaa samalla toistensa kanssa kommunikoiden. Tehtävät ovat rinnakkaiset (engl. parallel), jos ne etenevät yhtä aikaa toisistaan täysin riippumatta. Kummassakin tapauksessa yhtä aikaa tarkoittaa suoritusta eri prosessoreissa tai eri prosessoriytimissä taikka lomittamalla samassa prosessoriytimessä. Kannattaa tosin huomata, että on varsin yleistä sekoittaa nämä kaksi termiä toisiinsa. Samanaikaisuus on arkipäivää nykyisissä tietojenkäsittelytilanteissa. Arkipäivän henkilökohtaisissa tietokoneissa on jo useita toisistaan (ohjelmoijan näkökulmasta) riippumattomia prosessoriytimiä ja tehokkaammissa palvelimissa on tavallisesti useita prosessoreita. Tietokoneet on kytketty toisiinsa suhteellisen nopeaksi tietokoneverkostoksi, ja erittäin tavallista on, että ohjelma suoritetaan kahden tietokoneen (asiakas nykyisin enimmäkseen WWW-selain ja palvelin) yhteistyönä. Samanaikaisuutta on kahta lajia: on yhteismuistisamanaikaisuutta (engl. sharedmemory concurrency) sekä viestinvälityssamanaikaisuutta (engl. message-passing concurrency). Edellinen on vahvoilla tilanteissa, joissa samanaikaiset tehtävät ovat fyysisesti samassa koneessa ja käyttävät fyysisesti samaa muistia. Jälkimmäinen on luonnollinen valinta silloin, kun tehtävät voivat potentiaalisesti sijaita vain tietoverkon yhdistämissä erillisissä tietokoneissa. Molempia malleja voidaan myös onnistuneesti yhdistää. Ohjelmointikielissä samanaikaisuuden haaste liittyy sen hallinnan abstrahointiin: millä tavalla ohjelmoijalle voidaan tarjota samanaikaisuuden hallintaan vahvat työkalut, jotka vähentävät ohjelmointivirheiden todennäköisyyttä mutta jotka eivät liikaa häiritse ohjelmoijan päätehtävää, eli ohjelmointiongelman ratkaisemista? Monisteen päälähteet ovat Reynolds (1998, luvut 8 ja 9) ja Scott (2000, luku 12). 1
1 Samanaikainen suoritus Yksinkertaisin yhteismuuttujakieli saadaan laajentamalla while-kieltä uudella konstruktiolla s t, joka on lause, jossa lauseet s ja t suoritetaan samanaikaisesti ja molemmilla on pääsy sillä hetkellä näkyviin muuttujiin: s t (s, σ) λ (s, σ ) (s t, σ) λ (s t, σ ) (t, σ) λ (t, σ ) (s t, σ) λ (s t, σ ) (t, σ) λ σ (s t, σ) λ (s, σ ) (s, σ) λ σ (s t, σ) λ (t, σ ) Tämä konstruktio vastaa Algol 68 -kielessä käytössä ollutta par begin -lohkorakennetta. Jokaisella tehtävällä on usein myös tarvetta omille muuttujilleen. Tähän kieleen (kuten myöskin tavalliseen while-kieleen) voidaan varsin yksinkertaisesti lisätä tuki paikallisille muuttujille: v Values {x = e ; s} (s, σ {(x, v)}) λ (s, σ ) ({x = v ; s}, σ) λ ({x = σ (x) ; s }, σ {(x, σ(x))}) 2
(s, σ {(x, v)}) λ σ ({x = v ; s}, σ) λ σ {(x, σ(x))}) (e, σ) (e, σ) ({x = e ; s}, σ) ({x = e ; s}, σ) Tässä joudutaan lyhytaskelsemantiikassa temppuilemaan, jotta paikallinen muuttuja tosiaan pysyy paikallisena eikä esimerkiksi sotke samanaikaista tehtävää. Vastaava, tosin varsin erilainen, temppuilu joudutaan tekemään myös kielen toteutuksissa. Tällainen yhtäaikaisten tehtävien malli vaatii, että molemmat tehtävät tulevat loppuun suoritetuksi ennen kuin suoritus voi jatkua -operaation jälkeen. On myös mahdollista (ja varsin yleistä) luoda taustatehtäviä, joiden päättyminen ei ole tarpeen, jotta suoritus voisi jatkua tehtävän luovan operaation jälkeen. 2 Kriittiset lohkot Näin yksinkertaisella yhteismuuttujakielellä on kuitenkin yksi erittäin iso ongelma: kuinka estetään samanaikaisia tehtäviä astumasta toistensa varpaille? Erityisen iso ongelma se on silloin, kun tehtävien on tarkoitus kommunikoida yhteismuuttujien avulla. Tavallista on, että itse informaatio välitetään eri muuttujassa kuin tieto informaation valmistumisesta ja informaation tulemisesta käytetyksi. Nyt esimerkiksi tuottajatehtävä saattaisi toimia seuraavasti: (while empty do skip); datum ; empty false Tässä skip on lause, joka ei tee mitään. Vastaavasti kuluttajatehtävä toimisi seuraavasti: (while empty do skip); x datum; empty true Niin kauan kuin tuottajia ja kuluttajia on vain kaksi, tämä voi vielä toimia, mutta jos tuottajia tai kuluttajia on useita, voi tilanne muuttua kaaoottiseksi. Yksinkertaisin tapa korjata tilanne on lisätä kieleen tuki atomisille lohkoille (engl. atomic blocks) eli kriittisille alueille (engl. critical regions), joiden suorituksen aikana 3
muut tehtävät on pysäytetty: v Values atomic c c, d CriticalStatement c, d ::= x e c ; d if e then c else d {x = e ; c} (c, σ) σ (atomic c, σ) σ Huomaa, atomisen lohkon sisällä ei sallita samanaikaisuutta, siirräntää eikä silmukoita. 3 Ehdolliset kriittiset lohkot Tämä on tosin liian rajoittunut ratkaisu, sillä kuluttajaa ja tuottajaa ei voi atomisena lohkona kirjoittaa silmukan kiellon takia. Muutenkin tällainen busy-waittekniikka on tietokoneen resursseja tuhlaavaa, ja on parempi lisätä kieleen tuki ehdolliselle atomisuudelle: v Values c, d CriticalStatement await e then c (e, σ) (true, σ) (c, σ) σ (await e then c, σ) σ 4
(e, σ) (false, σ) (await e then c, σ) (await e then c, σ) Käytännön toteutuksessa await-ehdon tuottaessa epätoden tehtävä kirjataan awaitehdon lukemien muuttujien odotusjonoon ja jätetään odottamaan, ja kun jokin toinen tehtävä sijoittaa muuttujaan, se samalla aktivoi kaikki kyseisen muuttujan odotusjonossa olevat tehtävät. Nyt tuottaja voidaan kirjoittaa ja kuluttaja await empty then (datum ; empty false) await empty then (x datum; empty true) Ehdolliset atomiset lohkot riittävät klassisten semaforien toteutukseen. 4 Monitorit Edellä esitettyjen tekniikoiden ongelmana on, että ne ovat varsin matalan tason ratkaisuja. Hansen (1972) ja Hoare (1974) kehittivät rakenteisen ratkaisun, jota sanotaan monitoriksi. Monitorin ideana on, että yleensä olennaista on estää tietyn muuttujajoukon yhtäaikainen käsittely ja että tuolla muuttujajoukolla on vain pieni joukko operaatioita, jotka tarvitsevat suoraa pääsyä joukkoon. Monitorilla on kaksi tilaa: se on joko vapaa tai varattu. Jos se on varattu, niin on olemassa jokin tehtävä, joka on sen varannut. Jos joku tehtävä kutsuu monitorin operaatiota ja monitori on jonkin toisen tehtävän varaama, kyseinen tehtävä jää jonoon odottamaan monitorin vapautumista. Jos monitori onkin vapaa, kyseinen tehtävä pääsee suorittamaan kyseistä operaatiota, ja monitori on varattu niin kauan kuin tuon operaation suoritus kestää. Kun operaatio on valmis, tehtävä vapauttaa monitorin ja jokin jonossa odottavista tehtävistä pääsee jatkamaan suoritustaan. Monitorissa voi olla ehtomuuttujia. Tällöin monitorin varannut tehtävä voi jäädä odottamaan ehtomuuttujaan, jolloin monitori vapautuu ja kyseinen tehtävä 5
jää odottamaan kyseisen ehtomuuttujan jonoon. Jokin toinen tehtävä, pitäessään monitoria varattuna, voi signaloida ehtomuuttujaa, jolloin tämä tehtävä jää odottamaan ja jokin ehtomuuttujan jonossa oleva tehtävä päästetään jatkamaan monitorissa, ja signaloinut tehtävä jatkaa heti monitorin vapauduttua. Joissakin monitoritoteutuksissa signaloiva tehtävä jatkaa välittömästi ja ehtomuuttujassa odottava tehtävä jatkaa heti signaloivan tehtävän vapautettua monitorin. Monitorit on helppo yhdistää oliokielten kanssa, sillä olio voi hyvin toimia monitorina. Javassa synchronized-luokan oliot ovat monitoreita. 5 Transaktiomuisti Viimeisen kymmenen vuoden aikana on kehitetty ohjelmointikieliä, joissa työmuistia käsitellään tietokantaohjelmista tuttujen transaktioiden avulla (Harris et al., 2005). Transaktioiden pääetu on, että niitä voidaan yhdistellä mikään muu yhteismuistin hallintaan kehitetyt menetelmät eivät ole turvallisesti yhdistyvä. Transaktiomuisti tarvitsee kielessä tuen kahdelle konstruktiolle, atomic s, joka sanoo, että s on yksi transaktio, ja retry, joka sanoo, että transaktio epäonnistui ja se on aloitettava alusta. Kielensuunnittelijan kannalta oleellinen haaste on, kuinka kieli yhdistää peruutettavat transaktiot ja peruuttamattomat sivuvaikutukset. Viitteet Per Brinch Hansen. Structured multiprogramming. Communications of the ACM, 15(7):574 578, 1972. ISSN 0001-0782. doi: http://doi.acm.org/10.1145/361454. 361473. Tim Harris, Simon Marlow, Simon Peyton-Jones, and Maurice Herlihy. Composable memory transactions. In PPoPP 05: Proceedings of the tenth ACM SIGPLAN symposium on Principles and practice of parallel programming, pages 48 60, New York, NY, USA, 2005. ACM. ISBN 1-59593-080-9. doi: http://doi.acm.org/10.1145/1065944.1065952. C. A. R. Hoare. Monitors: an operating system structuring concept. Communications of the ACM, 17(10):549 557, 1974. ISSN 0001-0782. doi: http: //doi.acm.org/10.1145/355620.361161. John C. Reynolds. Theories of Programming Languages. Cambridge University Press, Cambridge, 1998. Michael L. Scott. Programming Language Pragmatics. Morgan Kaufmann, 2000. 6