Säikeet

Perinteisen eli suunnilleen 90-luvun alulle vallalla olleen näkemyksen mukaan prosessi on joukko varattuja resursseja, sekä suorituksen yksikkö. Yksittäinen prosessi on siis sisältänyt vain yhden itsenäisen suorituspolun (engl. thread of execution).

Jos jokin sovellus on muodostunut useammasta itsenäisestä suorituspolusta, on sovellus pitänyt koostaa käyttäen useaa prosessia. Hankaluutta aiheuttaa se, että oletusarvoisesti jokaisella prosessilla on oma itsenäinen muistiavaruus eli tiedon jakaminen yhden sovelluksen muodostavien prosessien kesken saattaa olla hiukan hankalaa. Kuten edellisillä viikoilla olemme nähneet, niin tiedon jakaminen prosessien kesken ei Unix-tyyppisissä käyttöjärjestelmissä tuota suurta ongelmaa esim. jaettujen muistialueiden ansiosta.

Usein olisi helpompaa ja selkeämpää, jos samaa muistia voisi käyttää useampi rinnakkainen, samaan sovellukseen liittyvä suorituspolku. Modernimpi 1980-luvun jälkeen yleistynyt näkemys prosessista on se, että prosessi sisältää joukon resursseja (muisti, avoimet tiedostot, putket, ym.), ja prosessin sisällä on useita rinnakkaisia säikeita (engl. thread), joiden suoritus etenee itsenäisesti. Säikeet siis jakavat prosessin muistin. Tällöin on helppo tehdä useasta säikeestä koostuvia sovelluksia, jotka kaikki näkevät saman muistialueen. Tämä onnistuu ilman erityisiä ohjelmallisia temppuja (jaettuja muistialueita, putkia, signaaleja ym) mitä prosessien väliseen tiedon jakoon tarvitaan.

Säikeiden käyttö on edullista myös siksi, että säikeet ovat prosesseja kevyempi ratkaisu niiden KJ-tasolle aiheuttaman kuormituksen suhteen. Säikeistetyn prosessin säikeiden skeduloiminen on kevyempää kun vastaavan prosessimäärän skeduloiminen jos lasketaan se, kuinka paljon käyttöjärjestelmä kuluttaa aikaa skedulointiin ja suoritettavan säikeen/prosessin vaihtoon. Ero voi olla todella huomattava, riippuen siitä miten säikeet on KJ:n tasolla toteutettu. Linuxissa säikeiden ja prosessien välillä ei ole yhtä suurta suorituskykyeroa kuin jossain muissa järjestelmissä.

Yhden prosessin säikeet siis jakavat saman muistiavaruuden sekä joukon muita resursseja, kuten avoimena olevat tiedostot. Koska säikeiden suoritus on toisistaan riippumaton, tarvitaan jokaiselle säikeelle oma kopio CPU:n rekisterien arvoista, erityisesti oma ohjelmalaskuri, sekä oma pino. Jokaisella säikeellä on oma tila, eli säie on joko running, runnable tai sleeping -tilassa. Säikeiden tilat siis vastaavat täysin sitä mitä aiemmin sanoimme prosessin tiloista.

Useimmissa nykyisissä käyttöjärjestelmissä tilanne on se, että prosesseilla on oletusarvoisesti yksi säie. Jos ohjelmoidaan "vanhaan tapaan", prosessille ei edes luoda muita säikeitä ja kaikki etenee niin kun mitään säikeistystä ei edes olisi olemassa.

Esim. Unixeissa prosessiin voidaan luoda uusia säikeitä käyttämällä pthread.h-kirjastossa määritellyn säie-API:n funktioita.

Säikeiden toteutus

Säikeiden toteutukseen on kolme mahdollisuutta:
  • Toteutus prosessien sisäisesti siten, että KJ ei edes tiedä säikeiden olemassaolosta
  • KJ:n sisäinen toteutus, eli KJ on tietoinen säikeistä
  • Edellisten välimuoto
  • Kaikilla tavoilla omat hyvät ja huonot puolensa, tarkastellaan niitä seuraavassa.

    Säikeiden toteutus KJ:n ulkopuolella eli ohjelmistokirjastona toteutetut prosessitason säikeet

    Tässä toteutustyypissä KJ:lle tilanne näyttää siltä että prosessin sisällä on vain yksi suorituspolku. Ohjelmoitaessa käytetään säie-kirjastoa ja säikeitä sisältävään prosessin liitetään säiesuoritusympäristö, joka ylläpitää säietaulua, joka sisältää eri säikeisiin liittyvät kirjanpitotiedot. Prosessin sisäinen suoritusympäristö ylläpitää jokaiselle säikeelle omaa pinoa ja hoitaa yhden prosessin sisältämien säikeiden välisen skeduloinnin.

    Säikeiden skedulointi tapahtuu siis prosessien sisällä. Säikeille ei voida antaa aikaviipaleita samaan tapaan kuin prosesseille. Tästä seurauksena on se, että prosessin yksittäinen säie voi jatkaa suoritusta niin kauan kun haluaa. Yleensä käytössä on kirjastorutiini thread_yield, jonka avulla säie voi antaa suoritusvuoron jollekin toiselle saman prosessin säikeelle. Ohjelmoijan on siis osittain itse huolehdittava skeduloinnin tapahtumisesta.

    Prosessitason säikeiden hyviä puolia

  • Säikeen vaihto, uuden säikeen luominen ym. ovat erittäin kevyitä operaatioita verrattuna normaaliin prosessien vaihtoon ja luomiseen
  • Sovellukset voivat itse määritellä sopivimman skedulointitavan
  • Ei vaadi käyttöjärjestelmäytimeltä minkäänlaista tukea säikeille
  • Prosessitason säikeiden huono puoli on seuraava: Jos prosessin sisällä oleva säie tekee odotusta vaativan systeemikutsun (esim. lukee näppäimistöä gets():illä), tarkoittaa se sitä, että koko prosessi siirtyy sleeping-tilaan

    Eli vaikka prosessin muut säikeet voisivat jatkaa, ei se onnistu, sillä prosessi, jonka yksikin säie blokkaa, on KJ:n suhteen sleeping-tilassa. Tämä tekee ohjelmakirjastona toteutetut säikeet käyttökelvottomiksi tietynlaisiin sovelluksiin.

    KJ:n ulkopuolella toteutetut säikeet olivat suosittuja säikeiden historian alkuvaiheissa. Nykyään on kuitenkin siirrytty useimmiten säikeisiin, jotka myös käyttöjärjestelmä tuntee.

    Säikeiden toteutus KJ:n tasolla

    Nykyään yleisin tapa on toteuttaa säikeet siten, että KJ on täysin tietoinen säikeiden olemassaolosta.

    KJ pitää kirjaa sekä prosesseista että säikeistä. Prosessikirjanpito tallettaa prosessikohtaiset tiedot, mm. muistiavaruuden ja avoimet tiedostot sekä tiedon prosessiin kuuluvista säikeistä. Säiekirjanpito tallettaa kaiken säiekohtaisen tiedon, eli CPU:n rekisterien tilan sekä säikeen oman pinon tiedot. Säikeiden skedulointi tapahtuu KJ:n toimesta. KJ:n skeduloinnin yksikkö ei enää ole prosessi vaan säie.

    Hyvänä puolena KJ-tason toteutuksessa on se, että Vaikka yksi prosessin säikeistä suorittaisi odotusta vaativan systeemikutsun, muut prosessin säikeet säilyvät suorituskelpoisena. Huonona puolena se, että KJ:n tasolla toteutettuna säikeet ovat CPU:n kulutuksen kannalta hieman raskaammat kun prosessin sisällä toteutetut säikeet eli suorituksessa olevan säikeen vaihtaminen sekä uuden säikeen luominen vie hieman enemmän resursseja KJ:n tason säikeissä.

    Esim. Linuxissa ja Windowsissa säikeet on toteutettu KJ:n tasolla. Käyttöjärjestelmäytimen tasolla tarkastellen Linuxissa ei oikeastaan edes ole säikeitä. On ainoastaan prosesseja, mutta on mahdollista määritellä joukko prosesseja toimimaan siten, että ne toimivat kuten säikeet, eli jakavat muistiavaruuden ja muut resurssit. Ohjelmointikielessä käytetyt primitiivit määräävät sen, minkälaisia prosesseja ydin luo. Jos käytetään fork:ia, luodaan "normaali" prosessi, joka saa oman muistiavaruuden. Jos taas käytetään säikeen luovaa pthread_create:a, luodaan prosessi, joka sijoitetaan samaan muistiavaruuteen missä kutsun suorittanut prosessi on. Näinollen Linux toimii siis säikeiden ja prosessien suhteen juuri niinkuin speksit (esim. Posix-standardi) määrittelee, mutta alla oleva toteutus on ovelampi kun useissa muissa käyttöjärjestelmissä, joihin säikeet on sisällytetty "liimaamalla" ne prosessien kylkeen.

    Java:n säikeiden toteutus eri alustoilla saattaa olla erilainen. Esim. Linuxissa tilanne on se, että Java 2 JRE 1.2:n asti virtuaalikonetta ajettiin yhden prosessin sisällä ja ohjelman säikeet toteutettiin puhtaasti yhden prosessin sisällä prosessitason säikeinä. Java 2 JRE 1.3:sta alkaen säikeet on toteutettu oikeina KJ-tason säikeinä.

    Hybriiditoteutus

    Esim. Sunin Solaris KJ:ssä (Unix klooni) on toteutettu sekä KJ:n että prosessitason säikeet. Jokaista prosessia kohti varataan joukko KJ-tason säikeitä. Ohjelmoija käyttää KJ:n ulkopuoleisia prosessitason säikeitä. Prosessitason säikeitä suoritetaan KJ-tason säikeiden sisällä. Kullakin prosessilla voi olla prosessitason säikeitä enemmän kun prosessia varten varattuja KJ-säikeitä. Jos jokin prosessitason säie blokkaa, voidaan muita prosessitason säikeitä suorittaa muiden prosessille varattujen KJ-säikeiden sisällä.

    Säikeiden käyttö

    Usein ohjelma kannattaa rakentaa käyttäen useampia säikeitä. Esim. taulukkolaskenta voitaisiin säikeistää siten, että yksi säie kuuntelee käyttäjän komentoja (kirjoitettu teksti ja valikkojen kautta annetut komennot) Jos talletetaan tiedosto levylle, kannattaa tallentamista varten luoda uusi säie, jotta muu sovellus voi jatkaa häiriintymättä. Kaavojen laskemisen ja siitä johtuvan tietojen päivittämisen voi hoitaa oma säie. Jos käytössä on dokumenttien automaattinen tallennus esim. 10 minuutin välien, tämän voi hoitaa oma 10min välein heräävä säie.

    Nykyisin vallalla oleva prosessoriteknologia eli moniytimiset prosessorit (dual core, quad core) tehostavat yksittäisen sovelluksen suorittamista vain jos sovellus on toteutettu monisäikeisesti (tai useana prosessina). Sovellusten monisäikeisyyteen pitää todennäköisesti tulevaisuudessa panostaa yhä enemmän.

    Sovelluksen toteuttaminen monisäikeisenä on kuitenkin teknisesti erittäin haastavaa. Joitain asioita on helppo laittaa omaksi säikeekseen, kuten esim. dokumentin automaattinen tallettaminen 10 minuutin välein. Paljon CPU-tehoa vaativat tehtävän säikeistäminen onkin usein paljon hankalampaa ellei tehtävää voida pilkkoa helposti toisistaan erillisiksi osiksi. Kaikkein suurin säikeistyksen (ja kaiken muunkin rinnakkaisuutta sisältävän ohjelmoinnin) hankaluus on kuitenkin se, miten säikeet saadaan keskenään synkronoiduksi ja niiden toiminta koordinoitua siten, että säikeiden käyttämä data pysyy koko ajan konsistentissa muodossa ja etteivät säikeet joudu vahingossa ikuiseen odotukseen, eli lukkiumatilaan (engl. deadlock).

    Yksi esimerkki helposti säikeistyvästä ohjelmasta on www-palvelin, joka voidaan toteuttaa säikeiden avulla esim. seuraavasti. Yksi säie kuuntelee asiakkailta tulevia yhteyspyyntöjä. Avattu yhteys ohjataan jollekin työntekijäsäikeistä. Palvelimen nopeutta tehostava sivuvälimuisti on kaikille säikeille näkyvässä muistissa. Työntekijäsäie katsoo ensin onko sivu välimuistissa, jos ei niin tehdään levyoperaatio ja haetaan haluttu sivu. Jos kyseessä on dynaamisten sivujen näyttäminen, työntekijäsäie voi tehdä tarvittaessa kyselyn tietokantaan. Koska jokaista www-pyyntöä hoitaa oma säie, ei esim. yhden sivupyynnön aiheuttama aikaavievä tietokantaoperaatio haittaa muiden yhteyksien palvelemista.

    Palvelimen ohjelmointi ilman säikeitä olisi hankalaa. Linuxilla se tosin hoituisi kohtuullisen hyvin myös prosesseja ja esim. jaettua muistia käyttäen.

    Säikeistettyjen ohjelmien tekeminen

    Säikeiden ohjelmoiminen tapahtuu käyttämällä pthread.h-kirjastoa. Tutkitaan ohjelmaa 11-1.c. Ensin määritellään koodi, mitä säikeet alkavat käynnistyessään suorittamaan. Säie aloittaa aina funktiolla, joka saa yhden void * parametrin sekä palauttaa void * -muotoisen arvon, eli void osoitteen. Säikeen aloitusfunktio voi kutsua mitä tahansa ohjelmassa määriteltyä funktiota. Ohjelma määrittelee kaksi erilaista aloitusfunktiota:
    #include < pthread.h>
    #include < stdio.h>
    
    // säikeen aloitusfunktio, paluuarvo ja ainoa parametri aina tyyppiä void *
    void *ownthread(void *arg){
      printf("Olen säie\n");
    
      pthread_exit(NULL);
    }
    
    void *ownthread2(void *arg){
      printf("Olen erilainen säie\n");
    
      pthread_exit(NULL);
    }
    
    Mainissa käynnistetään kolme säiettä. Käynnistys tapahtuu funktiolla pthread_create(), josta toistaiseksi käytetään vain ensimmäistä ja kolmatta parametria. Ensimmäisenä osoitin pthread_t-tyyppiseen muuttujaan, joka saa arvokseen säietunnisteen. Kolmas parametri on funktio, jota säie alkaa suorittamaan. Tämän jälkeen prosessin oletussäie (eli mainin aloittava suoritus) alkaa odottamaan säikeiden lopettamista funktiolla pthread_join(), jonka ensimmäisenä parametrina säie, jota odotetaan. Tässä vaiheessa emme tarvitse funktioiden muita parametreja ja niiden arvoksi asetettu NULL.
    int main(void){
        // muuttujat säietunnisteille
        pthread_t tid1, tid2, tid3;
    
        // käynnistetään kolme säiettä
        pthread_create( &tid1, NULL, ownthread, NULL );
        pthread_create( &tid2, NULL, ownthread, NULL );
        pthread_create( &tid3, NULL, ownthread2, NULL );
    
        // odotetaan säikeiden lopetusta
        pthread_join( tid1, NULL );
        pthread_join( tid2, NULL );
        pthread_join( tid3, NULL );
    
        return 0;
    }
    
    Säikeitä sisältäviä ohjelmia käännettäessä on gcc:lle annettava optio -lpthread, jotta säikeet toteuttava koodi osataan linkittää mukaan.

    Linuxissa säikeet ovat teknisesti ottaen prosesseja. Siksi jokaisella säikeellä on oma PID. Säikeet saa näkyviin myös ps-komennolla käyttäen optiota -m.

    Esimerkissä 11-2.c luodaan kolme säiettä. Säikeet saavat parametrina kokonaisluvun, jonka arvoa ne tulostavat ruudulle muutaman kerran.

    Tällä kertaa säikeelle välitetään parametri. Parametri on tyypiltään int, joten se on castattava oikeaan tyyppiinsä. Säikeen suorittama funktio tulostaa ruudulle parametrinaan saamansa lukeman 50 kertaa. Säikeen aloitusfunktiolla on kaksi muuttujaa, i ja param. Kaikki funktioiden sisällä määritellyt muuttujat varataan pinon sisältä ja koska kaikilla säikeillä on oma pino, on kaikilla säikeillä funktioiden sisäisistä muuttujista omat kopionsa. Sama pätee myös main-funktion sisällä määriteltyihin muuttujiin. Jaettuihin muuttujiin pääsemme vasta seuraavassa esimerkissä.

    void *ownthread(void *arg){
       // parametri castataan oikeaan tyyppiinsä
       int param = (int)arg;
       int i;
    
       for ( i=0; i < 50000000; i++ )
        if ( i%1000000 == 0 ) {
          printf("%d ",param);
          fflush(stdout);
        }
    
       // fflush varmistaa että tulostettu numero menee heti ruudulle, näin ei
       // prinf:llä välttämättä tapahdu, jos tulostuksessa ei ole rivinvaihtoa
    
       pthread_exit(NULL);
    }
    
    Mainissa säikeet käynnistetään. Säikeille välitettävä parametri tulee pthread_creat:issa viimeiseksi. Kutsun ajaksi parametri castataan tyyppiin void-osoittimeksi.
    int main(void){
        pthread_t tid1, tid2, tid3;
    
        // käynnistetään säie ja annetaan parametriksi int-luku 1
        // funktiokutsun ajaksi parametri castataan tyypiin void *
        pthread_create( &tid1, NULL, ownthread, (void *)1 );
        // lisää säikeitä kaikille oma parametri
        pthread_create( &tid2, NULL, ownthread, (void *)2 );
        pthread_create( &tid3, NULL, ownthread, (void *)3 );
    
        // odotetaan säikeiden lopetusta
        pthread_join( tid1, NULL );
        pthread_join( tid2, NULL );
        pthread_join( tid3, NULL );
    
        printf("\n");
    
        return 0;
    }
    
    Ohjelmassa 11-3.c säikeet käyttävät jaettua muuttujaa. Säie 1 kasvattaa muuttujan arvoa 100000000 kertaa. Säie 2 tulostaa muuttujan arvon 20 kertaa ja viivyttelee välillä. Säikeet käynnistetään yhtä aikaa eli muuttujan kasvatus ja arvon tulostelu etenevät yhtäaikaisesti. Jotta muuttuja näkyy kaikille säikeille, täytyy se määritellä funktioden ulkopuolella.
    #include 
    #include 
    
    int x;  // globaali muuttuja, jonka kaikki säikeet näkevät
    
    // säie 1 kasvattaa muuttujan x arvoa
    void *th1(void *arg){
       int i;
       printf("säie 1 aloittaa\n");
    
       for( i=0; i < 100000000; i++ )
          x++;
    
       printf("säie 1 valmis\n");
       pthread_exit(NULL);
    }
    
    // säie 2 tulostaa x:n arvon 20 kertaa viivytellen välillä
    void *th2(void *arg){
       int i;
       printf("säie 2 aloittaa\n");
    
       for( i=0; i < 10000000; i++)
           if ( i%500000==0 ) printf("x = %d\n",x);
    
       printf("säie 2 valmis\n");
       pthread_exit(NULL);
    }
    
    Pääohjelma nollaa muuttujan aluksi, käynnistää säikeet ja säikeiden lopetettua tulostaa muuttujan arvon.
    int main(void){
        pthread_t tid1, tid2;
    
        // oletussäie nollaa jaetun muuttujan x
        x = 0;
        pthread_create( &tid1, NULL, th1, NULL );
        pthread_create( &tid2, NULL, th2, NULL );
    
        pthread_join( tid1, NULL );
        pthread_join( tid2, NULL );
    
        printf("lopussa x = %d\n",x);
    
        return 0;
    }
    
    Koska vain yksi prosessi kirjoittaa jaettuun muuttujaan, ei muuttujan yhteiskäytöstä synny ongelmia. Jos molemmat muuttaisivat muuttujan arvoa samaan tapaan kun prosessit esimerkissä 9-3.c olisi tilanne toinen ja säikeiden olisi suojattava jaettu muuttuja. Palaamme jaettujen muuttujien suojaamiseen ja säikeiden synkronointitapoihin seuraavalla viikolla.

    Funktioiden ulkopuolella määritellyt globaalit muuttujat siis näkyvät kaikille säikeille. Globaalien muuttujien käyttäminen ei kuitenkaan ole yleensä kovin viisasta. Parempi vaihtoehto säikeiden jakamien muuttujien tallentamiselle on varata muistialue mallocilla ja välittää osoitin varattuun muistialueelle kaikille sitä tarvitseville säikeille.

    Näin toimitaan ohjelmassa 11-4.c, missä säikeet käsittelevät oletussäikeen mallocilla varaamaa 1000000 paikkaista taulukkoa.

    Ohjelmassa on kaksi säiettä, joista toinen etsii taulukon pienimmän ja toinen suurimman luvun. Jaetun taulukon alun osoite välitetään säikeille parametrina.

    Säikeet palauttavat löydetyn pienimmän ja suurimman arvon oletussäikeelle lopetuksen yhteydessä funktion pthread_exit parametrina. Parametrin tyyppi on void-osoitin, joten paluuarvo on castattava kutsun ajaksi.

    // säie etsii pienimmän arvon parametrina saamastaan globaalista taulukosta
    // arvo palautetaan säikeen lopetuksen yhteydessä
    void *min(void *arg){
       int *values = (int *)arg;
       int i;
       int min = values[0];
    
       for ( i=0; i < N; i++ )
          if ( values[i] < min ) min = values[i];
    
       // paluuarvon tyypin oltava void *, joten int-arvo castataan
       pthread_exit( (void *)min );
    }
    
    // säie etsii suurimman arvon parametrina saamastaan globaalista taulukosta
    // arvo palautetaan säikeen lopetuksen yhteydessä
    void *max(void *arg){
       int *values = (int *)arg;
       int i;
       int max = values[0];
    
       for ( i=0; i < N; i++ )
          if ( values[i] > max ) max = values[i];
    
       // paluuarvon tyypin oltava void *, joten int-arvo castataan
       pthread_exit( (void *)max );
    }
    
    Oletussäie luo aluksi taulukon ja arpoo sen täyteen lukuja. Tämän jälkeen luodaan säikeet suorittaman suurimman ja pienimmän luvun etsiminen. Säikeiden paluuarvo otetaan vastaan funktion pthread_join toisena parametrina.
    int main(void){
        pthread_t tid1, tid2;
        int i, smallest, greatest;
    
        // luodaan taulukko
        int *values;
        values = malloc( sizeof(int)*N);
    
        // alustetaan satunnaislukugeneraattori
        srand( time(NULL) );
    
        // arvotaan taulukko täyteen lukuja väliltä 0-9999999
        for ( i=0; i < N; i++ )
          values[i] = rand()%10000000;
    
        // taulukon osoite välitetään säikeille parametrina
        pthread_create( &tid1, NULL, min, (void *)values );
        pthread_create( &tid2, NULL, max, (void *)values );
    
    
        pthread_join( tid1, (void **)&smallest );
        pthread_join( tid2, (void **)&greatest );
    
        printf("taulukon pienin %d ja suurin %d\n", smallest, greatest);
    
        return 0;
    }
    
    Säikeen lopetuksen yhteydessä palauttaman arvon vastaanottaminen tapahtuu mielenkiintoisella tavalla. pthread_join:in toinen parametri on tyypiltään void ** eli osoitin void osoittimeen. Tämä mahdollistaa sen, että säie voi palauttaa osoitteen, joka täytyy ottaa vastaan osoittimena osoitteeseen. Tällä kertaa paluuarvo on normaali int, joten funktiolle annetaan parametriksi osoite muuttujaan, johon paluuarvo sijoitetaan, eli &smallest, jotta kääntäjä on tyytyväinen, on tämä vielä castattava void **-tyypiin.

    Jos säie palauttaisi osoittimen, esim. dynaamisesti varatun int-taulukon, toimittaisiin ohjelman 11-5.c tapaan:

    void *saie(void *arg){
      int *t;
    
      // luodaan dynaamisesti neljän kokoinen int-taulukko
      t = (int *)malloc( sizeof(int)*4 );
    
      // täytetään taulukko
      t[0] = 2;
      t[1] = 5;
      t[2] = 3;
      t[3] = 8;
    
      // palautetaan osoite varattuun muistialueeseen
      pthread_exit( (void *)t );
    }
    
    int main(void){
      pthread_t tid;
      int i;
      int *t;
    
      pthread_create( &tid, NULL, saie, NULL );
    
      // otetaan vastaan palautettu muistiosoite, parametri int-osoitteen osoite
      pthread_join( tid, (void **)&t );
    
      // tulostetaan taulukko
      for ( i=0; i < 4; i++ )
        printf("%d\n", t[i]);
    
      return 0;
    }
    
    Eli pthrea_join:issa parametriksi annetaan osoite osoitinmuuttujaan. Näin osoitinmuuttuja saa arvokseen säikeen palauttaman osoitteen. HUOM: säie ei saa palauttaa osoitetta sisäiseen muuttujaansa! Osoitteen on oltava joko mallocilla varattuun alueeseen tai globaaliin (eli funktioiden ulkopuolella määriteltyyn) dataan.