Programmering i Ruby

Den Pragmatiske Programmerers Veiledning

Forrige < Innhold ^
Neste >

Tråder og prosesser



Ruby gir deg to grunnleggende måter å organisere ditt program slik at du kan kjøre forskjellige deler av programmet "samtidig". Du kan splitte ut samarbeidende oppgaver innen programmet, ved å bruke tråder, eller du kan splitte oppgaver mellom forskjellige programmer, ved å bruke prosesser. La oss se på hver av dem etter tur.

Kjøring av flere tråder

Ofte er den enkleste måten å gjøre to ting på en gang å bruke Ruby-tråder. Disse kjører internt i prosessen, implementert inne i Ruby-interpreteren. Det gjør Ruby-tråder fullstendig portable---du er ikke avhengig av operativsystemet---men du kan få visse fordeler å ha tråding innebygd i det underliggende operativsystemet. Du kan oppleve tråd-begrensning (det er når en lav-nivå tråd ikke får en sjanse til å kjøre). Hvis du klarer å få dine tråder låst, kan hele prosessen stoppe opp. Og hvis en tråd skulle gjøre et kall til operativsystemet som tar lang tid å gjøre ferdig, vil alle thråder henge til interpreteren får kontrollen tilbake. Imidlertid, ikke la disse potensielle problemene ta motet fra deg---Ruby-tråder er en lett og effektiv måte å oppnå parallellitet i koden din.

Lage Ruby-tråder

Å lage en ny tråd er ganske ukomplisert. Her er et enkel kode-fragment som laster ned et sett av web-sider i parallell. For hver henvendelse den får, lager koden en separat tråd som håndterer HTTP-transaksjonen.
require 'net/http'

pages = %w( www.rubycentral.com             www.awl.com             www.pragmaticprogrammer.com            )

threads = []

for page in pages   threads << Thread.new(page) { |myPage|

    h = Net::HTTP.new(myPage, 80)     puts "Fetching: #{myPage}"     resp, data = h.get('/', nil )     puts "Got #{myPage}:  #{resp.message}"   } end

threads.each { |aThread|  aThread.join }


        
produserer:
Fetching: www.rubycentral.com
Fetching: www.awl.com
Fetching: www.pragmaticprogrammer.com
Got www.rubycentral.com:  OK
Got www.pragmaticprogrammer.com:  OK
Got www.awl.com:  OK

La oss se nærmere på denne koden, da det er noen få subtile ting som foregår.

Ny tråder blir laget med Thread.new -kallet. Det blir gitt en blokk som inneholder koden som skal kjøres i den nye tråden. I vår tilfelle bruker blokken net/http-biblioteket for å få tak i topp-siden fra hver av våre nominerte sider. Vår sporing viser klar at disse hentingene går parallellt.

Når vi lager tråden sender vi den ønskede HTML-siden inn som en parameter. Denne parameteren blir sendt inn til blokken som myPage. Hvorfor gjør vi det, i stedet for å bruke verdien til variabelen page i blokken?

En tråd deler alle globale, instans- og lokale variabler som eksisterer på det tidspunktet tråden startes. Som alle med en lillebror kan fortelle deg, så er deling ikke alltid en god ting. I dette tilfellet ville alle disse tre trådene dele variabelen page. Den første tråden starter, og page blir satt til http://www.rubycentral.com. I mellomtiden så går løkken som starter trådene. Andre gangen blir page satt til http://www.awl.com. Hvis den første tråden ikke har avsluttet med å bruke page-variabelen så vil den plutselig starte å bruke sin nye verdi. Slike feil er vanskelige å spore opp.

Imidlertid så er lokale variabler som blir laget innen en tråds blokk virkelige lokale for den tråden---hver tråd vil ha sin egen kopi av disse variablene. I vårt tilfelle vil variabelen myPage bli satt på tidspunktet trådenen blir startet, og hver tråd vil ha sin egen kopi av side-adressen.

Manipulering av tråder

En annen spissfindighet kommer på siste linjen i programmet. Hvorfor kaller vi join på hver av de trådene vi laget?

Når et Ruby-program terminerer blir alle løpende tråder avbrutt, uavhengig av deres tilstander. Imidlertid kan du vente på at en spesiell tråd skal avsluttes ved å kalle trådens Thread#join -metode. Den kallende tråden vil blokkere til den angitte tråd er ferdig. Ved å kalle join på hver av forespørselstrådene kan du forsikre at alle tre forespørslene har avsluttet før du terminerer hovedprogrammet.

I tillegg til join, er det noen få andre hendige rutiner som blir benyttet til å manipulere tråder. Først av alt, den nåværende tråden er alltid tilgjengelig ved å bruke Thread.current . Du kan få en liste over alle Thread-objekter som er kjørbare eller stopppet. For å bestemme status på en enkelt tråd can du bruke Thread#status og Thread#alive? .

Du kan også justere prioriteten til en tråd ved å bruke Thread#priority= . Høyprioritetstråder vil bli kjørt foran lavprioritetstråder. Vi vil snakke mer om trådfordeling og stopping og starting av tråder om kort tid.

Tråd-variabler

Som vi har beskrevet i den forrige seksjonen, kan en tråd normalt aksessere alle variabler som er innen rekkevidde når tråden blir laget. Variabler lokale for en tråd-blokk er lokale til tråden, og er ikke delte.

Men hva hvis du trenger tråd variabler som kan bli aksessert av andre tråder--- inkludert hoved-tråden? Thread inneholder en spesiell fasilitet som tillater tråd-lokale variabler å bli laget og aksessert ved navn. Du behandler ganske enkelt tråd-objektet som om det var en Hash, som skriver til elementene ved å bruke []= og leser dem tilbake ved å bruke []. I dette eksemplet registrerer hver tråd den nåværende verdien til variabelen count i en tråd-lokal variabel med nøkkelen mycount. (Det er et spesielt forhold (race condition) i denne koden, men vi har ikke snakket om synkronisering ennå, så vi ignorerer det foreløpig):

count = 0
arr = []
10.times do |i|
  arr[i] = Thread.new {
    sleep(rand(0)/10.0)
    Thread.current["mycount"] = count
    count += 1
  }
end
arr.each {|t| t.join; print t["mycount"], ", " }
puts "count = #{count}"
produserer:
8, 0, 3, 7, 2, 1, 6, 5, 4, 9, count = 10

Hvoedtråden venter på sub-trådene for å avslutte og så skriver den ut verdiene til count som ble fanget opp av hver enkelt. For å gjøre det mer interessant har vi satt hver tråd til å vente en tilfeldig tid før verdien registreres.

Tråder og unntak

Hva skjer hvis en tråd framkaller et uhåndtert unntak? Det avhenger av settingen til abort_on_exception-flagget som er dokumentert på sidene 384 og 387.

Hvis abort_on_exception er false, som er standard, vil et uhåndtert unntak bare drepe den nåværende tråden---alle de andre vil forsette å kjøre. I det følgende eksempelet går tråd nummer 3 i lufta og feiler i å produsere noe output. Imidlertid kan du fortsatt se sporet fra de andre trådene.

threads = []
6.times { |i|
  threads << Thread.new(i) {
    raise "Boom!" if i == 3
    puts i
  }
}
threads.each {|t| t.join }
produserer:
01
2
45

prog.rb:4: Boom! (RuntimeError) from prog.rb:8:in `join' from prog.rb:8 from prog.rb:8:in `each' from prog.rb:8

Imidlertid sett abort_on_exception til true og et uhåndtert unntak vil drepe alle tråder som kjører. Med en gang tråd 3 dør, vil det ikke bli produsert noe output.

Thread.abort_on_exception = true
threads = []
6.times { |i|
  threads << Thread.new(i) {
    raise "Boom!" if i == 3
    puts i
  }
}
threads.each {|t| t.join }
produserer:
01
2
prog.rb:5: Boom! (RuntimeError)
	from prog.rb:7:in `initialize'
	from prog.rb:7:in `new'
	from prog.rb:7
	from prog.rb:3:in `times'
	from prog.rb:3

Å kontrollere tråd-kjøreren

I en veldesignet applikasjon vil du normalt bare la trådene gjøre sin ting; å bygge tidsavhengigheter inn i en flertrådet applikasjon er generelt sett på som en dårlig ide.

Imidlertid kan det noen ganger være nødvendig å kunne kontrollere trådene. Kanskje jukeboksen har en tråd som viser et lys-sjov. Vi trenger kanskje å stoppe det temporært når musikken stopper. Du må kanskje ha to tråder i et klassisk produsent-konsument-forhold, hvor konsumenten må ta en pause hvis produsenten kommer på etterskudd.

Klassen Thread tilbyr en rekke metoder for å kontrollere tråd-kjøreren. Ved å kalle Thread.stop stoppes den tråden som nå kjører, mens Thread#run setter opp for at en spesiell tråd skal bli kjørt. Thread.pass pauser den nåværenede tråden slik at andre tråder kan få kjøre. Thread#join og Thread#value suspenderer den kallende tråden inntil den angitte tråden avsluttes.

Vi kan demonstrere disse egenskapene i det følgende, meningsløse programmet.

t = Thread.new { sleep .1; Thread.pass; Thread.stop; }
t.status » "sleep"
t.run
t.status » "run"
t.run
t.status » false

Imidlertid, det å bruke disse primitivene for å oppnå noen form for ekte synkronisering er på sitt beste et skudd i blinde. Det vil alltid være spesielle tilfeller som venter på å bite deg. Når du arbeider med data delt mellom tråder, vil spesielle tilfeller garantert medføre lange og frustrerende avlusingsøkter. Heldigvis har tråder en ekstra fasilititet---kritiske seksjoner. Ved å benytte denne kan vi bygge en mengde sikre synkroniseringsmekanismer.

Felles ekslusjon

Den metoden for å blokkere andre tråder fra å kjøre, som operer på det laveste nivået, bruker en global "kritisk tråd"-tilstand. Når tilstanden blir satt til true (ved å bruke Thread.critical= -metoden) vil tråd-kjøreren ikke sette igang noen eksisterende tråd for kjøring. Imidlertid vil ikke dette hindre nye tråder i fra å bli laget og kjørt. Visse tråd-operasjoner (som å stoppe eller drepe en tråd, få den nåværende tråden til å sove, eller å fremme et unntak) kan få en tråd til å bli satt opp for kjøring selv når man er i en kritisk seksjon.

Det er mulig å bruke Thread.critical= direkte, men det er ikke særlig praktisk. Heldigvis har Ruby flere alternativer. To av de beste alternativene er klassen Mutex og klassen ConditionVariable, som er tilgjengelig i thread-biblioteksmodulen. Se dokumentasjonen som begynner på side 457.

Mutex-klassen

Mutex er en klasse som implementerer en enkel semafor-lås for å gi ekslusiv tilgang til en delt ressurs. Det betyr at kun en tråd kan holde låsen på et gitt tidspunkt. Andre tråder må velge mellom å vente i kø på at låsen skal bli tilgjengelig, eller å få en umiddelbar feil som indikerer at låsen ikke er tilgjengelig.

En mutex blir ofte brukt når oppdatering på delte data trenger å være atomisk. La oss si at vi trenger å oppdatere to variabler som del av en transaksjon. Vi kan simulere dette i et trivielt program ved å inkrementere noen tellere. Oppdateringene er forventet å være atomiske---resten av verden skal aldri se tellerne med forskjellige verdier. Uten noen type mutex-kontroll vill ikke dette virke.

count1 = count2 = 0
difference = 0
counter = Thread.new do
  loop do
    count1 += 1
    count2 += 1
  end
end
spy = Thread.new do
  loop do
    difference += (count1 - count2).abs
  end
end
sleep 1
Thread.critical = 1
count1 » 189263
count2 » 189263
difference » 90500

Dette eksempelt viser at "spy"-tråden våknet opp flere ganger og fant ut at verdiene for count1 og count2 var inkonsistente.

Heldigvis kan vi fikse dette ved å bruke en mutex.

require 'thread'
mutex = Mutex.new

count1 = count2 = 0 difference = 0 counter = Thread.new do   loop do     mutex.synchronize do       count1 += 1       count2 += 1     end   end end spy = Thread.new do   loop do     mutex.synchronize do       difference += (count1 - count2).abs     end   end end

sleep 1
mutex.lock
count1 » 17075
count2 » 17075
difference » 0

Ved å plassere alle tilganger til de delte dataene under kontroll av en mutex sikrer vi konsistens. Uheldigvis går dette også utover ytelsen, som du kan se utifra tallene.

Tilstands-variabler

Noen ganger er det ikke tilstrekkelig å bruke en mutex for å beskytte kritiske data. Forutsett at du er i kritisk seksjon men du trenger å vente på en spesiell ressurs. Hvis din tråd går i dvale for å vente på denne ressursen, er det mulig at ingen av de andre trådene kan frislippe denne ressursen fordi de ikke kan gå inn i den kritiske seksjonen---den opprinnelige prosessen har fortsatt låsen. Du trenger å kunne gi opp din ekslusive tilgang til det kritiske området og samtidig gi beskjed om at du venter på en ressurs. Når ressursen blir tilgjengelig må du få tak i den og få tilbake låsen på den kritiske regionen, alt på en gang.

Her kommer tilstands-variable inn. En tilstands-variabel er ganske enkelt en semafor som er assosiert med en ressurs og brukes i forbindelse med beskyttelse av en bestemt mutex. Når du trenger en ressurs som er utilgjengelig venter du på tilstands-variabelen. Den handlingen frigir låsen på den korresponderende mutexen. Når en annen tråd signaliserer at ressursen er tilgjengelig kommer den originale tråden ut av ventingen og simultant får tilbake låsen på den kritiske regionen.

require 'thread'
mutex = Mutex.new
cv = ConditionVariable.new

a = Thread.new {   mutex.synchronize {     puts "A: I have critical section, but will wait for cv"     cv.wait(mutex)     puts "A: I have critical section again! I rule!"   } }

puts "(Later, back at the ranch...)"

b = Thread.new {   mutex.synchronize {     puts "B: Now I am critical, but am done with cv"     cv.signal     puts "B: I am still critical, finishing up"   } } a.join b.join
produserer:
A: I have critical section, but will wait for cv(Later, back at the ranch...)

B: Now I am critical, but am done with cv B: I am still critical, finishing up A: I have critical section again! I rule!

For alternative implementeringer for synkronisering, se monitor.rb og sync.rb i lib-underkatalogen til distribusjonen.

Å kjøre flere prosesser

Noen ganger vil du ønske å splitte en oppgave opp i biter som passer som egne prosesser---eller kanskje du trenger å kjøre en separat prosess som ikke var skrevet i Ruby. Ikke noe problem: Ruby har flere metoder som du kan bruke til å sette igang og håndtere separate prosesser.

Sette i gang nye prosesser

Det er mange måter å sette igang en separat prosess. Den enkleste er å kjøre en kommando og vente på at den blir ferdig. Du kunne finne på å gjøre dette for å kjøre en separat kommando eller hente data fra verts-systemet. Ruby tilbyr dette med metodene system og `` (backquote).

system("tar xzf test.tgz") » true
result = `date`
result » "Sun Nov 25 23:43:35 CST 2001\n"

Metoden Kernel::system kjører den gitte kommandoen i en subprosess; den returnerer true hvis kommandoen ble funnet og kjørt skikkelig, ellers false. Ved feil vil du finne subprosessens utgangskode i den globale variabelen $?.

Et problem med system er at kommandoens output vil ganske enkelt gå til samme destinasjon som ditt programs output, hvilket ikke nødvendigvis er ønskelig. Får å fange standard-outputen til en sub-prosess kan du bruke backquotes som med `date` i det foregående eksempelet. Husk at du kan trenge å fjerne linjeskift-tegnene fra resultatet ved å bruke String#chomp metoden.

Ok, dette er greitt nok for enkle tilfeller---vi kan kjøre en annen prosess og få retur-statusen. Men ofte behøver vi litt mer kontroll enn som så. Vi ønsker å kunne ha en samtale med subprosessen, og muligens både sende data til den og motta data tilbake. Metoden IO.popen gjør akkurat dette. popen-metoden kjører en kommando som en sub-prosess og kobler seg til den subprosessens standard input og standard output til et Ruby IO-objekt. Skriv til IO-objektet og subprosessen kan lese det på standard input. Hva enn sub-prosessen skriver er tilgjengelig i Ruby-programmet ved å lese det fra IO-objektet.

For eksempel er et av de mer nyttige verktøyene på systemene våre pig, et program som leser ord fra standard input og skriver dem i "pig Latin" (en engelsk variant av "røverspråk"). Vi kan bruke dette når våre Ruby-programmer trenger å sende oss output som vår femåring ikke skal forstå.

pig = IO.popen("pig", "w+")
pig.puts "ice cream after they go to bed"
pig.close_write
puts pig.gets
produserer:
iceway eamcray afterway eythay ogay otay edbay

Dette eksemplet illustrerer både den åpenlyse enkelheten og virkelighetens kompleksitet ved å drive subprosesser gjennom rør. Koden ser ganske enkel ut: åpne et rør, skriv en frase, og les tilbake svaret. Men det viser seg at pig-programmet ikke skyller ut outputen det skriver. Vårt første forsøk på dette eksempelet, som hadde et pig.puts fulgt av et pig.gets, hang for alltid. pig-programmet prosesserte vår input, men svaret ble aldri skrevet til røret. Vi måtte legge inn pig.close_write-linjen. Dette sender en slutt-på-fil beskjed til pig sin standard input, og når pig terminerer skylles outputen ut til vårt kallende program.

Det er en ting til med popen. Hvis kommandoen du sender den er et enkelt minus-tegn (``--''), vil popen sette igang en ny Ruby-interpreter. Både denne og den opprinnelige interpreteren vil fortsette å kjøre ved å returnere fra popen. Den orgininale prosessen vil motta et IO-objekt tilbake, mens barnet vil motta nil.

pipe = IO.popen("-","w+")
if pipe
  pipe.puts "Get a job!"
  $stderr.puts "Child says '#{pipe.gets.chomp}'"
else
  $stderr.puts "Dad says '#{gets.chomp}'"
  puts "OK"
end
produserer:
Dad says 'Get a job!'
Child says 'OK'

I tillegg til popen, er de tradisjonelle Unix-kallene, Kernel::fork , Kernel::exec , and IO.pipe tilgjengelige på plattformer som støtter dem. Konvensjonene til filnavn vil hos mange IO-metoder og Kernel::open også føre til subprosesser hvis du putter en ``|'' som det første tegnet i filnavnet (se introduksjonen til klassen IO på side 325 for detaljer). Merk deg at du kan ikke lage rør ved å bruke File.new ; den er kun ment for filer.

Uavhengige barn

Noen ganger trenger vi ikke å være fullt så detaljstyrende: vi ønsker å gi subprosessen dens tildeling og så forsette med våre egne gjøremål. På et senere tidspunkt vil vi sjekke om subprosessen er fredig. For eksempel vil vi kanskje sette i gang en sortering som tar lang tid.

exec("sort testfile > output.txt") if fork == nil
# Sorteringen skjer nå i en barneprosess
# Fortsett med prosessering i hovedprogrammet

# Vent deretter på at sorteringen skal bli ferdig Process.wait

Kallet til Kernel::fork returnerer en prosess-id i foreldren, og nil i barnet, så barneprosessen vil utøve Kernel::exec -kallet og kjøre sortering. Noe senere kjører vi et Process::wait -kall, som venter på at sorteringen skal bli ferdig (og returnerer dens prosess-id).

Hvis du heller vil få beskjed når et barn lukkes (istedenfor å bare vente på det), kan du sette opp en signalhåndterer som bruker Kernel::trap (beskrevet på side 427). Her setter vi opp en felle på SIGCLD, som er signalet sendes ved "død-barneprosess".

trap("CLD") {
  pid = Process.wait
  puts "Child pid #{pid}: terminated"
  exit
}

exec("sort testfile > output.txt") if fork == nil

# gjør noe annet...

produserer:
Child pid 19326: terminated

Blokker og Subprosesser

IO.popen arbeider med en blokk på mye av samme måten som File.open gjør. Send en kommando til popen, slik som date, og blokken vil bli sendt et IO-objekt som parameter.

IO.popen ("date") { |f| puts "Date is #{f.gets}" }
produserer:
Date is Sun Nov 25 23:43:36 CST 2001

IO-objektet vil bli lukket automatisk når kodeblokken lukkes, akkurat som det er med File.open .

Hvis du assosierer en blokk med Kernel::fork vil koden i blokken bli kjørt i en Ruby subprosess, og foreldren vil fortsette etter blokken.

fork do
  puts "In child, pid = #$$"
  exit 99
end
pid = Process.wait
puts "Child terminated, pid = #{pid}, exit code = #{$? >> 8}"
produserer:
In child, pid = 19333
Child terminated, pid = 19333, exit code = 99

En siste ting. Hvorfor bitskiftes utgangskoden i $? åtte bits til høyre før den blir vist? Dette er en "egenskap" i Posix-systemer: de nederste 8 bits til en exit-kode inneholder grunnen til at programmet er terminert, mens de høyere 8 bitsene inneholder den egentlige utgangskoden.

( In progress translation to Norwegian by NorwayRUG. $Revision: 1.10 $ )
$Log: tut_threads.xml,v $
Revision 1.10  2003/08/31 11:58:58  kent
Fjerner masse KapitaliseringsMani i overskrifter.

Revision 1.9  2002/07/27 11:58:05  kent
Småfiks.

Revision 1.8  2002/07/27 11:54:18  kent
Endel omskriving frem til "Blockker og Subprosesser".

Revision 1.7  2002/07/26 17:06:12  kent
Gjennomgang og småfiks frem til Mutex-klassen.

Revision 1.6  2002/07/23 14:48:16  kent
Kjapp overgang frem til abort_on_exception, fikser små skriveleif m.m.


Forrige < Innhold ^
Neste >

Extracted from the book "Programming Ruby - The Pragmatic Programmer's Guide".
Translation to norwegian by Norway Ruby User Group.
Copyright for the english original authored by David Thomas and Andrew Hunt:
Copyright © 2001 Addison Wesley Longman, Inc.
This material may be distributed only subject to the terms and conditions set forth in the Open Publication License, v1.0 or later (the latest version is presently available at
http://www.opencontent.org/openpub/).

(Please note that the license for the original has changed from the above. The above is the license of the original version that was used as a foundation for the translation efforts.)

Copyright for the norwegian translation:
Copyright © 2002 Norway Ruby User Group.
This material may be distributed only subject to the terms and conditions set forth in the Open Publication License, v1.0 or later (the latest version is presently available at
http://www.opencontent.org/openpub/).
Distribution of substantively modified versions of this document is prohibited without the explicit permission of the copyright holder.
Distribution of the work or derivative of the work in any standard (paper) book form is prohibited unless prior permission is obtained from the copyright holder.