J INF1010 Tråder J Marit Nybakken marnybak@ifi.uio.no Motivasjon Til nå har vi kun skrevet programmer der programmet bare var på ett sted i koden til enhver tid (bortsett fra når vi har drevet med GUI, men der skjønte vi liksom ikke helt hva som skjedde). Men nå skal vi slippe ulven løs og begynne å lage programmer der programmet kan kjøre på flere steder i koden samtidig. Dette kan brukes til å utnytte ressursene i maskinen bedre Hvis maskinen har flere prosessorer kan en tråd kjøre på hver prosessor Hvis en tråd venter på lesing fra f.eks. tastatur eller disk, kan den andre tråden bruke CPUen. simulere situasjoner der flere oppgaver faktisk utføres samtidig Vi vil da gjerne skrive forskjellige klasser for de forskjellige oppgavene og kjøre en tråd i hver klasse. Dette fører til krangling og krig mellom trådene. Din oppgave som programmerer er nå å være megler mellom trådene og se til at alt går riktig for seg. Illustration 1Tråder er ikke lett å holde styr på Å lage en tråd Vi kan legge til en ny tråd i programmet vårt ved å lage en ny klasse som arver fra klassen Thread class MinTråd extends Thread {
// Her skal vi legge all koden som tråden // skal utføre for(int i=0;i<15;i++) System.out.println(""); Klassen skal ha en metode public void run(). I denne legger vi koden som tråden skal kjøre (den kan selvfølgelig kalle andre metoder også). For å starte tråden lager vi så et objekt av denne klassen. Deretter kaller vi metoden start() i objektet. Denne kaller så på run()- metoden igjen. Vi kaller aldri run-metoden direkte: class LageEnTråd { public static void main(string [] args) { // Lager en tråd MinTråd t = new MinTråd(); // Starter opp tråden t.start(); for(int i=0;i<15;i++) { System.out.println(""); Nå vil utskriften fra main og fra tråden komme hulter til bulter på skjermen. Først får main skrive ut litt, deretter får den nye tråden skrive ut litt, så main igjen. Vi sier at de kjører samtidig, men egentlig byttes det raskt mellom de to trådene:
Å pause en tråd Vi kan få en tråd til å ta en pause ved å kalle på sleep(sovetid_i_ms). Sleep kan kaste et unntak, InterruptedException, og dette må vi fange opp med try-catch (mer om unntak i kap 19). Det gjøres slik: class MinTråd extends Thread { // Pause tråden i 5 sekunder int tid_å_sove = 5000; // 5000 ms = 5 sek try { sleep(tid_å_sove); catch(interruptedexception e) { // Her legges koden som skal kjøres etter tråden // starter igjen En tråd vil også pause når den venter på input fra tastatur, akkurat som programmer vanligvis gjør (står og henger til vi skriver inn noe). Dette betyr dog ikke at alle de andre trådene også står og venter selv om en tråd har skrevet tast.inint(), og dette kan føre til forvirring for brukeren. Å stoppe en tråd Det er usannsynlig enkelt å stoppe en tråd. En tråd stopper rett og slett når den er ferdig med runmetoden sin. Kommunikasjon mellom trådene Når trådene kun jobber på sine egne data er det vanligvis ikke noe problem å bruke dem. Det er bare å kjøre dem i gang og la dem drive med sitt. Det virkelige problemet med trådene starter først når de skal
begynne å jobbe med data som de alle deler på. Her er en klasse med data som kan deles for to tråder (bare som et teit eksempel). Det kunne like gjerne vært data i et objekt av en klasse. class Delt { // Delte data static int [] liste = new int [100]; static int ant = 0; En trådklasse som bruker de delte dataene. Den legger en verdi inn i arrayen til vi har kommet til slutten av arrayen (indeks > 99). class Tråddings extends Thread { int verdi = 0; while(delt.ant < 100) { // Er du helt sikker på at Delt.ant fremdeles // er mindre enn 100? Delt.liste[Delt.ant] = verdi; // endre delte data Delt.ant++; verdi++; Så oppretter vi to tråder og setter dem i gang class DummeTråder { public static void main(string [] args) { // Lager to tråder Tråddings t1 = new Tråddings(); t1.start(); Tråddings t2 = new Tråddings(); t2.start(); De første gangene vi kjører det, går alt fint. Så... D:\komp\tråder>java DummeTråder D:\komp\tråder>java DummeTråder D:\komp\tråder>java DummeTråder D:\komp\tråder>java DummeTråder
java.lang.arrayindexoutofboundsexception: 100 at Tråddings.run(LageTråd.java:24) Hva skjedde her? Delt.ant er 99. Tråd 1: while(99 < 100) { Tråd 1 er inne i løkken. Så får tråd 2 kjøre: Tråd 2 : while(99 < 100) { Delt.liste[99] = verdi; Delt.ant = 100; Så får tråd 1 kjøre igjen. Tråd 1: Delt.liste[Delt.ant] = verdi; Men Delt.ant er nå 100! Derfor kræsjer programmet, vi har gått utenfor arrayen. synchronized, pass på dataene dine En slik situasjon kalles for en race condition. Resultatet avhenger av hvilken tråd som kommer først. Kode der tråder opererer på delte data kalles kritisk kode. Slik kode må beskyttes slik at kun en tråd kan kjøre den av gangen. Det siste kalles gjensidig utestengelse, når en tråd er inne i koden stenges alle andre tråder ute. Illustration 2race condition - førstemann til mølla I java har alle objekter noe som kalles for monitorer og som kan passe på at bare en tråd får lov til å kjøre kode i objektet til envher tid. Det funker slik at man setter synchronized foran metoder i objektet som inneholder kritisk kode. class Delt { static int [] liste = new int [100]; static int ant = 0;
synchronized static void delt(int verdi, int pos) { liste[pos] = verdi; Man kan også lage synchronized-blokker, slik: synchronized { <farlig kode> Illustration 3Bare en slipper innenfor synchronized av gangen, og tar samtidig monitoren Monitorer er dørvakter til de synkroniserte metodene. I det en tråd går inn i en synkronisert metode, støter den på monitoren. Er det ledig i metoden, slipper tråden inn. Hvis det er noen inni metoden fra før av, blir tråden pauset og satt på monitorens venteliste. Når tråden som er i metoden er ferdig vil monitoren la den første på ventelista få slippe inn. Vent på turen din
Tråder som er høflige nok kan også samarbeide om å synkronisere seg ved hjelp av monitoren. En tråd som finner ut at den ikke har noe å gjøre for tiden kan sette seg selv på monitorens venteliste ved å kalle på wait() i en synkronisert metode. Da slipper den samtidig monitoren. Dermed slipper andre tråder til i de synkroniserte metodene. Når de er ferdig med sitt arbeid, kan de vekke tråden opp igjen når det er tid for at den skal jobbe ved å kalle notify(). Dette vekker opp første tråd på lista, som så kan inspisere og se om det er noe å gjøre. Den får dog ikke lov til å gjøre noe som helst før monitoren er sluppet av den andre tråden. Det finnes også et notifyall()-kall, som vekker alle tråder på ventelista. Wait og notify bruker vi gjerne i produsent-konsument-programmer. Altså der vi har en tråd som produserer data og en som bruker (konsumerer) disse dataene. Konsumenten kan ikke konsumere før produsenten har produsert, og produsenten kan ikke produsere nye data før konsumenten har brukt de gamle. Her er et eksempel på dette: class Delt { // Delte data double [] liste = new double [10]; // Skal være true når listen er utskrevet og klar for å fylles // og false når listen er fyllt og klar for å bli utskrevet boolean listeutskrevet = true; // Skriv ut innholdet av listen (konsumentens metode) synchronized void skrivut() { // Vent til det er noe å skrive ut while(listeutskrevet) { try { wait(); catch(interruptedexception e) { // Skriv ut for(int i=0;i<liste.length;i++) System.out.println(liste[i]); // Si i fra at listen er klar til å fylles listeutskrevet = true; notify(); // Fyll listen med data (Produsentens metode) synchronized void fyllliste(int val) { // Vent til listen er ferdigskrevet while(!listeutskrevet) { try {
wait(); catch(interruptedexception e) { // Fyll listen med noe junk for(int i=0;i<liste.length;i++) { liste[i] = Math.cos((double)i/val); // Si fra at listen er klar til utskrift listeutskrevet = false; notify(); class Produsent extends Thread { Delt d; Produsent(Delt d) { this.d = d; for(int i=0;i<10;i++) d.fyllliste((i+1)); class Konsument extends Thread { Delt d; Konsument(Delt d) { this.d = d; for(int i=0;i<10;i++) d.skrivut(); Den ene tråden fyller listen, den andre skriver ut. Men før de gjør dette, sjekker de, ved hjelp av listeutskrevet-variabelen, hvorvidt dette skal gjøres nå eller ikke. Hvis listen ikke er fylt, setter konsumenten seg til å vente. Hvis listen er fylt, setter produsenten seg til å vente. De vekkes opp ved hjelp av notify() når tilstanden på listen er endret. Klassen som oppretter trådene: class Samarbeid {
public static void main(string [] args) { Delt d = new Delt(); Produsent p = new Produsent(d); Konsument k = new Konsument(d); p.start(); k.start(); Illustration 4Livet til en tråd