Mutex määritellään kaikille säikeille näkyvänä globaalina muuttujana:
pthread_mutex_t lukko;Ennen käyttöä, mutex alustetaan esim. oletussäikeessä:
pthread_mutex_init(&lukko, NULL);Tämän jälkeen säikeet voivat suojata kriittisen alueen seuraavasti:
pthread_mutex_lock(&lukko); // kriittisen alueen koodi pthread_mutex_unlock(&lukko);Alustuksen jälkeen mutexilla on alkuarvo 1. Jos mutexin arvo on 1, operaatio pthread_mutex_lock vähentää mutexin arvon nollaksi ja päästää kutsujan eteenpäin, eli "lukitsee lukon". Jos mutexin arvo on 0, operaation pthread_mutex_lock kutsuja jää odottamaan.
Operaatio pthread_mutex_unlock kasvattaa mutexin arvon nollasta yhteen ja antaa mahdollisuuden yhdelle odottajalle mutexin haltuun saamiseen. Jos mutexin arvo on 1, ei pthread_mutex_unlock:in suorittamisella ole mitään vaikutusta. Tässä mutex poikkeaa semaforista, joka voi saada myös suurempia arvoja kuin 1.
Tarkastellaan esimerkkiä 12-1.c. Ohjelma sisältää kokonaislukuja tallettavan linkitetyn listan, jota säikeet käsittelevät. Säikeistä yksi lisää ja poistaa listalla olevia alkioita. Loput 5 säiettä tulostavat listan sisällön aika ajoin. Listan käsittely on suojattu mutexilla.
Globaalit määrittelyt ja oletussäie näyttävät seuraavilta:
// mutex listan käsittelyn synkronointiin pthread_mutex_t lukko; // linkitetyn listan solmu struct data{ int luku; struct data *next; }; // linkitetyn listan pää struct data *lista; int main(int argc, char *argv[]){ pthread_t wid; pthread_t rid[5]; int i; srand( rand(NULL) ); // lista alussa tyhjä lista = NULL; // alustetaan mutex oletusattribuutein eli toinen parametrin NULL pthread_mutex_init(&lukko, NULL); // luodaan 1 kirjoittaja ja 5 lukijaa pthread_create( &wid, NULL, writer, NULL); for ( i=0; i < 5; i++ ) pthread_create( &rid[i], NULL, reader, (void *)i); // odotetaan kirjoittajan lopettamista pthread_join( wid, NULL ); // odotellaan vielä pari sekuntia sleep(2); // lopetetaan lukijat for ( i=0; i < 5; i++ ) pthread_cancel( rid[i] ); return 0; }Pääohjelma siis alustaa listan tyhjäksi, asettaa satunnaislukugeneraattorille siemenluvun ja alustaa mutexin. Tämän jälkeen luodaan 6 säiettä, yksi säie alkaa suorittamaan funktiota writer ja loput funktiota reader.
writer-säikeen lopetusta odotetaan pthread_join-funktiolla. Tämän jälkeen pääsäie lopettaa kaikki reader-säikeet käyttämällä pthread_cancel-funktiota, joka siis saa parametrikseen lopetettavan säikeen säietunnisteen.
writer-säie lisäilee ja poistelee listalta alkoita:
void *writer(void *arg){ int r; int i; // laitetaan listalle aluksi kaksi lukua addItem( rand()%10 ); addItem( rand()%10 ); // loopissa nukutaan satunnainen aika ja joko lisätään tai poistetaan luku for( i=0; i < N; i++ ){ sleep ( 1+rand()%5 ); r = rand()%2; if ( r < 1 ) addItem( rand()%10 ); else delItem(); } pthread_exit(NULL); }Listan käsittelyn toteuutavissa funktioissa addItem ja delItem listan käsittely on suojattu mutexilla siten, että ennen kun aloitetaan listan käsittely, kutsutaan pthread_mutex_lock(&lukko); ja kun alkion lisäys tai poisto on suoritettu, kutsutaan pthread_mutex_unlock(&lukko); ja näin lista on taas vapaana. Jos joku oli juuri lukemassa listaa kun lista yritetään lukita, odottaa lisäysfunktio mutexin takana niin kauan että lukko on taas vapaa.
// funktio, joka lisää listalle parametrina saadun int-arvon void addItem(int x){ struct data *p; struct data *uusi; // lukitaan lista pthread_mutex_lock(&lukko); printf("adding an item\n"); // tilaa uudelle solmulle uusi = (struct data *) malloc( sizeof(struct data) ); uusi->next = NULL; uusi->luku = x; // uusi solmu lisätään listan hännälle // ensin erikoistapaus jossa lista tyhjä if ( lista == NULL ){ lista = uusi; } else { // etsitään listan loppu ja lisätään uusi solmu viimeiseksi p = lista; while( p->next!=NULL ){ p = p->next; } p->next = uusi; } // vapautetaan lista pthread_mutex_unlock(&lukko); } // funktio, joka poistaa listan ensimmäisen alkion void delItem(){ struct data *p; // varataan lista pthread_mutex_lock(&lukko); printf("deleting an item\n"); // jos lista ei jo tyhjä, poistetaan ensimmäinen alkio if ( lista != NULL ) { p = lista; lista = lista->next; free( p ); } // vapautetaan lista pthread_mutex_unlock(&lukko); }Lukijasäie sisältää ikuisen silmukan, jossa käydään aina välillä tulostamassa listan sisältö. Säie ei siis koskaan itse lopeta toimintaansa vaan lopetuksen saa aikaan ohjelman oletussäikeen kutsuma pthread_cancel-funktio.
// säie, joka tulostaa listan sisällön satunnaisin välein void *reader(void *arg){ int i; while( 1 ){ sleep( 1+rand()%5 ); printList( (int)arg ); } // huom: kyseessä ikuinen looppi, tänne ei tulla koskaan pthread_exit(NULL); }Listan tulostuksen hoitavassa funktiossa jälleen periaatteena se, että ensin lista lukitaan ja tulostuksen jälkeen taas vapautetaan. Funktio saa parametrikseen tulostavan säikeen numeron.
void printList( int n ){ struct data *p; // lukitaan lista pthread_mutex_lock(&lukko); // tulostetaan alkiot, kerrotaan myös kuka tulostaa p = lista; printf("reader %d: ", n ); while ( p!=NULL ){ printf("%d ",p->luku); p = p->next; } printf("\n"); // vapautetaan lista pthread_mutex_unlock(&lukko); }Tässä tapauksessa mutexin avulla on suhteellisen helppo suojata kriittinen alue (eli listaa käsittelevä koodi) säikeiden yhtäaikaiselta käytöltä. Sama ohjelma ilman mutexin käyttöä 12-1b.c saa aikaan sotkua. Mistä sotku johtuu?
Operaatiosta pthread_mutex_lock on olemassa myös versio pthread_mutex_trylock, joka ei laita kutsujaa odotustilaan vaan toimii seuraavasti:
pthread_mutex_trylock behaves identically to pthread_mutex_lock, except that it does not block the calling thread if the mutex is already locked by another thread. Instead, pthread_mutex_trylock returns immediately with the error code EBUSY.
Säikeiden yhteydessä ei ole tarvetta käyttää nimettyä semaforia niin kuin prosessien yhteydessä. Semafori alustetaankin funktion sem_open sijasta funktiolla sem_init, jossa ensimmäinen parametri on osoitin sem_t-tyyppiseen semaforimuuttujaan, toisena aina nolla ja kolmantena semaforin alkuarvo.
Ohjelma 12-3.c on samanlainen kuin 12-2.c, mutta semaforin sijasta käytetään mutexia. Koodi näyttää molemmissa lähes samanlaiselta ja toiminnallisuudessa ei ole mitään eroa.
Suorituskyvyssä on kuitenkin huikea ero:
[luuma@telinux1 vko12]$ gcc 12-2.c -o 12-2 -lpthread [luuma@telinux1 vko12]$ gcc 12-3.c -o 12-3 -lpthread [luuma@telinux1 vko12]$ time ./12-2 jaettu muuttuja lopussa 0 real 0m27.193s user 0m12.500s sys 0m41.610s [luuma@telinux1 vko12]$ time ./12-3 jaettu muuttuja lopussa 0 real 0m7.618s user 0m5.130s sys 0m9.230s [luuma@telinux1 vko12]$Eli mutexeja kannattaa säikeiden kanssa käyttää aina jos se on mahdollista. On kuitenkin joitakin asioita, joita ei voi tehdä mutexilla mutta jotka onnistuvat helposti semaforeilla.
Voisi kuvitella, että mutexeja pystyisi käyttämään samaan tyyliin, eli alussa mutexin arvoksi asetettaisiin nolla, mutex olisi siis jo alussa lukittu. Tämän jälkeen säie 2 menisi odottamaan mutexin taakse ja jossain vaiheessa säie 1 antaisi aloitusluvan avaamalla mutexin. Näin ei kuitenkaan saa tehdä kuin tietyissä rajatuissa tapauksissa, sillä man-sivulta selviää:
pthread_mutex_unlock unlocks the given mutex. The mutex is assumed to be locked and owned by the calling thread on entrance to pthread_mutex_unlock.Eli mutexin sijasta synkronointiin on syytä käyttää semaforia tai kohta esiteltävää ehtomuuttujaa.
Ohjelman toimintaa saadaan tehostettua käyttämällä lukija/kirjoittajalukkoa, eli rwlukkoa. Rwlukon käyttäminen on yhtä helppoa kuin mutexin käyttäminen. Ainoana erona on se, että jos halutaan lukko kirjoittamista varten, toimitaan seuraavasti:
void delItem(){ struct data *p; // lukitaan kirjoittamista varten pthread_rwlock_wrlock(&rwlukko); // kriittisen alueen muuttujia muuttava koodi // vapautetaan lukko pthread_rwlock_unlock(&rwlukko); }Kun lukko on lukittu kirjoittamista varten, ei kukaan lukjia eikä kirjoittaja pääse kriittiselle alueelle.
Lukko lukemista varten saadaan seuraavasti:
void readList(){ struct data *p; // lukitaan lukemista varten pthread_rwlock_rdlock(&rwlukko); // kriittisen alueen dataa lukeva koodi // vapautetaan lukko pthread_rwlock_unlock(&rwlukko); }Kun lukko on lukittu lukemista varten, kirjoittajat eivät pääse kriittiselle alueelle. Muita lukijoita alueella sensijaan voi olla samaan aikaan lukulukon saaneen säikeen kanssa.
Ohjelma 12-4.c on ohjelman 12-1.c muunnos, joka käyttää rwlukkoa. Pienenä erona ohjelmissa on se, että lukijasäikeet tulostavat nyt ainoastaan listalla olevien lukujen summan.
Käytettäessä luku/kirjoituslukkoja tarvitaan joillan koneilla (esim. cs.stadia.fi:ssa, joka on Redhat Linux) käännökseen parametriksi -D_XOPEN_SOURCE=600, jotta rwlukot toteuttava koodi saadaan linkitettyä mukaan. Käännös siis tapahtuu komennolla:
gcc 12-4.c -lpthread -D_XOPEN_SOURCE=600
Lukitseminen on sitä helpompi toteuttaa, mitä vähemmän lukkoja ohjelmassa on. Jos ohjelmassa on paljon säikeitä ja käytettävät tietorakenteet ovat suuria, on usein tehokkaampi toimia siten, että lukitusta ei tehdä koko tietorakenteen tasolla, vaan pienemmissä osissa. Yleensä tämä edellyttää useiden mutexien tai rwlukkojen käyttöä. Samalla ohjelmoinnin vaikeusaste kasvaa. Erityisesti lukkiumien (engl. deadlock) eli tilanteiden, joissa useita säikeitä jää jumiin keskinäisten odotusketjujen takia, todennäköisyys kasvaa. Lukkiuma voi syntyä esim. tilanteessa jossa säikeellä A on hallussa mutex1 ja säikeellä B mutex2. Jos nyt säie A haluaa mutex2:n ja samaan aikaan säie B mutex1:n syntyy tilanne missä molemmat säikeet odottavat toisiaan ja odotus ei pääty koskaan.
Tämäntyyppiseen odotukseen kannattaa käyttää ehdomuuttujia (engl. condition variables). Ehtomuuttujan operaatio pthread_cond_wait aiheuttaa sen, että operaatiota kutsuva säie alkaa odottamaan ehdon toteutumista ja pthread_cond_signal herättää yhden ehtomuuttujan takana odottavan säikeen.
Ehtomuuttujaan liittyy aina mutex, jolla suojataan ehtomuuttujaan liityvä ehto. Jos odotettava ehto olisi esim. se, että muuttujan x arvo on 0, käytettäisiin ehtomuuttujaa seuraavaan tapaan. Käytössä olisi ehtomuuttuja ja siihen liittyvä mutex, eli globaalit esittelyt:
// mutex listan käsittelyn synkronointiin pthread_mutex_t lukko; // ehtomuuttuja odottamiseen pthread_cond_t ehto;Ehtoa testaava säie toimisi seuraavasti:
// ensin varataan mutex pthread_mutex_lock(&lukko); // testataan onko ehto tosi, jos ei, mennään odottamaan while ( x!=0 ) { pthread_cond_wait( &ehto, &lukko ); } // ehto oli tosi, koodi tänne // vapautetaan mutex pthread_mutex_unlock(&lukko);Ensin varataan ehtoa suojaava mutex ja testataan ehdon voimassaolo. Jos ehto ei ole voimassa, mennään odottamaan. Funktion pthread_cond_wait toisenaa parametrina siis on ehtoon liittyvä mutex. Funktion kutsuminen vapauttaa mutexin automaattisesti. Kun odottaja jälleen herää, on mutex sille varattuna. Ehdon voimassaolo on syytä tarkastaa uudelleen, siksi käytetään while-lausetta. Voi nimittäin käydä siten, että siinä välissä kun ehto oli tosi ja odotus loppuu, joku muu on ehtinyt käydä muuttamassa ehdon arvoa.
Ehdon todeksi tekevä säie toimii seuraavasti:
// varataan mutex pthread_mutex_lock(&lukko); // muutetaan ehtomuuttujan arvoa x++; // jos ehto tuli todeksi, signaloidaan eli herätetään joku odottaja if ( x==0 ) pthread_cond_signal(&ehto); // vapautetaan mutex pthread_mutex_unlock(&lukko);Palataan ohjelmaan 12-5.c. Oletussäikeessä alustetaan mutex ja ehtomuuttuja sekä käynnistetään säikeet:
int main(int argc, char *argv[]){ pthread_t wid; pthread_t rid[5]; int i; // alustetaan mutex oletusattribuutein eli toinen parametrin NULL pthread_mutex_init(&lukko, NULL); // alustetaan ehtomuuttuja oletusattribuutein eli toinen parametrin NULL pthread_cond_init(&ehto, NULL); // luodaan 1 kirjoittaja ja 5 lukijaa pthread_create( &wid, NULL, writer, NULL); for ( i=0; i<5; i++ ) pthread_create( &rid[i], NULL, reader, (void *)i); // odotetaan kirjoittajan lopettamista pthread_join( wid, NULL ); // odotellaan vielä pari sekuntia sleep(2); // lopetetaan lukijat for ( i=0; i<5; i++ ) pthread_cancel( rid[i] ); return 0; }Kirjoittajasäie toimii seuraavasti:
// säie, joka lisää ja poistaa listan alkioita void *writer(void *arg){ int r; int i; // laitetaan listalle aluksi kaksi lukua addItem( rand()%10 ); addItem( rand()%10 ); // loopissa nukutaan satunnainen aika ja lisätään alkio for( i=0; i < N; i++ ){ sleep ( 1+rand()%5 ); addItem( rand()%10 ); } pthread_exit(NULL); }Listalle lisäyksen tekevä funktio lukitsee ensin listan ja tekee lisäyksen. Jos lisäys tehtiin tyhjään listaan signaloidaan ehtomuuttujaa siltä varalta, että joku oli odottamassa uusia alkioita:
void addItem(int x){ struct data *p; struct data *uusi; // lukitaan lista pthread_mutex_lock(&lukko); printf("adding an item\n"); // tilaa uudelle solmulle uusi = (struct data *) malloc( sizeof(struct data) ); uusi->next = NULL; uusi->luku = x; // uusi solmu lisätään listan hännile // ensin erikoistapaus jossa lista tyhjä if ( lista == NULL ){ lista = uusi; // signaloidaan ehtomuuttujaa siltä varalta, että joku odotti alkioita pthread_cond_signal(&ehto); } else { // etsitään listan loppu ja lisätään uusi solmu viimeiseksi p = lista; while( p->next!=NULL ){ p = p->next; } p->next = uusi; } // vapautetaan lista pthread_mutex_unlock(&lukko); }Lukijasäie suorittaa yksinkertaista ikuista looppia:
// säie, joka tulostaa listan sisällön satunnaisin välein void *reader(void *arg){ int x; while( 1 ){ sleep( 1+rand()%5 ); x = delItem( (int)arg ); printf("reader %d: poistettiin %d\n", (int)arg , x); } // huom: kyseessä ikuinen looppi, tänne ei tulla koskaan pthread_exit(NULL); }Alkion poistaminen listalta edellyttää ehdon voimassaolon tarkistamista. Ehtona siis se onko listalla dataa, eli lista!=NULL jos ei, jäädään odottamaan ehtomuuttujaan.
// funktio, poistaa listalta ensimmäisenä olevan arvon // parametrina operaation tehneen säikeen numero int delItem( int i){ struct data *p; int x; // varataan lista pthread_mutex_lock(&lukko); // testataan onko listalla dataa, jos ei, mennään odottamaan // odottamaan meneminen vapauttaa mutexin lukko while ( lista==NULL ) { printf( "ei dataa, säie %d odottaa... \n",i ); pthread_cond_wait( &ehto, &lukko ); } // HUOM: kun herätään, on mutex lukko automaattisesti lukittuna // tehdään heti uusi testi ja varmistetaan, että listalla on dataa // nyt tiedetään että lista ei nyt ole tyhjä p = lista; lista = lista->next; x = p->luku; free( p ); // vapautetaan lista pthread_mutex_unlock(&lukko); return x; }
Joskus ehdon toteutuessa on syytä herättää kaikki ehtoa odottavat säikeet. Tällöin voidaan käyttää funktiota pthread_cond_broadcast.
Ehkä tärkein attribuutti on ns. detach state, jolla on kaksi arvoa: joinable ja detached. Oletusarvoisesti säie on joinable, joka tarkoittaa sitä, että säikeen luoja (tai joku muu säie) voi odottaa säikeen lopetusta funktiolla pthread_join. Näinhän olemme tähän mennessä tehneet. Jos säikeen on tarkoitus aloittaa täysin muista riippumaton elämä, säie voidaan "irroittaa" muista säikeistä kutsumalla funktiota pthread_detach, esim. seuraavasti:
int main(int argc, char *argv[]){ pthread_t id; pthread_create( &id, NULL, funktio, NULL); pthread_detach(id); // ... return 0; }Tämän jälkeen säikeelle ei voi tehdä pthread_join-operaatiota. Etu tässä on se, että detached-tilassa olevan säikeen käyttämät resurssit vapautetaan heti kun säie lopettaa. Normaalisti resurssit vapautuvat vasta kun säikeelle on tehty pthread_join.
Säie voi irroittaa myös itse itsensä muista säikeistä kutsumalla pthread_detached-funktiota omalle säietunnisteelleen joka saadaan selville funktiolla pthread_self:
pthread_detach( pthread_self() );Muita säikeisiin liittyväiä atribuutteja ovat esim. säikeen pinon koko, skedulointitapa sekä skedulointiprioritetti. Lisätietoa esim. seuraavien komentojen mansivuilta: Signaalien toiminta säikeistetyssä ohjelmassa poikkeaa normaaleista yksisäikeisisistä prosesseista jonkin verran. Monisäikeisessä ohjelmassa prosessikohtainen signaalimaski ei ole määritelty, vaan jokaisella säikeellä on oma signaalimaskinsa. Kaikkien yhden prosessin säikeet kuitenkin käyttävät samoja signaalinkäsittelijöitä. Säikeistetyissä ohjelmissa ei ole välttämättä tarvetta edes erillisille signaalinkäsittelijöille, yksi säie voidaan laittaa huolehtimaan signaalien käsittelystä. Vaarana on kuitenkin se, että signaali voi mennä väärälle säikeelle, eli säikeelle, joka on blokannut signaalin. Katso aiheesta enemmän esim. komentojen pthread_sigmask, pthread_kill ja sigwait man-sivuilta.
Lisää säikeisiin liittyvää informaatiota löytyy esim. täältä.