Side 1 av 11, prosesser, tråder, synkronisering, V. Holmstedt, HiO 2006 Prosesser og tråder Datamaskinen lager prosesser. En prosess organiserer arbeidet i et program ved å administrere tråder. Det er derfor alltid minst en tråd i et program. Det kan vi sjekke med følgende lille program: public class A_Test { public static void main(string[] args) { System.out.println("Aktive tråder:"+thread.activecount()); Denne koden og eventuelle utvidelser i form av flere kodelinjer og klasser, vil høre til den første tråden. Men det er også mulig å lage nye tråder med utgangspunkt i den første tråden. Tråder i Java Det er enkelt å lage nye tråder i Java. Enhver klasse som spesialiserer klassen Thread, blir en tråd når den instansieres. Tråden settes klar for kjøring med meldingen start(). Når tråden er satt klar for kjøring, får den etter en stund meldingen run fra den noden den er klargjort i. Denne noden er nesten alltid JVM. public class EnTraad extends Thread { System.out.println( Hello world ); Thread t = new EnTraad(); t.start(); Men det kan kreve litt mer planlegging å bruke trådene riktig. Planlegging kombinert med deklarasjonen synchronized når det er nødvendig, kan føre til programmer som oppfører seg riktig i de fleste kjøremiljøer. Synkronisering Ordet synchronized brukes for å deklarere områder som skal holde monitoren i et objekt. Det å holde monitoren betyr å ha enerett på ressursene i objektet. Ordet synchronized kan brukes på metoder og på områder inne i en metode, men med litt ulik syntaks. Bruksområde Flere tråder vil operere på de samme ressursene. Utfordring Når flere vil forandre en ressurs, må selve forandringsarbeidet beskyttes mot feil. Feil kan oppstå når to tråder prøver å endre en felles ressurs samtidig. Selve det å forandre en ressurs
Side 2 av 11, prosesser, tråder, synkronisering, V. Holmstedt, HiO 2006 må beskyttes, slik at hver tråd alltid opererer med korrekt datagrunnlag. Problemet er knyttet til begrepet race condition. Race condition Oppdatering av en ressurs baseres av og til på eksisterende verdi, som når verdien i en variabel skal økes i stedet for bare å settes. Skal endringen være i forhold til nåværende verdi, må operasjonen foregå i flere trinn. Først må verdien leses, deretter må den økes og til slutt lagres tilbake før noen andre har rukket å lese, øke, eller legge den tilbake. Prosessen med å lese, øke og lagre tilbake må være atomisk, som betyr udelelig. Hvis den ikke er atomisk oppstår en race condition. En mutator kan gjøres atomisk ved å deklarere den med synchronized. Det som skjer er at objektet som kaller en synkronisert metode, anvender sin lock, eller lås, på alle synkroniserte metoder i objektet. Så lenge dette objektets lock er assosiert med tjenerobjektet, kan ingen andre objekter slippe til i objektets synkroniserte metoder. Klassemetoder har en parallell mekanisme basert på en instans av Class. Eksempler Alle eksemplene oppfører seg forskjellig på ulike maskiner, og under ulike stressnivåer på hver maskin. Eksempel 1 I disse eksemplene deklareres variabelen n i trådklassen med static, og kan derfor brukes som en felles ressurs. I eksemplene er n i utgangspunktet ikke beskyttet mot race conditions. public class B_Sync extends Thread { static int n = 1; for (int i = 1; i <= 10; i++) { System.out.println(n); n = n + 1; Thread thr1 = new B_Sync(); Thread thr2 = new B_Sync(); thr1.start(); thr2.start(); Klassen B_Sync inneholder en race condition. Ved kjøring kan det være vanskelig å påvise feil, selv ved å legge inn stressfaktorer.
Side 3 av 11, prosesser, tråder, synkronisering, V. Holmstedt, HiO 2006 Ved å legge til ordet this i utskriften, får vi vite hvilken tråd som skriver ut for øyeblikket. Selve utskriften, println, er beskyttet mot race conditions, men ikke før argumentene er beregnet. System.out.println(n+ " "+this); Dette lar oss følge med på hvilken tråd som gjør hva. Hvis maskinen som tester klassen ikke gjør feil, kan grunnen være at den ene tråden blir så raskt ferdig at den allerede har stoppet når den andre begynner. Dermed går de selvsagt ikke i veien for hverandre. Det kan altså noen ganger være vanskelig å få påvist race condition, dersom det ikke settes inn kode som forsinker trådene litt ujevnt. public class C_Sync extends Thread { static int n = 0; boolean w = true; for (int i = 1; i <= 10; i++) { if (w =!w) { sleep(10); catch (InterruptedException e) { System.out.println(n+" "+this); Thread thr1 = new C_Sync(); Thread thr2 = new C_Sync(); thr1.start(); thr2.start(); Dette forsinker annenhver iterasjon i metoden run. Dersom et objekt rekker å gjøre to iterasjoner, er det altså sikret en forsinkelse. Denne ujevnheten kan være nok til å påvise race condition i et gitt testmiljø. Siden vi ikke har kontroll med hvilket objekt som kaller run i en Thread, vil vi ikke bruke run til å teste synchronized. Vi lager heller en metode for å øke verdien i n. Vi deklarerer derfor en mutator for ressursen. Se metoden incr. public class D_Sync extends Thread { static int n = 0; static void incr(d_sync sync) { System.out.println(n+" "+sync); n++;
Side 4 av 11, prosesser, tråder, synkronisering, V. Holmstedt, HiO 2006 boolean w = true; for (int i = 1; i <= 10; i++) { if (w =!w) { sleep(10); catch (InterruptedException e) { incr(this); Thread thr1 = new D_Sync(); Thread thr2 = new D_Sync(); thr1.start(); thr2.start(); En klasse som D_Sync kan inneholde en race condition. Ved kjøring kan det være vanskelig å påvise feil, selv ved å legge inn stressfaktorer. Vi kan imidlertid legge inn argumentet this i kallet til incr, for å finne ut, for å finne ut hvilken tråd som er aktiv for øyeblikket. Selv om trådene opererer i ujevn rekkefølge, er det ikke gitt at vi kan påvise race condition, selv om vi vet at risikoen er der. En grunn kan være at vi rett og slett ikke tester mange nok ganger. Eksempel 2 For å legge bedre til rette for å påvise race condition, kan vi bygge et litt mer strukturert og sammensatt programsystem. Som ellers i objektorientert programmering, skiller vi klassene slik at vi oppnår høyere kohesjon, som betyr større konsentrasjon og sammenheng i oppgavene. Først skilles entry point fra testklassene, slik at det blir lettere å kontrollere inngangsverdier, parametre og etterbehandling. public class A_Entrypoint { static final int antall = 25; for (int j = 0; j < antall; j++) { new A_Synchronize().start(); Med et slikt startpunkt kan vi enkelt regulere antall ganger testen skal kjøres, uten at den egentlige testkoden blir mer komplisert. Ved å øke antallet testkjøringer øker sannsynligheten for å kunne påvise race condition. Selve testkoden vises i klassen A_Synchronize.
Side 5 av 11, prosesser, tråder, synkronisering, V. Holmstedt, HiO 2006 public class A_Synchronize extends Thread { static int n = 0; static void incr() { System.out.println(n); for (int i = 1; i <= 10000; i++) { float f = 2; f = f * f; f = f / f; for (int i = 1; i <= 10000; i++) { float f = 2; f = f * f; f = f / f; n++; for (int i = 1; i <= 10; i++) { incr(); Denne versjonen kan stresse prosessen en del, og problemet er denne gangen at det er vanskelig å sjekke dataene med println, fordi det krever for mye menneskelige ressurser å kontrollere så mange datarader. Strategien videre er å bygge et array med Color som enkelt kan etterbehandles i et grafisk vindu. Dette legges det til rette for ved å fortsette å skille ut kode, denne gangen ved å skille logikk og databehandling fra presentasjon. Setningene med println påvirker i seg selv resultatet av testene, og bør helst erstattes med andre testmetoder. Variabelen n skal ikke lenger være statisk, men en objektressurs. Eksemplet er altså blitt mer objektorientert. Den nye strategien er dermed gunstig i forbindelse med flere prinsipper i objektorientert programmering. public class B_Entrypoint { static final int COUNT_OF_THREADS = 150; static final int ITER_THREAD = 10; new B_ClientTester(); Her får B_Entrypoint oppgaven å sentralisere to testparametre. Dette gjør det enkelt og oversiktlig å variere antall tråder og antall iterasjoner i hver tråd, for å kalibrere testen til det maskinmiljøet testen skal kjøres i. Selve test-programmet og den felles ressursen n er i B_ClientTester.
Side 6 av 11, prosesser, tråder, synkronisering, V. Holmstedt, HiO 2006 public class B_ClientTester { int n = 0; Color[] p = new Color[B_Entrypoint.ITER_THREAD * B_Entrypoint.COUNT_OF_THREADS]; boolean w = false; public B_ClientTester() { for (int j = 0; j < B_Entrypoint.COUNT_OF_THREADS; j++) { new B_Client(this).start(); new Proof(p); void incr(object o) { p[n] = Color.GREEN; if (w =!w) if (n % 3 == 0) Thread.sleep(5); catch (InterruptedException e) { System.out.println(n+" "+o); n++; Hver gang incr besøkes, oppdateres arrayet med fargen GREEN i det punktet variabelen n peker på for øyeblikket. Dersom n har samme verdi som ved et tidligere besøk, skyldes dette en race condition. Dette vil resultere i at enkelte elementer i arrayet ikke oppdateres med fargen GREEN. Den som foretar besøk og operasjoner på den felles ressursen, er objekter fra klassen B_Client. public class B_Client extends Thread { B_ClientTester caller; public B_Client(B_ClientTester synctester) { this.caller = synctester; for (int i = 1; i <= B_Entrypoint.ITER_THREAD; i++) { caller.incr(this); Selv om koden er blitt mer sammensatt, er det er fortsatt minst like enkelt å regulere antall tester. Nå spares testresultatet i variablen p, som kan presenteres grafisk etter at dataene er samlet opp. Presentasjonen gjøres i klassen Proof. public class Proof extends JFrame { public Proof(Color[] p) { settitle("race condition examination");
Side 7 av 11, prosesser, tråder, synkronisering, V. Holmstedt, HiO 2006 setsize(500, 400); getcontentpane().setbackground(color.gray); setlocationrelativeto(null); int l = (int) Math.sqrt(p.length); setlayout(new GridLayout(l, 0, 1, 1)); for (Color c : p) { JPanel r = new JPanel(); r.setbackground(c); add(r); setvisible(true); Nå er det atskillig enklere å påvise race conditions. Det er altså vanskelig å påvise race conditions med for få testkjøringer. Nesten 300 kjøringer måtte til før den første feilen kom i akkurat dette miljøet. I andre miljøer og med andre stressfaktorer, som ikke trenger å komme fra testprogrammet, kan det kreves mange flere iterasjoner før det kan påvises feil. Men ett problem gjenstår. Legg merke til at den siste delen av denne kjøringen viser utelukkende feil. Siden dette er et stort sammenhengende område, tyder det på at det også har oppstått en annen feil enn race conditions. Feilen er at ikke alle trådene rakk å bli ferdige før testresultatet ble vist. Testresultatet må altså ikke vises før alle trådene er ferdige med sine operasjoner. Denne feilen oppdages ikke i alle maskinmiljøer. Dersom en gitt maskin har ressurser som gjør at alle trådene rekker å bli ferdige før resultatet vises, kan ikke utvikleren oppdage feilen. Dersom testen i det viste miljøet hadde stoppet på rad 23, ville ikke feilen vært oppdaget her heller. Testklassen må derfor modifiseres, slik at den venter på at alle trådene blir ferdige før resultatet kan vises riktig.
Side 8 av 11, prosesser, tråder, synkronisering, V. Holmstedt, HiO 2006 public class B_ClientTester { int n = 0; Color[] p = new Color[B_Entrypoint.ITER_THREAD * B_Entrypoint.COUNT_OF_THREADS]; boolean w = false; public B_ClientTester() { for (int j = 0; j < B_Entrypoint.COUNT_OF_THREADS; j++) { new B_Client(this).start(); while (Thread.activeCount()!= 1) { Thread.sleep(200); catch (InterruptedException e) { new Proof(p); void incr(object o) { p[n] = Color.GREEN; if (w =!w) if (n % 3 == 0) Thread.sleep(5); catch (InterruptedException e) { System.out.println(n+" "+o); n++; Ved å sjekke Thread.activeCount kan vi regne alle trådene som ferdige når det bare er en tråd igjen. Ved å bruke sleep, parkeres testtråden i 200 ms uten å bruke ressurser. Det neste resultatet ser derfor mer rimelig ut.
Side 9 av 11, prosesser, tråder, synkronisering, V. Holmstedt, HiO 2006 I denne versjonen av B_ClientTester er det mulig å variere mod-faktoren, sleep-verdien så vel som antall tråder, for å se variasjoner i forekomsten av race conditions i akkurat det miljøet der testingen foregår. Ved å deklarere incr med synchronized, skal neste testkjøring vise utelukkende grønne felt, fordi alle mulige varianter av n har pekt på testarrayet. Vær oppmerksom på at også arrayet p er utsatt for race conditions. Eksempel 3 Det kan av og til være utfordrende å observere race conditions på en kontrollert måte. Av og til er testen i seg selv utsatt for race conditions. En strategi for å kontrollere dette, er å bruke innkapsling for å konstruere objektverdier parallelt med felles ressurser. Hver gang et objekt (altså en tråd) vil oppdatere en felles ressurs, må det også oppdatere sin egen innkapslede ressurs. Den innkapslede ressursen er nemlig ikke utsatt for race conditions. Når alle trådene er ferdige, kan moderobjektet sammenligne sin felles ressurs med summen av alle trådenes ressurser. Dersom det ikke har oppstått feil pga race conditions underveis, og alle trådene er helt ferdige med sine arbeidsoppgaver, skal disse verdiene være like. Vi lager en isolert inngang til programmet: public class Entrypoint { public static final int COUNT_OF_THREADS = 2000; public static final int ITER_THREAD = 500; static final int COUNT_OF_TESTS = 10; public static void main(string[] args) { for (int i = 0; i < COUNT_OF_TESTS; i++) new ClientTester(); Dette testoppsettet er litt anderledes enn de tidligere eksemplene. I tillegg til den felles ressursen n, er det også laget en testvariabel, testsum. I dette eksemplet lar vi i tillegg hver tråd sende en egen melding, countdown, til modertråden når de er ferdige. Modertråden teller hvor mange som er ferdige, og går videre når alle trådene er ferdige med sitt arbeid. Denne tellingen er også utsatt for race conditions, og må altså beskyttes med synchronized. public class ClientTester { int n; int testsum; int activethreads; synchronized void countdown() { activethreads--;
Side 10 av 11, prosesser, tråder, synkronisering, V. Holmstedt, HiO 2006 Client[] a = new Client[Entrypoint.COUNT_OF_THREADS]; public void incr() { n++; public ClientTester() { n = 0; testsum = 0; activethreads = Entrypoint.COUNT_OF_THREADS; System.out.println("Starter synkrontest"); for (int i = 0; i < Entrypoint.COUNT_OF_THREADS; i++) { a[i] = new Client(this); if (r.nextboolean()) { a[i].setpriority(thread.min_priority); a[i].start(); while (activethreads > 0){ Thread.sleep(500); catch (InterruptedException e) { for (Client t : a) { testsum += t.get(); System.out.println("Ferdig!\tFelles ressurs: " + nf.format(n) + "\tkontroll: " + nf.format(testsum)+"\n"); NumberFormat nf = NumberFormat.getInstance(); I enkelte testmiljøer viser det seg at denne testen ikke viser race conditions. Problemet er likevel til stede, ikke minst fordi feilen viser seg på andre maskiner. Vi kan bruke en variabel i incr for å oppdage at metoden har besøk av flere objekter på samme tid. Det er nettopp dette som fører til race condition, og som kan unngås med synchronized. For å kunne påvise race conditions på maskiner som ikke viser feilen med denne testkoden, kan du legge inn for eksempel denne incr metoden i stedet: boolean w = false; public void incr() { int i = n; if (w =!w) if (n % 1000 == 0) Thread.sleep(5); catch (InterruptedException e) { n = i + 1;
Side 11 av 11, prosesser, tråder, synkronisering, V. Holmstedt, HiO 2006 Her deler vi opp lesing, beregning og skriving av n for å provosere fram feil ved race conditions. I praktiske algoritmer er det helt vanlig at objekter som skal muteres, gjør tilsvarende arbeid i flere trinn. En maskin som aldri viser race conditions med den enkle versjonen av algoritmen, kan få store feil med den mer sammensatte algoritmen. Her er et eksempel på resultater etter usynkronisert bruk av den sammensatte algoritmen: Starter synkrontest Venter på at alle skal bli ferdige Ferdig! Felles ressurs: 460 Kontroll: 1 500 Starter synkrontest Venter på at alle skal bli ferdige Ferdig! Felles ressurs: 530 Kontroll: 1 500 Starter synkrontest Venter på at alle skal bli ferdige Ferdig! Felles ressurs: 510 Kontroll: 1 500 Synkronisert bruk av den samme algoritmen foregår uten forskjeller i felles og kontrollert ressurs. Uten den forstyrrende koden i incr oppstod det heller ikke forskjeller ved ikke-synkronisert bruk på en bestemt datamaskin. Men dette endrer seg fra maskin til maskin, og med ulike belastninger på hver maskin.