Follow Us on Twitter

Gebruik de multi-core omgeving met Java 7

Oktober 2009 - Een van de belangrijkste revoluties in ICT-land is de opkomst van multi-core processors in computers. Dit biedt mogelijkheden voor softwareontwikkelaars. In plaats van programmacode die achter elkaar wordt uitgevoerd, kan code geschreven worden die tegelijkertijd uitgevoerd wordt door meerdere processoren of cores, dit noemen we parallelle software of parallism.

In dit Whitebook zullen de mogelijkheden uitgelegd worden die Java EE voor multi-core omgevingen biedt. Daarna zullen we een nieuwe feature van Java 7 uitlichten, die het zeer eenvoudig maakt om software te schrijven die gebruik maakt van de multi-core mogelijkheden: het fork-join framework.

Course-grained parallism: Out-of-the-box parallism

JEE applicatieservers zoals JBoss, Oracle Weblogic of Websphere maken het schrijven van parallelle software voor een groot deel gemakkelijk. De meeste Java/JEE applicaties hebben veel gelijktijdige gebruikers. De applicatieserver zorgt ervoor dat alle acties die gebruikers uitvoeren netjes worden verdeeld over alle processors. De applicatieserver zorgt vrijwel er automatisch voor dat database- en netwerkconnecties niet gelijktijdig gebruikt worden, beans op het juiste moment geinstantieerd worden, threads worden aangemaakt en user sessions worden bijgehouden.

Op deze manier gebruik maken van multi-processor omgevingen is course-grained parallism: gebruikerstaken worden verdeeld over processors, maar iedere gebruikertaak wordt door maar één processor uitgevoerd. In veel gevallen is course-grained parallism voldoende. Enige voorwaarde is dat een programmeur niet de functionaliteit van de JEE-server omzeilt door bijvoorbeeld toch zelf threads aan te maken, databaseconnecties te openen of singleton beans aan te maken.

Fine-grained parellism: Opsplitsen van een taak

In enkele kan gevallen course-grained parallism niet voldoende zijn en is fine-grained parellism nodig. Deze situatie zal met name voorkomen bij gegevens intensieve webapplicaties, zoals bedrijfsportals of administratieve applicaties. Een losstaande taak moet tegelijkertijd op meerdere cores uitgevoerd worden. Voorbeelden hiervan zijn:

  • Een gebruikersscherm van een webapplicatie die door een complexe berekening pas na lange tijd weergeven zal worden, waardoor het gevaar bestaat dat een gebruiker besluit de webapplicatie niet meer te gebruiken.
  • Batch-taken die automatisch gestart worden en een zeer lange looptijd hebben. Bijvoorbeeld een taak die gegevens uit een database verwerkt en ´s nachts uitgevoerd kan worden tussen 2:00 en 6:00 maar er gemiddeld 8 uur over doet om af te ronden.

Een Java-programmeur moest in deze gevallen niet zo lang geleden zelf aan de slag gaan met threads, locks aanmaken via synchronized methodes en bovenal zelf uitzoeken hoe hij het beste een taak kon verdelen over meerdere cores. Gelukkig bestaan sinds enige tijd libraries die het maken van multi-core software een stuk eenvoudiger maken.
In dit artikel zullen we jsr-166 uitlichten: het fork-join framework. Dit framework zal standaard deel uitmaken van de nog uit te komen Java 71 maar kan nu al gedownload en gebruikt worden: het framework zelf is al geruime tijd final.

We zullen eerst de basis van fork-join uitleggen aan de hand van een voorbeeld. Vervolgens zal een onderdeel van het fork-join framework, ParallelArray, nader toegelicht worden.

De basis van de fork-join library is divide-and-conquer, een term die voor veel programmeurs bekend zal zijn. Een bepaalde taak moet opgesplitst worden in subtaken die vervolgens afzonderlijk uitgevoerd kunnen worden. Het resultaat van al die taken kan gecombineerd worden.

Het fork-join framework zal niet automatisch een taak opsplitsen in subtaken. Daarvoor is inventiviteit van de programmeur nodig. Wat doet het framework dan wel? Op eerste gezicht zou je voor iedere subtaak een thread kunnen aanmaken. Toch is de oplossing niet zo eenvoudig als het lijkt:

  • Het aantal threads moet in verhouding staan tot het aantal cores, aanmaken van een thread is namelijk relatief zwaar.
  • Om alle cores evenveel werk te laten doen, moet het werk efficiënt verdeeld worden over threads. Als een core klaar is met uitvoeren van een subtaak, of als een subtaak tijdelijk stil ligt, moet de core onmiddellijk een andere subtaak krijgen om uit te voeren.
  • Tot slot moet de het verdelen van taken ook weer niet te veel tijd kosten: soms kunnen alle of een gedeelte van de taken beter achter elkaar uitgevoerd worden.

Kortom, multi core programming is voor een programmeur ingewikkelder dan op eerste gezicht lijkt. Het fork-join framework zal echter veel van deze complexiteit voor de programmeur uit hand nemen. Voor een uitgebreide uitleg kunnen referenties geraadpleegd worden. We zullen het fork-join framework demonstreren door een multi-core variant van merge-sort te maken, een algoritme om lijsten te sorteren.

1) Strikt genomen moet nog gesproken worden over JDK 1.7 in plaats van Java 7, zie http://www.jroller.com/scolebourne/entry/no_more_java_7

Mergesort met fork-join

Om te beginnen moet een implementatie van jsr 166 gedownload worden. Java 7 is nog niet uit, maar de implementatie is al beschikbaar:http://g.oswego.edu/dl/concurrency-interest/. Alleen de jsr166y.jar is nodig. Deze moet worden toegevoegd aan het classpath.

We beginnen met een class MergeSort, met een constructor die een lijst van Comparable objecten heeft als parameter en indices die het gedeelte van de lijst aangeven dat we willen sorteren.

public class MergeSort extends RecursiveAction {
   private final int from, to;
   private Comparable[] l;
   protected MergeSort(Comparable[] l, int from, int to) {
      this.l = l;
      this.from = from;
      this.to = to;
   }
}

We laten de class RecursiveAction extenden, een class die deel uitmaakt van jsr166y. Vervolgens moeten we de method compute van deze class overriden. Deze methode moet de subtaak bevatten die over verschillende cores verdeeld kan worden.

@Override
protected void compute() {
   if (to > from) {
      int split = to + (to - from) / 2;

      MergeSort m1 = new MergeSort(l, from, split);
      MergeSort m2 = new MergeSort(l, split + 1, to);
      m1.fork();

      m2.fork();

      mergeSortedList(from, split, split + 1, to);
   }
}

Binnen de compute method wordt de lijst l opgesplitst in tweeën die vervolgens, recursief, door de class zelf weer gesorteerd wordt. De twee fork methods zorgen ervoor dat de twee subtaken m1 en m2 uitgevoerd worden. De aanroep zou je kunnen vergelijken met de start() method die de java Thread class bevat, met het verschil dat de start() method altijd een nieuwe Thread aanmaakt, en de fork-join slimt kijkt of dit daadwerkelijk nodig is.

Het resultaat wordt vervolgens gecombineerd door onze eigen mergeSortedList:

private void mergeSortedList(int f1, int t1, int f2, int t2) {
   for (int i = f1; i <= t1; i++) {
      for (int j = f2; j <= t2; j++) {
         if (l[i].compareTo(l[j])  0) {
            Comparable t = l[j];
            l[j] = l[i];
            l[i] = t;
         }
      }
   }
}

Aanroep van de mergeSort is vervolgens als volgt:

static void sortMe(Comparable[] list) {
   MergeSort f = new MergeSort(list, 0, list.length - 1);

   int nThreads = 2;
   ForkJoinPool fjPool = new ForkJoinPool(nThreads);

   fjPool.invoke(f);
}

De ForkJoinPool is hier de 'engine' die de taken uitvoert en heeft als parameter het aantal gelijktijdige threads dat gestart moet worden. Een goede waarde is het aantal cores/CPU's die beschikbaar zijn. Voor mijn dual-core laptop heb ik de 2 als aantal threads gekozen.

ParallelArray

Bovenstaande voorbeeld demonstreert hoe een algemene taak, die al opgesplitst is, makkelijk over meerdere cores verdeeld kan worden. Opsplitsen moet de ontwikkelaar zelf nog doen. Gelukkig is veel standaardfunctionaliteit al beschikbaar via de ParallelArray. Zo is het bovenstaande sorteervoorbeeld al standaard beschikbaar in de ParallelArray via de sort method.

Op de JSR 1.66 site kunnen is deze implementatie beschikbaar, in het package extra166y. JDK 1.7 zal de ParallelArray net als het fork-join framework standaard bevatten in het nieuwe java.concurrent package.

Naast sorteren kan de ParallelArray onder andere een maximum of minimum zoeken in een array, alle elementen in een array transformeren of een filter toepassen.
Als voorbeeld nemen we het volgende probleem: We hebben een verkoopresultaten van accountmanagers per jaar. We willen weten wie in het afgelopen jaar de best verkopende accountmanager was.

Verkoopresultaat[] resultaatArray = generateDemoArray();
int nThreads = 2;
ForkJoinPool fjPool = new ForkJoinPool(nThreads);

Ops.Predicate<Verkoopresultaat>vanDitJaar = new Ops.Predicate<Verkoopresultaat>() {
   public boolean op(Verkoopresultaat resultaat) {
      return resultaat.jaar == 2008;
   }
};
Ops.ObjectToDouble<Verkoopresultaat> selectOmzet = new Ops.ObjectToDouble<Verkoopresultaat>() {
   public double op(Verkoopresultaat resultaat) {
      return resultaat.omzet;
   }
};

ParallelArray<Verkoopresultaat> verkoopresultaten = ParallelArray.createUsingHandoff(resultaatArray, fjPool);

double hoogsteOmzet = verkoopresultaten.withFilter(vanDitJaar).withMapping(selectOmzet).max();

In het bovenstaande voorbeeld wordt eerst een filter toegepast op de lijst, om alle accountManagers van dit jaar te selecteren. Vervolgens wordt het verkoopresultaat met de hoogste omzet gekozen. ParallelArray zal automatisch het werk distribueren over meerdere threads, en daarmee cores, op zo'n efficiënt mogelijke wijze. Het lijkt in het bovenstaande voorbeeld wellicht of eerst alle verkoopresultaten van 2008 geselecteerd worden, en vervolgens de hoogste omzet wordt gekozen, maar dat is schijn: pas op het moment dat de max() method wordt aangeroepen wordt de berekening uitgevoerd.

Veel DBA'ers zullen wellicht opmerken dat bovenstaande voorbeeld nog makkelijker met SQL uitgevoerd kan worden. Moderne DBMS's als Oracle zullen een query automatisch optimaliseren voor een multi-core omgeving. In nogal wat gevallen is de data in de database echter niet voorhanden, of is het veel praktischer de functionaliteit in Java te maken. Bovendien is de performance en looptijd van ParallelArray zeer voorspelbaar, in dezelfde omstandigheden zal het bovenstaande voorbeeld even snel lopen. Bij een database hangt dit vaak af van optimalisaties die handmatig of automatisch doorgevoerd worden.

Conclusie

Programmeren voor multi-core omgevingen zal steeds meer nodig zijn. Zelf threads aanmaken zal snel tot inefficiënte of zelfs incorrecte code leiden. Multi-core programming is dus complex, maar Java biedt goede faciliteiten om deze complexiteit voor de programmeur uit handen te nemen.

In veel gevallen zijn de faciliteiten van JEE-applicatieservers al voldoende, maar in sommige gevallen zal een programmeur toch zelf parallelle software moeten schrijven om efficiënt van meerdere cores gebruik te maken.

Java 7 maakt met JSR-166 het programmeren van parallelle software een stuk eenvoudiger. Veel complexiteit wordt de programmeur uit handen genomen, zodat hij zich op de functionaliteit kan richten.

Referenties

  1. Goetz, Brian. Java theory and practice: Stick a fork in it, Part 1. http://www.ibm.com/developerworks/java/library/j-jtp11137.html
  2. Concurrency Features in JDK™ 7. http://developers.sun.com/learning/javaoneonline/2008/pdf/TS-5515.pdf
  3. Lea, Doug. A Java Fork/Join Framework. http://gee.cs.oswego.edu/dl/papers/fj.pdf
  4. Wikipedia. http://en.wikipedia.org/wiki/Merge_sort
Waardering:
 

Reacties

Nieuwe reactie inzenden

De inhoud van dit veld is privé en zal niet openbaar worden gemaakt.

Meer informatie over formaatmogelijkheden

CAPTCHA
Deze vraag is om te testen of u een persoon bent en om spam te voorkomen
Image CAPTCHA
Enter the characters shown in the image.