ITF225129 Innføring i operativsystemerObligatorisk oppgave 6: SynkroniseringI denne obligatoriske oppgaven skal vi fortsette med C-programmering med pthreads i Linux. Vi skal se på hvordan tråder kan samarbeide om å løse en oppgave. Dette krever at trådene synkroniseres — de må kunne vente på hverandre, kommunisere og dele ressurser. Spesielt må vi unngå at trådene "ødelegger" for hverandre når de leser og skriver i de samme delene av minnet Synkronisering av tråder er kanskje noe av det vanskeligste å mestre innenfor programmering. Vi skal holde oss til relativt enkle eksempler, og bare se på noen grunnleggende mekanismer for synkronisering. Oppgaveteksten her består av to deler:
1 Synkronisering i pthreadsI den obligatoriske oppgaven om trådprogrammering har du lært hvordan vi oppretter og avslutter en tråd i pthreads, ved å bruke pthread_create() og pthread_exit(). Vi har også sett hvordan en tråd/prosess kan vente på at en annen tråd blir ferdig, med pthread_join(). For å lage mer generelle programmer som utnytter tråder bedre, trenger vi også at trådene både kan "snakke" sammen og stoppe/starte hverandre mens de fortsatt eksekverer. I flere av eksemplene vi har sett tidligere, bruker vi tråder som bare starter og kjører ferdig en funksjon. Det vil da ofte være bedre (og enklere) å bare kalle funksjonen på vanlig måte, i stedet for å lage en ny tråd for å kjøre den. Eksemplet med matrisemultiplikasjon, der hver tråd fikk "ansvaret" for å beregne "sin del" av matriseproduktet, er et bedre eksempel på utnyttelse av tråder. Hvis maskinen har flere CPU'er som kjører hver sin tråd parallelt, vil kjøretiden her kunne reduseres betydelig for store matriser. Det er allikevel et relativt enkelt eksempel, siden trådene arbeider helt uavhengig av hverandre i adskilte deler av minnet, og ikke trenger å kommunisere eller synkroniseres. Når vi bruker tråder til å håndtere større og mer kompliserte problemer, må vi først finne en oppdeling av problemet i mindre delproblemer som hver tråd kan løse. Resultatene fra hver tråd settes da til slutt sammen til en fullstendig løsning. For mer generelle problemer enn f.eks. matrisemultiplikasjon vil det oftest være slik at:
I resten av denne oppgaven skal vi se eksempler på hvordan vi kan få til trådsynkronisering i pthreads ved å bruke de to mekanismene mutexer og betingede variable. For en bedre og mer detaljert beskrivelse av ulike problemer og løsninger knyttet til synkronisering, se læreboken og læringsmodulen om interprosesskommunikasjon i Canvas. 1.1 Race conditionsEn såkalt "race condition" oppstår når:
Race conditions bør unngås fordi de kan gjøre programvaren uforutsigbar og gi feil resultater. Debugging kan også være svært vanskelig hvis feilene skyldes at trådene deler minne. Et svært enkelt eksempel på et C-program med en race condition er gitt nedenfor:
Dette programmet oppretter ti tråder som kjøres parallelt. Hver tråd starter funksjonen skriv_sum(), som kjører en løkke der en global delt variabel tall "telles opp" fra 1 til n. Verdien på n, som også er en delt variabel, leses fra bruker. Merk at alle de ti trådene vil prøve å oppdatere tall "samtidig" — vi får en race condition på denne variabelen! Dvs. at trådene her kan ødelegge for hverandre, slik at ingen av dem produserer "riktig" resultat. Kompiler og kjør dette programmet selv, først for små verdier av n (f.eks. n=100). Du vil da se at alle summene blir "riktige". Det er fordi hver tråd får kjøre helt ferdig, før den har brukt opp sin "timeslice" på noen tusendels sekunder i CPU og blir byttet ut med neste tråd som skal kjøre (vi skal forklare timeslices og timesharing nøyere senere i kurset). Kjør deretter programmet med f.eks. verdien 1000000 (en million) for n. Du vil da se at verdiene som skrives ut ser helt tilfeldige ut, og vil variere mellom hver kjøring. Dette skjer fordi trådene nå må bytte på å bruke CPU'en(e) før de er ferdige. Hver gang en tråd skriver en verdi til tall ødelegger den for de andre trådene som nå ikke lenger har riktig resultat. Vi har en race condition som gjør programmet både uforutsigbart og feil. 1.2 Kritiske områderEt såkalt kritisk område i koden for et trådprogram, er den delen av koden hvor en race condition oppstår. Som oftest er dette kode som kan gjøre at flere tråder prøver å lese eller skrive samme del av minnet samtidig. I C-eksemplet ovenfor ligger det kritiske området i koden inne i funksjonen skriv_sum(), der trådene oppdaterer og skriver ut verdien av den delte variabelen tall. Det er kun her at trådene jobber mot samme minne, i resten av programmet er det ingen race conditions og trådene kan trygt kjøre parallelt. Noe av "kunsten" med å programmere med tråder er å kunne identifisere de delene av koden som er kritiske områder, og prøve å gjøre disse små. Deretter må vi håndtere dem riktig for å unngå at det oppstår race conditions. 1.3 Gjensidig utelukkelseFor å unngå race conditions i trådprogrammering kan man bruke såkalt "gjensidig utelukkelse" ("mutual exclusion"):
Hvis de kritiske områdene i et program er store og krever mye kjøretid, vil effektiviseringen vi får av å bruke flere tråder være dårlig. Dette fordi den kritiske koden kjøres sekvensielt (en tråd om gangen) når vi bruker gjensidig utelukkelse. Trådene vil gå mye på "tomgang" (være "idle") mens de venter på å få gå inn i det kritiske området. Det finnes ulike verktøy i forskjellige OS og programmeringsspråk for å unngå race conditions. Vi skal i denne oppgaven bare se på et par mekanismer i pthreads som kan brukes til gjensidig utelukkelse. Se læreboken og læringsmodulene i Canvas for (mye) mer om hvordan man kan få til "mutual exclusion". 1.4 Bruk av mutex i pthreadsTrådbiblioteket pthreads tilbyr en egen type "låsvariabel", en såkalt mutex, for å få til "mutual exclusion". En mutex kan brukes til å blokkere/stoppe tråder som er på vei inn i et kritisk område, og vekke/starte dem igjen når kritisk område ikke lenger er opptatt av andre tråder. Mutexer er implementert i OS på en slik måte at det ikke kan bli en race condition på selve mutex-variabelen. En mutex er en C-variabel av datatypen pthread_mutex_t. Her deklarerer/oppretter vi en mutex som får variabelnavnet lock:
Vi bør alltid initialisere mutexer før bruk. Det er en egen metode for dette i pthreads som heter pthread_mutex_init(). Vi kommer til å bruke funksjonen på denne måten for f.eks. å initialisere mutexen lock:
Tråder som skal samarbeide om å unngå race conditions i et kritisk område, vil dele på en og samme mutexvariabel. Vi bruker vanligvis en mutex på denne måten:
I avsnitt 1.1. ovenfor så vi et enkelt eksempel på en race condition som ga uforutsigbar og feil oppførsel av programmet. Nedenfor er det samme eksemplet, men her brukes det en mutex til å sørge for at bare én tråd av gangen får kjøre den kritiske koden:
I koden ovenfor ser vi at mutex-variabelen lock deklarereres globalt, slik at den er tilgjengelig for alle trådene. Hovedprogrammet main() kaller pthread_mutex_init() for å initialisere mutexen, og pthread_mutex_destroy() etter at trådene er ferdige. Hver tråd vil kalle pthread_mutex_lock() i funksjonen skriv_sum(), før eksekveringen av kritisk kode starter. Når trådene forlater det kritiske området, kaller de pthread_mutex_unlock(). Kjør dette programmet selv og se hvordan det virker. Du vil da se at trådene ikke lenger "ødelegger" hverandres resultater. I Canvas-modulen for interprosesskommunikasjon vil du finne flere eksempler på bruk av mutex for å løse mer kompliserte problemer. 1.5 Betingede variablerI forrige avsnitt brukte vi en mutex for å sikre at bare én tråd om gangen får kjøre i et kritisk område, slik at vi unngår race conditions. For å få tråder til å samarbeide om å løse problemer trenger vi mer generelle mekanismer for synkronisering. En slik mekanisme i pthreads er betingede variabler, som kan brukes for å sikre at en tråd venter med å kjøre videre inntil en bestemt betingelse er oppfylt. En betinget variabel ("conditional variable") tilhører datatypen pthread_cond_t som er definert i pthreads.h. Opprettelse av en betinget variabel med navn cond_var gjøres slik i C:
Betingede variabler skal initialiseres før de brukes, med et kall på pthread_cond_init():
Når vi er ferdig med å bruke en betinget variabel, kan vi fjerne den igjen ved å bruke:
En tråd kan "blokkere på en betinget variabel" (stoppe eksekvering) ved selv å kalle funksjonen pthread_cond_wait(). Tråden kan vekkes opp/startes igjen ved at en annen tråd kaller funksjonen pthread_cond_signal(). Her er et lite eksempel som bruker to tråder, en mutex og en betinget variabel:
Programmet ovenfor fungerer på følgende måte:
Kjør programmet selv og se hvordan det fungerer. Nedenfor er det gitt mer utfyllende beskrivelser av hvordan de to funksjonene pthread_cond_wait() og pthread_cond_signal() virker. For enda mer informasjon om metodene, se f.eks. man pthread_cond_wait og man pthread_cond_signal på itstud.hiof.no. 1.5.1 Blokkere på en betinget variabel: pthread_cond_wait()For at en betinget variabel skal fungere sikkert og pålitelig i pthreads, er det viktig at variabelen alltid brukes sammen med en mutex. Mutexen brukes til å beskytte håndtering av den betingede variabelen, slik at vi ikke får en race condition. Anta at cond_var er en betinget variabel og at lock er en mutex. For å få en tråd til å blokkere (vente på den betingede variabelen), brukes følgende kall:
Før dette kallet skal mutexen lock låses med et kall på pthread_mutex_lock(). Et kall på pthread_cond_wait() fungerer slik:
Flere tråder kan vente på samme betingede variabel, med ulike betingelser for å gå videre. Det kan hende at en tråd frigis fra en blokkering på en betinget variabel uten at betingelsen den venter på faktisk er oppfylt (se virkemåten for pthread_cond_signal() beskrevet nedenfor). Det er derfor vanlig (men ikke nødvendig) å legge kallet på pthread_cond_wait() i en løkke som sjekker om tråden faktisk kan gå videre fordi betingelsen den venter på er oppfylt. Standard kode for å blokkere på en betinget variabel ser omtrent slik ut:
Merk at denne måten å bruke en betinget variabel på henger nøye sammen med hvordan tråden vekkes opp igjen med pthread_cond_signal().
1.5.2 Vekke opp en tråd som er blokkert på en betinget variabel: pthread_cond_signal()For å vekke opp (starte igjen) en tråd som er blokkert på en betinget variabel cond_var, kan en annen tråd kalle funksjonen pthread_cond_signal():
Kallet på pthread_cond_signal() sender et "oppvåknings-signal" til den betingede variabelen. Dette fungerer slik:
Funksjonen pthread_cond_signal() brukes vanligvis slik:
Det er igjen viktig å låse koden med en mutex før kallet på pthread_cond_signal(). Hvis dette ikke gjøres, kan vi få en race condition og risikerer å miste signalet. 1.5.3 Forenklet producer/consumerI læreboken og i læringsmodulen i Canvas brukes "producer/consumer"-problemet for å illustrere bruk av mutexer og betingende variable. Nedenfor er en enklere variant av problemet, der produsenten bare produserer ett "item" om gangen, og konsumenten alltid forbruker dette før neste kan produseres:
Programmet ovenfor bruker bare en enkel variabel buffer, som enten vil være lik null eller en, til å "lagre" et produsert "item". Systemet som programmet implementerer har følgende regler:
Merk også at det er lagt inn et tilfeldig "tidsforbruk" for hver gang det produseres eller konsumeres, får å få litt forsinkelse og dynamikk i systemet. En utskrift fra dette programmet, der nummeret på siste item skrives ut, ser slik ut:
Kjør producer/consumer programmet selv. Legg merke til hvordan de to trådene bruker den betingede variabelen til hele tiden å stoppe og starte hverandre. 2 OppgaverNedenfor er det gitt tre oppgaver som skal besvares med både vanlig tekst og C-kode. Oppgave 1Følgende C-program bruker to tråder som begge "teller opp" og "teller ned" samme delte variabel count. Første tråd legger til 1 på variabelen 100 millioner ganger, andre tråd trekker 1 fra variabelen 100 millioner ganger:
Oppgave 2Programmet nedenfor bruker to mutexer:
Hovedprogrammet main() initaliserer her de to mutexene, og oppretter så to tråder. Første tråd starter med å kjøre funksjonen print1(), andre tråd kjører print2(). Deretter går main() inn i en evig løkke som i hver iterasjon skriver ut et punktum og deretter "sover" i et tidels sekund. Kjør dette programmet selv. Du vil da se at ingen av de to trådene skriver ut noen meldinger, selv om begge kaller printf()! Spørsmål: Gi en forklaring på hvorfor ingen av trådene får eksekvere printf(). Oppgave 3Programmet nedenfor leser først antall tråder som skal opprettes, n, fra bruker. Deretter startes n tråder som alle kjører funksjonen terningkast():
Hver av de n trådene kjører en evig løkke. I hvert gjennomløp av løkken "kastes en terning" ved å velge tilfeldig ett av tallene 1, 2, 3, 4, 5 eller 6. "Terningkastet" skrives deretter ut og tråden "sover" en liten stud. Merk at utskriften beskyttes med en mutex, slik at trådene ikke overskriver hverandres output. Når du kjører dette programmet får du en utskrift som f.eks. kan se slik ut med n=5 tråder:
Hva skal du levere?Svarene på oppgave 1a, oppgave 2 og oppgave 3b skal leveres sammen i et PDF-dokument med filnavn oblig_6.pdf. C-koden fra oppgave 1b og 3a skal leveres på to filer som heter oppgave_1.c og og oppgave_3.c. PDF-dokumentet og C-filene skal lastes opp i Canvas som svar på oppgaven. |