sunnuntai 3. elokuuta 2008

Threadit eli säikeet.






POSIX Threadit eli pthreads kirjasto


Kuten edellisessä blogipostissani mainitsin, threadit eli säikeet ovat keino ajaa useampaa kohtaa ohjelmakoodista yhden prosessin sisällä, näennäisesti yhtä aikaa. Yhdenaikaisuuden näennäisyys johtuu siitä, että mikäli koneessa on vain yksi (yksiytiminen) prosessori, ei se luonnollisestikaan voi tehdä kahta asiaa yhtä aikaa. Näennäinen yhdenaikaisuus saadaan aikaan käyttämällä ns. scheduleria, joka paloittelee koodin pienempiin suorituspätkiin, ja ajaa kutakin pätkää vuorollaan. Se mitä pätkää milloinkin ajetaan, ja kuinka paljon, riippuu schedulerin luonteesta. Muutamia yleisiä "hyvätietää pointteja" voin kuitenkin mainita.

Ensinnäkin, skedulereihin (ja käyttöjärjestelmiin) voidaan tehdä karkea jako, tavalliset käyttöjärjestelmät ja reaaliaika systeemit. Tavallinen käyttöjärjestelmä on "joustavampi". Mikäli teet esimerkiksi ajastimen, jonka tulisi laueta 2 millisekunnin välein, voit olla varma siitä että normaali käyttöjärjestelmä ei tähän kykene. Ja harvoin sen tarvitseekaan. Normaali käytössä tärkeää on muun muassa se, että kun käyttäjä painaa nappia, kone reagoi nopeasti. Eli korkeampi prioriteetti on I/o operaatioilla, ja scheduleri suosiikin I/o prosesseja. Lisäksi aina kun scheduleri vaihtaa toisen threadin (tai prosessin) ajoon, kuluttaa se ajoaikaa läpikäydessään omaa tehtävälistaansa ja hoitaessa muita sisäisiä asioitaan. Siksi kovin tiuhaan tehtävää vaihtava scheduleri hidastaa koneen kokonaistoimintaa. Reaaliaikasysteemit taas on kehitetty vastaamaan tarpeeseen saada ajastimet jne. mahdollisimman tarkoiksi. Ajatellaanpa esim auton tietokonetta, jonka tehtävänä on hoitaa luistonesto mekanismia lisäämällä/vähentämällä vääntöä pyöristä ajo-ominaisuuksien parantamiseksi. Muutaman millisekunnin epätarkkuus saattaisi jo kriittisessä tilanteessa saada auton lähtemään luisuun. Reaaliaikasysteemeissä scheduleri tyypillisesti vaihtaa suoritettavaa tehtävää huomattavasti useammin kuin normaalisysteemeissä. Mutta ennen kun unohdan koko kirjoitukseni alkuperäisen idean, on parasta vain todeta että schedulerit ovat mielenkiintoisia kikkuloita, ja niihin liittyy monia nerokkaita algoritmeja. Alkuun pääset vaikkapa hakemalla googlella tietoa round robinista.

mmm.. mistä aioinkaan kirjoittaa?

Ai niin, säikeistä. Yhdenaikainen ohjelmakoodin suoritus aiheuttaa omat ongelmansa. Se mahdollistaa kokonaisen joukon uusia mahdollisia bugeja tehtäväksi. Esim saman resurssin käpistely kahdesta eri säikeestä yhtä aikaa, on melko varma keino saada systeemi nurin...

Ah. Nyt tarkkaavainen lukijani (joo, taisit olla sinä Kari S., Vesa H. kun ei jaksanut näin pitkälle ;) ) on huomaavinaan aukon selvityksessäni. Aiemminhan minä totesin, että yhdenaikaisuus on näennäestä, prosessori kun tekee vain yhtä asiaa kerrallaan - miten siis samaa resurssia voitaisiin käpistellä yhtä aikaa? Kari hyvä, vaikka kerroinkin ettei prosessori tee kuin yhtä asiaa kerrallaan, niin yhtä C-kielen funktiota saattaa vastata useampi prosessorikohtainen komento. Ja moniprosessori ympäristössä asia mutkistuu entisestään. Sitäpaitsi oli syy mikä tahansa, niin jos teet testiohjelman jossa useampi säie käpistelee samaa osoitetta tiuhaan tahtiin, niin ennemmin tai myöhemmin napsahtaa...

Usein kuitenkin on tarpeen käyttää samaa resurssia useammasta säikeestä. Miten siis tehdä tämä turvallisesti? No onneksi tähän on myös keksitty ratkaisu. Semaforit, mutexit ja luku/kirjoitus lukot. Mutex on kuin lukko, johon on vain yksi avain. Ensin lukko luodaan ja alustetaan (avain jätetään lukkoon :D) Ts. tehdään 'pthread_mutex_t muuttuja' ja kutsutaan pthread_mutex_init funktiota. Tämän jälkeen kun jokin threadi haluaa käpistellä jotain yhteistä resurssia, se kutsuu lukitusfunktiota (vetää lukon kiinni ja ottaa avaimen mukaansa) pthread_mutex_lock. Nyt lukko on kiinni, ja mikäli jokin toinen säie kutsuu lukkofunktiota mutexin ollessa lukittuna, blokkaa lukkofunktio kutsuvan säikeen suorituksen kunnes aiemmin lukon sulkenut säie avaa sen pthread_mutex_unlock. Finally the mutex is destroyed with pthread_mutex_destroy.

Semaforit ovat kuten mutexitkin mutta tälläkertaa avaimia onkin yhden sijasta useampi.

Ja jälleen kerran kun keksittiin uusi hieno ratkaisu ongelmaan, aikaansaatiin myös uusi bugin mahdollisuus. Eli ole varovainen semaforien ja mutexien kanssa, puolihuolimattomalla käytöllä aikaansaadaan ikuinen odotustilanne, eli "deadlock". Deadlock siis on tilanne, missä kahdella (tai useammalla) säikeellä on jokin resurssi hallussaan jota toinen tarvitsee, ja molemmat ovat juuttuneena odottamaan että toinen luovuttaa resurssin toiselle.

No nyt koetan viimeinkin päästä itse asiaan, eli threadin luontiin ja lopetukseen. Uuden threadin luonti tapahtuu pthread_create() functiolla. pthread_create() funktiolle annetaan argumenttina osoitin funktioon josta uuden säikeen suoritus aloitetaan. Lisäksi pthread_createlle annetaan parametrina starttifunktiolle menevä void * tyypin argumentti. Uusi säie voidaan käynnistää joko"joinablena" tai "detachedina", mutta detach eli irroitus voidaan halutessa tehdä myös myöhemmin, kutsumalla pthread_detach funktiota. Mikäli säiettä ei irroiteta, se voidaan (ja se tulee) 'liittää' kutsuneeseen säikeeseen sen varaamien resurssien vapauttamiseksi ja säikeen palauttaman paluuarvon lukemiseksi. Liittäminen tehdään pthread_join kutsulla, ja se on blokkaava kutsu, eli se pysäyttää liittävän säikeen suorituksen siksi aikaa, kunnes liitettävä säie on lopettanut suorituksensa joko pthread_exit kutsulla, tai palaamalla starttifunktiosta. Irroitettua säiettä ei tarvitse (eikä voi) liittää resurssien vapauttamiseksi, eikä sen paluuarvoa voida tutkia.

Ja vielä ennen koodiesimerkkiä pieni varoituksen sana:

Kuten aiemmin mainitsin useista säikeistä koostuvassa ohjelmassa tulee olla tarkkana staattisten sekä globaalien resurssien kanssa. Myös jotkut standardikirjaston funktiot käyttävät allaolevissa toteutuksissaan näitä, eivätkä niinollen sovi monisäikeiseen ohjelmointiin ilman asianmukaista semafori/mutex suojausta! Monisäikeisiin ohjelmiin soveltuvista funktioista käytetään englanninkielistä nimitystä "reentrant". Kallisarvoinen ystävämme google osaa varmasti koostaa sinulle listaa näistä funktioista.

Mutta nyt esimerkin pariin.

Esimerkki 1 - Blokkaavien sokettien käyttö, ohjelmassa joka ei saa jäädä "jumiin".
HUoMAA: Esimerkkikoodia ei ole testattu!


#include <sys/socket.h>
#include <inet/arpa.h>
#include <pthread.h>
#include <stdio.h>

#define NUKKUAIKA 1
#define YHTEYS_HUKASSA -666

pthread_mutex_t data_lukko;
void *socketti_data;
size_t datan_koko;

int main(int argc, char *argv[])
{
pthread_t thred;
int paluuarvo;
datan_koko=0;
data=NULL;
char ip[]="127.0.0.1";
.
.
.

/* Alusta mutex käyttöä varten */

if(pthread_mutex_init(&data_lukko, NULL))
{
//virhekäsittely, mutexia ei voitu alustaa.
}
/*
argumentit: starttifunktio, attribuutit,
osoitin threadin kahvaan, starttifunktion
argumentit.
*/
if(
pthread_create
(
&pollaile_sockettia,
NULL,
&thred,
(void *)ip
)
)
{
//virhekäsittely, threadin luonti epäonnistui.
}

sleep(NUKKUAIKA);
while(1)
{
//suojaa globaalit muuttujat lukoilla!
pthread_mutex_lock(&data_lukko);
if(datan_koko!=0)
{
if(*(int *)data==YHTEYS_HUKASSA)
{
if( (paluuarvo=pthread_join(thred)) )
{
//virhekäsittely, tarkista virhe paluuarvosta
}
pthread_mutex_destroy(&data_lukko);
return 0;
}
kasittele_sockettidata();
}
pthread_mutex_unlock(&data_lukko);
tee_tuiki_tärkeä_tehtävää_joka_ei_odotaa_sockettia();
}
}

void *pollaile_sockettia(void *argumentit)
{
char *ip;
int socketti;
size_t luettu;
char puskuri[RECV_MAX];

ip=(char *)argumentit;
.
.
.
/*
Tässävaiheessa ohjelmaa socketti on luotu
ja yhdistetty, ja odotellaan vastausta palvelimelta
toisen säikeen hoitaen samalla tuiki tärkeää tehtävää...
*/
while( 1)
{
luettu=recv(socketti,puskuri,RECV_MAX,0);
if(luettu==0||luettu==-1)
{
pthread_mutex_lock(&data_lukko);
data=(void *)YHTEYS_HUKASSA;
pthread_mutex_unlock(&data_lukko);
pthread_exit(0);
}
pthread_mutex_lock(&data_lukko);
datan_koko=luettu;
data=puskuri;
pthread_mutex_unlock(&data_lukko);
/*
nuku hetki että naapurithread ehtii lukea datan,
ennen uuden datan lukua socketista
*/
sleep(NUKKUAIKA);
}
}

Ja vielä viimeisenä huomautuksena:
käännettäessä gcc kääntäjällä threadeja käyttävää koodia, tule linkkausvaiheessa käyttää flagia -lpthread, eli ottaa mukaan pthread kirjasto.

2 kommenttia:

Anonyymi kirjoitti...

Mahtavaa, kiitos. Kerrankin tajusin jotain rinnakkaisuudesta.

Maz kirjoitti...

Ole hyvä! Mukavaa kuulla, että joku jaksoi lukea jatinoitani :)