Concurrency - Rinnakkaisuus Group: 9 Joni Laine Juho Vähätalo
Sisällysluettelo 1. Johdanto... 3 2. C++ thread... 4 3. Python multiprocessing... 6 4. Java ExecutorService... 8 5. Yhteenveto... 9 6. Lähteet... 11
1. Johdanto Rinnakkaisuudella (Concurrency) tarkoitetaan kahden tai useamman ohjelman tai niiden osien samanaikaista suorittamista. Tämä on ohjelmoinnissa usein välttämätön ominaisuus, joka mahdollistaa yhden suorittimen ympäristössä useamman ohjelman yhtäaikaisen suorittamisen. Tällöin puhutaan näennäisestä rinnakkaisuudesta, joka tapahtuu vuorottelemalla suoritettavana olevaa ohjelmaa. Rinnakkaisuus voi olla myös aitoa, jolloin ohjelmia tai niiden osia suoritetaan samaan aikaan eri suorittimilla. Usein on myös mahdollista jakaa yksittäisen ohjelman suoritusta eri suorittimille, mikä voi antaa huomattavaa suorituskykyetua esim. moniydinprosessoria käytettäessä. Koska käyttöjärjestelmä vastaa pääasiassa ohjelmien välisestä rinnakkaisuudesta ja vuorontamisesta, keskitytään tässä tekstissä lähinnä yhden ohjelman rinnakkaiseen suorittamiseen. Käyttöjärjestelmä hallitsee ohjelmien rinnakkaista suorittamista ja niiden jakamista suoritinyksiköille säikeiden (thread) avulla. Säie on abstraktio ohjelmakoodille, joka voidaan rinnakkaisesti suorittaa, minkä tahansa muun säikeen kanssa. Valtaosa rinnakkaista ohjelmointia tukevista ohjelmointikielistä tarjoaakin ohjelmoijille erilaisia rajapintoja ja abstraktiota näiden säikeiden hallintaan. Seuraavissa kappaleissa esitellään kolmen yleisen ohjelmointikielen tarjoamaa abstraktiota rinnakkaisen ohjelmakoodin toteuttamiseen ja vertaillaan niitä ja niistä saatavia hyötyjä. 3
2. C++ thread C++-ohjelmointikieli perustuu olio-ohjelmointiin ja siinä käytetäänkin rinnakkaisuuden toteuttamiseen tyypillisesti thread-luokkaa, joka on toteutettu osaksi Multi-threading kirjastoa. Luokan tarkoituksena on antaa ohjelmoijalle oliopohjainen abstraktio käyttöjärjestelmän säikeiden hallintaan. Käytännössä ohjelmoija voi kirjaston avulla varata säikeitä omaan käyttöönsä ja jakaa ohjelmakoodiaan näihin suoritettavaksi. Thread-luokkaa käytetään normaalin C++-luokan kaltaisesti, jolloin ohjelmoijan varaamia säikeitä voidaan hallinnoida luokan metodien avulla. (1) Kuvassa 1 on yksinkertaisena esimerkkinä ohjelma, joka tekee funktioissa tehtava ja suoritus jotakin ohjelmoijan tahtomalla tavalla. Main-funktiossa nämä kaksi funktiota laitetaan ajamaan kahdessa säikeessä rinnakkain ennen kuin main-funktio suoritetaan loppuun. Todellisuudessa säikeitä ei suoriteta täysin rinnakkaisesti vaan niitä suoritetaan vuorotellen, kunnes funktiot on suoritettu loppuun. Kuva 1. Esimerkki säikeiden käytöstä C++:ssa. 4
C++ säikeitä käytettäessä on syytä muistaa käyttää vuoden 2011 C++ standardia tai uudempaa, sillä vanhemmat standardit eivät hyödynnä tätä luokkaa. Toisena rajoitteena tulee huomioida se, että saman ohjelman luomat säikeet hyödyntävät samaa muistiavaruutta, mikä edellyttää ohjelmoijalta tarkkuutta yhteisten muuttujien hallinnassa. (1) 5
3. Python multiprocessing Python-ohjelmointikielen multiprocessing-moduuli antaa ohjelmoijan käyttöön threads-moduulin kaltaisen rajapinnan, joka tukee tehtävien jakamista rinnakkaisiin suoritusyksiköihin. Multiprocessing-moduulia käytettäessä ohjelmoija kuitenkin hyödyntää säikeiden sijasta prosesseja. Moduuli tarjoaa myös Pool-luokan, jonka avulla ohjelmoija voi hallinnoida helposti joukkoa prosesseja. (2) Pythonin multiprocessing-moduulin käytöstä on kuvassa 2 esitetty esimerkki, jossa moniprosessointia hyödynnetään. Siinä funktiot tehtava ja suorite suoritetaan käyttäen prosesseja suoritus, tehtava_1 ja tehtava_2. Prosessien suorittaminen alkaa, kun prosessit käynnistetään startkomennolla ja niitä suoritetaan rinnakkain, kunnes prosessi on valmis. Lopulta ohjelma päättyy, kun kaikki prosessit on suoritettu loppuun. Kuva 2. Esimerkki multiprocessingin hyödyntämisestä 6
Nykyiset Python-tulkit sallivat vain yhden säikeen suorittamisen kerralla (Global Intepreter lock - mekanismi), mikä estää aidon rinnakkaisuuden käytettäessä säikeitä. Python sallii kuitenkin useamman prosessin suorittamisen samanaikaisesti, mikä mahdollistaa koodin jakamisen ajettavaksi eri prosessoreille ja sitä kautta aidon rinnakkaisuuden. Prosessit eivät kuitenkin käytä samaa muistiavaruutta, joten ne joutuvat käyttämään datan jakamiseen Queue ja Pipe -luokkia. (2, 3) 7
4. Java ExecutorService Java on olio-ohjelmointiin perustuva kieli ja se tarjoaa erääksi keinoksi rinnakkaisuudenhallintaan ExecutorService-luokan, joka on oliopohjainen abstraktio joukolle säikeitä. Luokan avulla ohjelmoija voi hallita rinnakkaisesti suoritettavien ohjelmalohkojen eli tehtävien (runnable/callable) suoritustapaa, -aikaa ja -järjestetystä joutumatta käsittelemään ja hallinnoimaan yksittäisiä säikeitä. (4) ExecutorService varaa luontihetkellään käyttöönsä halutun määrän verran säikeitä altaaseen (thread pool). Tämän jälkeen sille voidaan antaa tehtäviä suoritettavaksi, jolloin se siirtää tehtävän suoritettavaksi johonkin sen varaamista säikeistään. Jos yhtään säiettä ei ole altaassa vapaana, se odottaa jonkin tehtävän loppumista ja siirtää tehtävän vapautuneelle säikeelle. Vastaavasti luokka huolehtii sen varaamiin säikeisiin liittyvistä muista tehtävistä, kuten niiden ajoittamisesta ja vapauttamisesta. (4) Yksinkertainen esimerkki ExecutorServicen käytöstä on kuvassa 3 oleva ohjelma, joka luo uuden ExecutorServicen ja varaa tämän käyttöön viisi säiettä. Tämän jälkeen ohjelma suorittaa ExecutorServicellä 5 tehtävää, jotka ovat tässä tapauksessa vain tulostuksia. Kukin tehtävä ajetaan omassa säikeessään ja niiden suorituksen jälkeen ExecutorService sammutetaan, mikä vapauttaa sen altaassa olevat säikeet muuhun käyttöön. Kuva 3. Esimerkki ExecutorServicen käytöstä. 8
5. Yhteenveto Aiemmissa kappaleissa esitellyt kolme rinnakkaisuudenhallintaan tarkoitettua menetelmää ovat melko pitkälle varsin samanlaisia, sillä ne kaikki perustuvat käyttöjärjestelmän säikeiden tai prosessien hyödyntämiseen. Lisäksi kaikki esitellyt ohjelmointikielet ovat olio-ohjelmoinninparadigmaa noudattavaa, mistä johtuen ne ovat myös ohjelmoijalle hyvin samanlaisia käyttää. Menetelmien joitakin ominaisuuksia on vertailtu taulukossa 1. Menetelmien abstraktiotasot eroavat hieman toisistaan, sillä C++:n thread-kirjasto tarjoaa hieman matalamman tason menetelmiä kuin kaksi muita. Tämä näkyy esimerkiksi siinä, että korkeamman tason multiprocessing ja ExecutorService tarjoavat mahdollisuuden varata joukko säikeitä/prosesseja ohjelman käyttöön, kun taas C++:ssa joutuu jokaisen säikeen varaamaan erikseen. Vastaavasti C++:ssa ohjelmoija joutuu itse huolehtimaan säikeen vapauttamisesta. Koska multiprocessing käyttää prosesseja säikeiden sijaan, ei ohjelmoija voi käyttää datan välitykseen yhteisiä muuttuja tai yhteistä muistiavaruutta, vaan joutuu käyttämään erityisiä kommunikaatioväyliä. Tämä on toisaalta ohjelmoijan kannalta myös hyvä, sillä se edellyttää ohjelmoijan huolehtimaan enemmän yhteisen datan suojaamisesta esim. data-race-ongelmaa vastaan. Paradigmat Abstraktio Tuki usealle säikeelle yhden olion kautta Datan hallinta Säikeen automaattinen vapauttaminen C++ thread Käännettävä, oliopohjainen, imperatiivinen, vahva tyypitys Säie Ei Yhteinen muistiavaruus Ei Python multiprocessing Tulkattava, oliopohjainen, imperatiivinen, heikko tyypitys Prosessi Kyllä Queue ja Pipe - luokat Kyllä Java ExecutorService Käännettävä, oliopohjainen, imperatiivinen, vahva tyypitys virtuaalikoneella suoritettava Säie Kyllä Yhteinen muistiavaruus Taulukko 1. Menetelmien vertailua Kyllä ExecutorServicen sisällä, mutta ExecutorService on sammutettava, jotta säikeet vapautuvat yleiseen käyttöön. 9
Kaikissa esitetyissä ohjelmointikielissä rinnakkaisuus on toteutettu erillisinä komponentteina, mikä viestii sitä, että kieliin ei ole alunperin suunniteltu tukea rinnakkaisuudelle. Tämä voi osaltaan vaikeuttaa rinnakkaisten ohjelmien tekoa näillä kielillä, jos kielen ydinrakenteissa ja kääntäjissä ei ole otettu huomioon rinnakkaisuuden aiheuttamia ongelmia. Toisaalta tässä tekstissä esitetyt komponentit toteuttavat tehtävänsä melko hyvin, jolloin rinnakkaista ohjelmaa tehdessään tuskin joutuu ongelmiin, kunhan ohjelmoija on tarpeeksi tarkkana. 10
6. Lähteet 1. http://www.cplusplus.com/reference/thread/thread/ 2. https://docs.python.org/3/library/multiprocessing.html 3. https://docs.python.org/3/glossary.html#term-global-interpreter-lock 4. https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/executorservice.html 11