Säikeiden välinen synkronointi

Jos säikeet käyttävät yhteistä dataa, samoja tiedostoja, samoja putkia tai muita yhteisiä resursseja, törmätään täsmälleen samoihin ongelmiin, joista oli puhetta viikolla 9, eli jotta ohjelma toimii oikein on säikeiden pystyttävä synkronoimaan toimintansa siten, että jaettuja resursseja käyttävät kriittiset alueet pystytään suojaamaan yhtäaikaisen käsittelyn estämiseksi.

mutexien käyttö

Yksinkertaisin säikeiden synkronointimekanismi on mutex-muuttuja, joka toimii melkein samalla tavalla kun viikolla 9 käsittelemämme semafori. Oikeastaan mutex on semafori, jonka arvona voi olla vain 0 tai 1. Mutex ajatellaankin usein lukkomuuttujaksi, jos mutexin arvo on 0, on lukko kiinni ja jos arvo on 1, on lukko auki.

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.

Posix-semaforien käyttö säikeiden yhteydessä

Viikolla 9 käytettyjä Posix semaforeja voidaan käyttää myös säikeiden yhteydessä. Posix semaforeja kriittisen alueen suojaamiseen käyttävä ohjelma 12-2.c on säieversio ohjelmasta 9-4.c.

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.

Säikeiden synkronointi

Esimerkissä 9-5.c käytettiin semaforia prosessien synkronointiin siten, että semaforilla oli alkuarvo nolla ja lapsiprosessi kutsui heti aloittaessaan sem_wait ja jäi näin odottamaan. Hetken kuluttua vanhempi teki operaation sem_post, joka sai aikaan sen, että lapsi pääsi etenemään.

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.

Lukija/kirjoittajalukot

Jaetut muuttujat ovat yleensä sellaisia, että periaatteessa ei ole estettä sille että useampi säie lukisi jaettuja muuttujia yhtä aikaa. Ongelmia syntyy silloin jos joku yrittää lukea muuttujia samalla kun toinen säie kirjoittaa jaettuihin muuttujiin tai jos useampi säie kirjoittaa yhtä aikaa muuttujiin. Ohjelmassa 12-1.c lukitus hoidettiin siten, että koko lista oli lukittuna kerrallaan yhdelle säikeelle. Periaatteessa tämä on hätävarjelun liioittelua yhtäaikaisten lukijoiden yhteydessä.

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.

Ehtomuuttujat

Usein tulee eteen tilanteita, jossa säikeen on odotettava niin kauan kunnes jokin ehto on tosi. Ohjelmassa 12-5.c yksi säie (suorittaa funktiota writer) lisää data-alkioita linkitettynä listana toteutettuun jonoon. Viisi säiettä (suorittavat funktiota reader) poistaa jonossa olevia data-alkikoita. Jos jonossa ei ole dataa, on reader-säikeen odotettava kunnes writer saa taas laitettua jonoon lisää dataa.

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.

Sekalaisia huomioita

Säikeillä on muutamia niiden toimintaan vaikuttavia attribuutteja. Attribuutit voidaan asettaa pthread_create-funktion toisena parametrina. Jos käytetään oletusattribuutteja, parametrin arvoksi tulee NULL niinkuin meillä tähän asti on ollut. Toinen tapa attribuuttien asettamiseen on käyttää suoria funktiokutsuja, joissa parametrina on säietunniste.

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:
  • pthread_attr_setschedpolicy
  • pthread_attr_setinheritsched
  • sched_get_priority_max
  • sched_get_priority_min
  • 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ä.