wait og notify

For å slippe diverse dårlige/kreative løsninger som fort kan innebære mye knoting tilbyr Java wait() og notify() som gjør det enkelt for tråder å dele på de samme objektene.

Her er et enkelt og forståelig eksempel som tar utgangspunkt i det å være en vaskemaskin:

class Vaskemaskin {
 
   private static boolean klar = true;
   private static String klesvask = null;
 
   public synchronized void start(String møkketeKlesvask) throws InterruptedException {
      if (klesvask == null && klar) {
         klar = false; 
         klesvask = møkketeKlesvask;
         System.out.println("Starter vasking av " + klesvask);
         Thread.sleep(3000); // vi later som 3 sekund er 3 timer
         klar = true;
         notify(); // si i fra til ventende tråd
      } else {
         wait(); // vent
      }
   }
 
   public synchronized void tøm() throws InterruptedException {
      if (klesvask == null || !klar) {
         wait(); // vent
      } else {
         if (klesvask != null) {
            System.out.println("Tømmer vaskemaskin for " + klesvask);
            klesvask = null;
         }
         notify(); // si i fra til ventende tråd
      }
   }
}

Klassen Vaskemaskin tilbyr her bare to metoder – en for å starte en ny omgang og en for å tømme etterpå.

Begge er synkroniserte så programmet ville faktisk fungert uten notify() og wait(), problemet er da at tråden for å tømme vaskemaskinen stadig vil gjøre unødvendige kall på tøm().

For å teste denne klassen kan følgende benyttes:

private static Vaskemaskin vaskemaskin = new Vaskemaskin();

public static void main(String[] args) {

   // Sett på maskin
   new Thread(new Runnable() {
      public void run() {
         while (true) {
            try {
               vaskemaskin.start( klesvask() );
            } catch (InterruptedException ex) {}
         }
      }
   }).start();

   // Vent på maskin for å tømme
   new Thread(new Runnable() {
      public void run() {
         while (true) {
            try {
               vaskemaskin.tøm();
            } catch (InterruptedException ex) {}
         }
      }
   }).start();
}

public static String klesvask() {
 
   String[] klær = new String[] {
      "noen sokker", "litt håndklær", "ett dynetrekk",
      "noen putetrekk", "et par laken", "litt gensere", "noen bukser"
   };
 
   return klær[(int) (Math.random() * klær.length)];
}

.. som så gir et eller annet som ligner dette:

Starter vasking av litt håndklær
Tømmer vaskemaskin for litt håndklær
Starter vasking av noen sokker
Tømmer vaskemaskin for noen sokker
Starter vasking av litt håndklær
Tømmer vaskemaskin for litt håndklær
...

varargs

Som med C# og sikkert en rekke andre språk har også Java mulighet for å ha funksjoner/metoder som kan ta et ukjent antall innverdier.

Her er to slike funksjoner/metoder hvor man kan mate inn så mange String man vil:

public static void skrivUtMedMellomrom(String ... tekst) {
   String linje = "";
   for (String t : tekst) {
      linje += t + " ";
   }
   linje = linje.trim();
 
   System.out.println(linje);
}
public static void skrivUtMedMellomrom(int antallMellomrom, String ... tekst) {
   String linje = "";
   for (String t : tekst) {
      linje += t;
      for (int m = 0; m < antallMellomrom; m++) {
         linje += " ";
      }
   }
   linje = linje.trim();
 
   System.out.println(linje);
}

For å teste kan man for eksempel kjøre følgende:

public static void main(String[] args) {
   skrivUtMedMellomrom("Dette", "er", "en", "test!");
   skrivUtMedMellomrom(3, "I", "want", "cake!");
}

.. som gir:

Dette er en test!
I   want   cake!

String er ikke det eneste som er støttet selvsagt, alle typer kan benyttes.

Og signaturen trenger ikke være slik den er over – man har uendelig mange muligheter.

Synkronisering

Med asynkron programmering dukker ofte behovet opp for kontrollere hvilken tråd som bruker bestemte ressurser på et gitt tidspunkt.

Det er ganske vanlig å kun tillate èn tråd, i stedet for flere likt. Har man ingen kontroll risikerer man at oppdaterte/endrede data blir mangelfulle, at ting ikke blir gjort i riktig rekkefølge, o.s.v. Her kommer lock, Monitor, [Synchronization] med flere, inn i bildet.

Lock og Monitor

Vanligvis kommer man langt med lock som i praksis fungerer slik:

lock (this) // hvis dette er inni en privat metode
{
   ...
}

.. eller slik:

private object låsObjekt = new object(); // utenfor metoden
...
lock (låsObjekt) // hvis dette er inni en åpen metode
{
   ...
}

Og siden lock bare er en kortform for Monitor kan man visst i følge boka også kjøre på med følgende:

private object låsObjekt = new object(); // utenfor metoden
...
Monitor.Enter(låsObjekt) // inni åpen eller stengt metode
try
{
   ...
}
finally
{
   Monitor.Exit(låsObjekt)
}

Fordelen er visst at dette muliggjør større kontroll fordi Monitor blant annet har støtte for å kontakte ventende tråder.

[Synchronization]

Vil man ordne låsing for en hel klasse trenger man bare å plassere [Synchronization] fremfor klassedefinisjonen sin. Ulempen er at dette vil gjelde for absolutt alt innhold – til og med de metodene som ikke trenger det.

Asynkron programmering

Etter å ha brukt ca èn uke har jeg nå fått god oversikt over de forskjellige måtene man kan bedrive asynkron programmering på i NET. Og ikke bare har jeg fått repetert gammelt stoff, jeg har også fått lært mye nytt.

Hvorfor jeg ikke har brukt tid på dette før vet jeg ikke helt. Jeg har alltid hatt for vane å opprette egne tråder og ikke tenkt mer på det, det er jo så enkelt og intuitivt:

new Thread(() =>
{
   ...
}).Start();

Delegater

Først, for å i det hele tatt kunne komme i gang med å lære hvilke alternativer som fantes måtte jeg lære hvordan delegater fungerer: En delegat er simpelthen bare en egen type for å holde på referanser til funksjoner/metoder. Disse brukes fordi man ikke alltid kan benytte de aktuelle funksjonene/metodene direkte. En delegat har samme signatur som de funksjonene/metodene den skal kunne referere til.

Om vi først tar utgangspunkt i to enkle mattefunksjoner:

static double Pluss(double tall1, double tall2)
{
   return tall1 + tall2;
}
static double Minus(double tall1, double tall2)
{
   return tall1 - tall2;
}

.. så gir dette utgangspunktet for hvordan definisjonen til delegaten må se ut:

delegate double MatteFunksjon(double tall1, double tall2);

Deretter kan vi opprette en stk. for hver av de opprinnelige funksjonene:

MatteFunksjon PlussDelegat = new MatteFunksjon(Pluss);
MatteFunksjon MinusDelegat = Minus; // en kortere form

Disse delegatene, altså PlussDelegat og MinusDelegat, kan nå brukes hver gang man ønsker å benytte Pluss og/eller Minus i en eller annen asynkron sammenheng.

IAsyncResult

Boka jeg bruker begynte dette temaet med å ta i bruk delegater og (I)AsyncResult.

Et eksempel på hvordan dette fungerer i praksis, med Pluss og PlussDelegat:

IAsyncResult plussAsyncResultat = PlussDelegat.BeginInvoke(2, 2, null, null);
while (!plussAsyncResultat.IsCompleted)
{
   ...
}
double svar = PlussDelegat.EndInvoke(plussAsyncResultat);

Her sendes tallene 2 og 2 til Pluss (som skal kjøre asynkront) via PlussDelegat, og det returneres en instans av AsyncResult via en IAsyncResult-referanse. Denne brukes helt til slutt for å hente ut svaret.

I mellomtiden venter man på at plussAsyncResultat skal bli ferdig.


En alternativ løsning kan være å benytte en egen funksjon/metode som tar seg av det som skal gjøres etter Pluss er ferdig.

I praksis kan man sause alt sammen inn i BeginInvokePlussDelegat:

IAsyncResult plussAsyncResultat = PlussDelegat.BeginInvoke(2, 2, new AsyncCallback(PlussResultat), new double[] {2,2});

.. hvor PlussResultat får ansvaret for å avslutte:

void PlussResultat(IAsyncResult iar)
{
   double[] tall = (double[]) iar.AsyncState;
   double svar = PlussDelegat.EndInvoke(iar);
   ...
}

Dermed blir det aldri noe venting i den opprinnelige tråden.

Thread

Min favoritt som er egne tråder er nok fortsatt det alternativet jeg liker best:

Thread tråd = new Thread(() =>
{
   double svar = Pluss(2, 2);
   ...
});
tråd.Start();

I stedet for anonyme funksjoner kan man selvsagt også bruke en funksjon/metode som allerede eksisterer, hvis den har riktig signatur:

Thread tråd = new Thread(FunksjonMedRiktigSignatur); // gitt funksjon må returnere void uten å ta inn parametere

.. eller:

Thread tråd = new Thread(FunksjonMedRiktigSignatur); // gitt funksjon må returnere void og akseptere èn stk. object inn
...
tråd.Start(new double[] {2, 2});

Det som kan være et problem her er at ikke noe returneres. En mulig løsning er da å lagre resultatet i en variabel opprettet utenfor tråden, men siden dette åpner for å få en «race condition» må man være påpasselig med hvordan man gjør det.

ThreadPool

Mye omtalte ThreadPool som ofte er anbefalt, men som jeg aldri har lært å like:

WaitCallback plussDelegatForTrådSamling = new WaitCallback((data) =>
{
   double svar = Pluss(2, 2);
   ...
});
ThreadPool.QueueUserWorkItem(plussDelegatForTrådSamling);

Også her kan man bruke en eksisterende funksjon/metode hvis noen passer:

WaitCallback minusDelegatForTrådSamling = new WaitCallback(FunksjonMedRiktigSignatur); // gitt funksjon må returnere void og ta inn èn stk. object
ThreadPool.QueueUserWorkItem(minusDelegatForTrådSamling, new double[] {2, 2});

Task

Et alternativ jeg har begynt å like er Task siden det gir lite kode:

Task.Factory.StartNew(() =>
{
   double svar = Pluss(2, 2);
   ...
});

Også her kan vi angi en eksisterende funksjon/metode hvis signaturen passer:

using (Task plussOppgave = Task.Factory.StartNew(FunksjonMedRiktigSignatur)) // gitt funksjon må returnere void uten å ta inn parametere
{
   while (!plussOppgave.IsCompleted)
   {
      ...
   }
}

Task er støttet fra og med versjon 4.0 av NET.

async og await

I motsetning til (I)AsyncResult, Thread, ThreadPool og Task så er async og await litt vanskelig å komme inn i til å begynne med.

Først et eksempel på hvordan funksjonen/metoden kan se ut her for at man skal kunne bruke den asynkront:

async void AsyncPlussResultat(double tall1, double tall2)
{
   await Task.Run(() =>
   {
      double svar = Pluss(tall1, tall2);
      ...
   });
}

.. som kan brukes med:

AsyncPlussResultat(2, 2);

(Man trenger riktignok ikke alltid mate inn data slik som her, man kan ha funksjoner/metoder som ikke tar parametere i det hele tatt.)

Som med Thread og ThreadPool er det også her litt kronglete å få fraktet data helt tilbake dit det opprinnelige kallet begynner. Men ved å bruke async og await med Task er det fullt mulig:

async Task<double> AsyncPluss(double tall1, double tall2)
{
   return await Task.Run(() =>
   {
      double svar = Pluss(tall1, tall2);
      return svar;
   });
}

.. hvor denne kalles ved å f.eks. bruke følgende:

Task<double> oppgave = AsyncPluss(2, 2);
while (!oppgave.IsCompleted)
{
   ...
}
double svar = oppgave.Result;

Altså får man også her returnert et svar, om man er villig til å vente litt.

Parallel

Siste alternativet (akkurat nå) er Parallel, her med bruk av Invoke:

Parallel.Invoke(() =>
{
   double svar = Pluss(2, 2);
   ...
},
() =>
{
   double svar = Minus(2, 2);
   ...
});

Parallel har også For og ForEach for å trygt kunne kjøre gjennom «collections», og er støttet fra og med 4.6 av NET.

TBC