sql >> Database >  >> RDS >> Database

Basisprincipes van parallel programmeren met het Fork/Join Framework in Java

Met de komst van multicore-CPU's in de afgelopen jaren, is parallel programmeren de manier om ten volle te profiteren van de nieuwe verwerkingswerkpaarden. Parallel programmeren verwijst naar de gelijktijdige uitvoering van processen vanwege de beschikbaarheid van meerdere verwerkingskernen. Dit leidt in wezen tot een enorme boost in prestaties en efficiëntie van de programma's in tegenstelling tot lineaire single-core uitvoering of zelfs multithreading. Het Fork/Join-framework is een onderdeel van de Java-concurrency-API. Dit raamwerk stelt programmeurs in staat om algoritmen te parallelliseren. Dit artikel onderzoekt het concept van parallel programmeren met behulp van het Fork/Join Framework dat beschikbaar is in Java.

Een overzicht

Parallel programmeren heeft een veel bredere connotatie en is ongetwijfeld een enorm gebied om in een paar regels uit te werken. De kern van de zaak is vrij eenvoudig, maar operationeel veel moeilijker te bereiken. Simpel gezegd betekent parallel programmeren het schrijven van programma's die meer dan één processor gebruiken om een ​​taak uit te voeren, dat is alles! Raad eens; het klinkt bekend, niet? Het rijmt bijna met het idee van multithreading. Maar merk op dat er enkele belangrijke verschillen tussen hen zijn. Op het eerste gezicht zijn ze hetzelfde, maar de onderstroom is absoluut anders. In feite werd multithreading geïntroduceerd om een ​​soort illusie van parallelle verwerking te creëren zonder echte parallelle uitvoering. Wat multithreading echt doet, is dat het de inactieve tijd van de CPU steelt en het in zijn voordeel gebruikt.

Kortom, multithreading is een verzameling discrete logische eenheden van taken die worden uitgevoerd om hun deel van de CPU-tijd te pakken, terwijl een andere thread tijdelijk wacht op bijvoorbeeld gebruikersinvoer. De inactieve CPU-tijd wordt optimaal verdeeld tussen concurrerende threads. Als er maar één CPU is, is deze tijd gedeeld. Als er meerdere CPU-cores zijn, worden deze ook altijd gedeeld. Daarom perst een optimaal multithreaded programma de prestaties van de CPU uit door het slimme mechanisme van time-sharing. In wezen is het altijd één thread die één CPU gebruikt terwijl een andere thread wacht. Dit gebeurt op een subtiele manier zodat de gebruiker het gevoel krijgt van parallelle verwerking, terwijl de verwerking in werkelijkheid snel achter elkaar plaatsvindt. Het grootste voordeel van multithreading is dat het een techniek is om het maximale uit de verwerkingsbronnen te halen. Dit idee is best handig en kan in elke omgeving worden gebruikt, of het nu een enkele CPU of meerdere CPU's heeft. Het idee is hetzelfde.

Parallel programmeren betekent daarentegen dat er meerdere toegewijde CPU's zijn die parallel worden gebruikt door de programmeur. Dit type programmering is geoptimaliseerd voor een multicore CPU-omgeving. De meeste moderne machines gebruiken multicore-CPU's. Daarom is parallel programmeren tegenwoordig heel relevant. Zelfs de meest goedkope machine is uitgerust met multicore-CPU's. Kijk naar de draagbare apparaten; zelfs ze zijn multicore. Hoewel alles met multicore-CPU's knap lijkt, is hier ook een andere kant van het verhaal. Betekenen meer CPU-cores sneller of efficiënter computergebruik? Niet altijd! De hebzuchtige filosofie van "hoe meer hoe beter" is niet van toepassing op computers, noch in het leven. Maar ze zijn er, onmiskenbaar - dual, quad, octa, enzovoort. Ze zijn er meestal omdat we ze willen en niet omdat we ze nodig hebben, althans in de meeste gevallen. In werkelijkheid is het relatief moeilijk om zelfs maar een enkele CPU bezig te houden bij het dagelijkse computergebruik. Multicores hebben echter hun nut onder speciale omstandigheden, zoals bij servers, gaming, enzovoort, of het oplossen van grote problemen. Het probleem van het hebben van meerdere CPU's is dat het geheugen vereist dat de snelheid moet aanpassen aan de verwerkingskracht, samen met razendsnelle datakanalen en andere accessoires. Kortom, meerdere CPU-kernen in het dagelijkse computergebruik zorgen voor prestatieverbetering die niet opweegt tegen de hoeveelheid middelen die nodig zijn om het te gebruiken. Als gevolg hiervan krijgen we een onderbenutte dure machine, misschien alleen bedoeld om te worden tentoongesteld.

Parallel programmeren

In tegenstelling tot multithreading, waarbij elke taak een afzonderlijke logische eenheid is van een grotere taak, zijn parallelle programmeertaken onafhankelijk en doet de volgorde van uitvoering er niet toe. De taken worden gedefinieerd op basis van de functie die ze vervullen of de gegevens die bij de verwerking worden gebruikt; dit heet functioneel parallellisme of gegevensparallellisme , respectievelijk. Bij functioneel parallellisme werkt elke processor aan zijn deel van het probleem, terwijl bij gegevensparallellisme de processor aan zijn deel van de gegevens werkt. Parallel programmeren is geschikt voor een grotere probleembasis die niet past in een enkele CPU-architectuur, of het kan zijn dat het probleem zo groot is dat het niet binnen een redelijke tijdsinschatting kan worden opgelost. Als gevolg hiervan kunnen taken, wanneer ze over processors worden verdeeld, relatief snel het resultaat opleveren.

Het Fork/Join Framework

Het Fork/Join Framework is gedefinieerd in de java.util.concurrent pakket. Het bevat verschillende klassen en interfaces die parallel programmeren ondersteunen. Wat het in de eerste plaats doet, is dat het het proces van het maken van meerdere threads en het gebruik ervan vereenvoudigt en het mechanisme van procestoewijzing tussen meerdere processors automatiseert. Het opmerkelijke verschil tussen multithreading en parallel programmeren met dit framework lijkt erg op wat we eerder noemden. Hier is het verwerkingsgedeelte geoptimaliseerd om meerdere processors te gebruiken, in tegenstelling tot multithreading, waar de inactieve tijd van de enkele CPU wordt geoptimaliseerd op basis van gedeelde tijd. Het extra voordeel van dit framework is om multithreading te gebruiken in een parallelle uitvoeringsomgeving. Geen kwaad daar.

Er zijn vier kernklassen in dit raamwerk:

  • ForkJoinTask: Dit is een abstracte klasse die een taak definieert. Meestal wordt een taak gemaakt met behulp van de fork() methode gedefinieerd in deze klasse. Deze taak lijkt bijna op een normale thread die is gemaakt met de Thread klasse, maar is lichter dan dat. Het mechanisme dat het toepast, is dat het beheer van een groot aantal taken mogelijk maakt met behulp van een klein aantal daadwerkelijke threads die deelnemen aan de ForkJoinPool . De vork() methode maakt asynchrone uitvoering van de aanroepende taak mogelijk. De join() methode maakt het mogelijk te wachten tot de taak waarop deze wordt aangeroepen definitief is beëindigd. Er is een andere methode, genaamd invoke() , die de vork . combineert en doe mee bewerkingen in een enkel gesprek.
  • ForkJoinPool: Deze klasse biedt een gemeenschappelijke pool voor het beheren van de uitvoering van ForkJoinTask taken. Het biedt in feite het toegangspunt voor inzendingen van niet-ForkJoinTask klanten, evenals beheer- en monitoringactiviteiten.
  • Recursieve actie: Dit is ook een abstracte uitbreiding van de ForkJoinTask klas. Meestal breiden we deze klasse uit om een ​​taak te maken die geen resultaat oplevert of een leegte . heeft soort retour. De compute() methode gedefinieerd in deze klasse wordt overschreven om de rekencode van de taak op te nemen.
  • Recursieve taak: Dit is een andere abstracte uitbreiding van de ForkJoinTask klas. We breiden deze klasse uit om een ​​taak te maken die een resultaat retourneert. En, net als ResursiveAction, bevat het ook een beschermde abstracte compute() methode. Deze methode wordt overschreven om het rekengedeelte van de taak op te nemen.

De Fork/Join Framework-strategie

Dit raamwerk maakt gebruik van een recursieve verdeel-en-heers strategie om parallelle verwerking te implementeren. Het verdeelt een taak in feite in kleinere subtaken; vervolgens wordt elke subtaak verder onderverdeeld in sub-subtaken. Dit proces wordt recursief toegepast op elke taak totdat het klein genoeg is om opeenvolgend te worden afgehandeld. Stel dat we de waarden van een array van N . moeten verhogen nummers. Dit is de taak. Nu kunnen we de array door twee delen om twee subtaken te maken. Verdeel ze elk weer in nog twee subtaken, enzovoort. Op deze manier kunnen we een verdeel en heers . toepassen strategie recursief totdat de taken worden onderscheiden in een eenheidsprobleem. Dit eenheidsprobleem kan dan parallel worden uitgevoerd door de beschikbare meervoudige kernprocessors. In een niet-parallelle omgeving moesten we de hele array doorlopen en de verwerking in volgorde uitvoeren. Dit is duidelijk een inefficiënte benadering met het oog op parallelle verwerking. Maar de echte vraag is of elk probleem verdeeld en overwonnen kan worden ? Absoluut niet! Maar er zijn problemen die vaak gepaard gaan met een soort array, verzameling of groepering van gegevens die in het bijzonder bij deze benadering passen. Er zijn trouwens problemen waarbij het verzamelen van gegevens nog niet kan worden geoptimaliseerd om de strategie voor parallel programmeren te gebruiken. Welk type rekenproblemen geschikt zijn voor parallelle verwerking of discussie over parallelle algoritmen valt buiten het bestek van dit artikel. Laten we een snel voorbeeld bekijken van de toepassing van het Fork/Join Framework.

Een snel voorbeeld

Dit is een heel eenvoudig voorbeeld om u een idee te geven hoe u parallellisme in Java kunt implementeren met het Fork/Join Framework.

package org.mano.example;
import java.util.concurrent.RecursiveAction;
public class CustomRecursiveAction extends
      RecursiveAction {
   final int THRESHOLD = 2;
   double [] numbers;
   int indexStart, indexLast;
   CustomRecursiveAction(double [] n, int s, int l) {
      numbers = n;
      indexStart = s;
      indexLast = l;
   }
   @Override
   protected void compute() {
      if ((indexLast - indexStart) > THRESHOLD)
         for (int i = indexStart; i < indexLast; i++)
            numbers [i] = numbers [i] + Math.random();
         else
            invokeAll (new CustomRecursiveAction(numbers,
               indexStart, (indexStart - indexLast) / 2),
               new CustomRecursiveAction(numbers,
                  (indexStart - indexLast) / 2,
                     indexLast));
   }
}

package org.mano.example;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;
public class Main {
   public static void main(String[] args) {
      final int SIZE = 10;
      ForkJoinPool pool = new ForkJoinPool();
      double na[] = new double [SIZE];
      System.out.println("initialized random values :");
      for (int i = 0; i < na.length; i++) {
         na[i] = (double) i + Math.random();
         System.out.format("%.4f ", na[i]);
      }
      System.out.println();
      CustomRecursiveAction task = new
         CustomRecursiveAction(na, 0, na.length);
      pool.invoke(task);
      System.out.println("Changed values :");
      for (inti = 0; i < 10; i++)
      System.out.format("%.4f ", na[i]);
      System.out.println();
   }
}

Conclusie

Dit is een beknopte beschrijving van parallel programmeren en hoe het wordt ondersteund in Java. Het is een vaststaand feit dat het hebben van N cores gaat niet alles maken N keer sneller. Slechts een deel van Java-applicaties maakt effectief gebruik van deze functie. Parallelle programmeercode is een moeilijk frame. Bovendien moeten effectieve parallelle programma's rekening houden met zaken als taakverdeling, communicatie tussen parallelle taken en dergelijke. Er zijn enkele algoritmen die beter passen bij parallelle uitvoering, maar veel niet. In ieder geval komt de Java API niet tekort aan zijn ondersteuning. We kunnen altijd aan de API's sleutelen om erachter te komen wat het beste past. Veel plezier met coderen 🙂


  1. Hoe Joda-Time te gebruiken met java.sql.Timestamp

  2. Nieuwe functies in SQL Server 2017 (Database Engine)

  3. Resultaten van een SQL-query samenvoegen in Oracle

  4. Oracle Instant Client voor op ARM gebaseerd Debian-apparaat