Database
 sql >> Datenbank >  >> RDS >> Database

Grundlagen der parallelen Programmierung mit dem Fork/Join-Framework in Java

Mit dem Aufkommen von Multicore-CPUs in den letzten Jahren ist die parallele Programmierung der Weg, um die Vorteile der neuen Verarbeitungs-Arbeitspferde voll auszuschöpfen. Parallele Programmierung bezieht sich auf die gleichzeitige Ausführung von Prozessen aufgrund der Verfügbarkeit mehrerer Prozessorkerne. Dies führt im Wesentlichen zu einer enormen Leistungs- und Effizienzsteigerung der Programme gegenüber linearer Single-Core-Ausführung oder gar Multithreading. Das Fork/Join-Framework ist Teil der Java Concurrency API. Dieses Framework ermöglicht es Programmierern, Algorithmen zu parallelisieren. Dieser Artikel untersucht das Konzept der parallelen Programmierung mit Hilfe des in Java verfügbaren Fork/Join-Frameworks.

Ein Überblick

Parallele Programmierung hat eine viel breitere Konnotation und ist zweifellos ein riesiges Gebiet, das in wenigen Zeilen behandelt werden muss. Der Kern der Sache ist ganz einfach, aber operativ viel schwieriger zu erreichen. Einfach ausgedrückt bedeutet parallele Programmierung das Schreiben von Programmen, die mehr als einen Prozessor verwenden, um eine Aufgabe zu erledigen, das ist alles! Erraten Sie, was; Klingt bekannt, nicht wahr? Es reimt sich fast auf die Idee des Multithreading. Beachten Sie jedoch, dass es einige wichtige Unterschiede zwischen ihnen gibt. An der Oberfläche sind sie gleich, aber die Unterströmung ist absolut unterschiedlich. Tatsächlich wurde Multithreading eingeführt, um eine Art Illusion paralleler Verarbeitung ohne wirkliche parallele Ausführung zu erzeugen. Was Multithreading wirklich bewirkt, ist, dass es CPU-Leerlaufzeit stiehlt und zu seinem Vorteil nutzt.

Kurz gesagt, Multithreading ist eine Sammlung diskreter logischer Einheiten von Tasks, die ausgeführt werden, um ihren Anteil an CPU-Zeit zu beanspruchen, während ein anderer Thread vorübergehend beispielsweise auf Benutzereingaben wartet. Die Leerlauf-CPU-Zeit wird optimal auf konkurrierende Threads aufgeteilt. Wenn es nur eine CPU gibt, ist sie zeitgeteilt. Wenn mehrere CPU-Kerne vorhanden sind, werden sie auch immer gemeinsam genutzt. Daher quetscht ein optimales Multithread-Programm die Leistung der CPU durch den cleveren Mechanismus des Timesharing aus. Im Wesentlichen ist es immer ein Thread, der eine CPU verwendet, während ein anderer Thread wartet. Dies geschieht auf subtile Weise, sodass der Benutzer ein Gefühl der Parallelverarbeitung bekommt, bei der die Verarbeitung in Wirklichkeit in schneller Folge erfolgt. Der größte Vorteil von Multithreading besteht darin, dass es sich um eine Technik handelt, um das Beste aus den Verarbeitungsressourcen herauszuholen. Nun, diese Idee ist sehr nützlich und kann in jeder Umgebung verwendet werden, unabhängig davon, ob sie eine einzelne CPU oder mehrere CPUs hat. Die Idee ist dieselbe.

Parallele Programmierung bedeutet andererseits, dass es mehrere dedizierte CPUs gibt, die vom Programmierer parallel genutzt werden. Diese Art der Programmierung ist für eine Multicore-CPU-Umgebung optimiert. Die meisten der heutigen Maschinen verwenden Multicore-CPUs. Daher ist die parallele Programmierung heutzutage ziemlich relevant. Selbst die billigste Maschine ist mit Multicore-CPUs bestückt. Schauen Sie sich die tragbaren Geräte an; auch sie sind Multicore. Obwohl bei Multicore-CPUs alles in Ordnung zu sein scheint, gibt es hier auch eine andere Seite der Geschichte. Bedeuten mehr CPU-Kerne schnelleres oder effizienteres Rechnen? Nicht immer! Die gierige Philosophie „Je mehr, desto besser“ gilt weder für Computer noch für das Leben. Aber sie sind da, unübersehbar – Dual, Quad, Octa und so weiter. Sie sind meistens da, weil wir sie wollen und nicht, weil wir sie brauchen, zumindest in den meisten Fällen. In Wirklichkeit ist es relativ schwierig, auch nur eine einzelne CPU mit der täglichen Rechenleistung zu beschäftigen. Multicores werden jedoch unter besonderen Umständen verwendet, z. B. bei Servern, Spielen usw. oder zum Lösen großer Probleme. Das Problem bei mehreren CPUs besteht darin, dass Speicher erforderlich sind, der mit der Verarbeitungsleistung Schritt halten muss, zusammen mit blitzschnellen Datenkanälen und anderem Zubehör. Kurz gesagt, mehrere CPU-Kerne bei der täglichen Datenverarbeitung bieten eine Leistungsverbesserung, die die Menge an Ressourcen, die für ihre Nutzung erforderlich sind, nicht aufwiegen kann. Folglich erhalten wir eine nicht ausgelastete, teure Maschine, die vielleicht nur zur Präsentation gedacht ist.

Parallele Programmierung

Anders als beim Multithreading, bei dem jede Aufgabe eine eigenständige logische Einheit einer größeren Aufgabe ist, sind parallele Programmieraufgaben unabhängig und ihre Ausführungsreihenfolge spielt keine Rolle. Die Aufgaben werden nach der von ihnen ausgeführten Funktion oder den bei der Verarbeitung verwendeten Daten definiert; dies wird als funktionale Parallelität bezeichnet oder Datenparallelität , bzw. Bei der funktionalen Parallelität arbeitet jeder Prozessor an seinem Abschnitt des Problems, während bei der Datenparallelität der Prozessor an seinem Abschnitt der Daten arbeitet. Die parallele Programmierung eignet sich für eine größere Problembasis, die nicht in eine einzelne CPU-Architektur passt, oder das Problem kann so groß sein, dass es nicht in einer angemessenen Zeitschätzung gelöst werden kann. Infolgedessen können Aufgaben, wenn sie auf Prozessoren verteilt werden, das Ergebnis relativ schnell erhalten.

Das Fork/Join-Framework

Das Fork/Join-Framework ist in java.util.concurrent definiert Paket. Es enthält mehrere Klassen und Schnittstellen, die die parallele Programmierung unterstützen. Was es in erster Linie tut, ist, dass es den Prozess der Erstellung mehrerer Threads und ihre Verwendung vereinfacht und den Mechanismus der Prozesszuweisung zwischen mehreren Prozessoren automatisiert. Der bemerkenswerte Unterschied zwischen Multithreading und paralleler Programmierung mit diesem Framework ist dem bereits erwähnten sehr ähnlich. Hier ist der Verarbeitungsteil für die Verwendung mehrerer Prozessoren optimiert, im Gegensatz zu Multithreading, bei dem die Leerlaufzeit der einzelnen CPU auf der Grundlage der gemeinsamen Zeit optimiert wird. Der zusätzliche Vorteil dieses Frameworks besteht darin, Multithreading in einer parallelen Ausführungsumgebung zu verwenden. Kein Schaden.

Es gibt vier Kernklassen in diesem Framework:

  • ForkJoinTask: Dies ist eine abstrakte Klasse, die eine Aufgabe definiert. Typischerweise wird eine Aufgabe mit Hilfe von fork() erstellt Methode, die in dieser Klasse definiert ist. Diese Aufgabe ähnelt fast einem normalen Thread, der mit Thread erstellt wurde Klasse, ist aber leichter als sie. Der angewandte Mechanismus besteht darin, dass er die Verwaltung einer großen Anzahl von Aufgaben mit Hilfe einer kleinen Anzahl tatsächlicher Threads ermöglicht, die dem ForkJoinPool beitreten . Die Gabel() -Methode ermöglicht die asynchrone Ausführung der aufrufenden Aufgabe. Das join() -Methode ermöglicht das Warten, bis die Task, auf der sie aufgerufen wird, endgültig beendet ist. Es gibt noch eine andere Methode namens invoke() , das die Fork kombiniert und beitreten Vorgänge in einem einzigen Aufruf.
  • ForkJoinPool: Diese Klasse stellt einen gemeinsamen Pool bereit, um die Ausführung von ForkJoinTask zu verwalten Aufgaben. Es bietet im Wesentlichen den Einstiegspunkt für Einreichungen von Nicht-ForkJoinTask Kunden sowie Verwaltungs- und Überwachungsvorgänge.
  • Rekursive Aktion: Dies ist auch eine abstrakte Erweiterung der ForkJoinTask Klasse. Normalerweise erweitern wir diese Klasse, um eine Aufgabe zu erstellen, die kein Ergebnis zurückgibt oder eine void hat Rückgabetyp. Das compute() Die in dieser Klasse definierte Methode wird überschrieben, um den Berechnungscode der Aufgabe einzuschließen.
  • RekursiveAufgabe: Dies ist eine weitere abstrakte Erweiterung der ForkJoinTask Klasse. Wir erweitern diese Klasse, um eine Aufgabe zu erstellen, die ein Ergebnis zurückgibt. Und ähnlich wie ResursiveAction enthält es auch ein geschütztes abstraktes Compute() Methode. Diese Methode wird überschrieben, um den Berechnungsteil der Aufgabe einzuschließen.

Die Fork/Join-Rahmenstrategie

Dieses Framework verwendet ein rekursives Teile-und-Herrsche Strategie zur Implementierung der Parallelverarbeitung. Es unterteilt eine Aufgabe im Grunde in kleinere Teilaufgaben; dann wird jede Unteraufgabe weiter in Unter-Unteraufgaben unterteilt. Dieser Prozess wird rekursiv auf jede Aufgabe angewendet, bis sie klein genug ist, um sequentiell behandelt zu werden. Angenommen, wir sollen die Werte eines Arrays von N erhöhen Zahlen. Das ist die Aufgabe. Jetzt können wir das Array durch zwei teilen, um zwei Unteraufgaben zu erstellen. Teilen Sie jede von ihnen wieder in zwei weitere Unteraufgaben auf und so weiter. Auf diese Weise können wir ein Teile-und-Herrsche anwenden Strategie rekursiv, bis die Aufgaben zu einem Einheitsproblem herausgegriffen werden. Dieses Einheitenproblem kann dann von den verfügbaren Mehrkernprozessoren parallel ausgeführt werden. In einer nicht parallelen Umgebung mussten wir das gesamte Array durchlaufen und die Verarbeitung der Reihe nach durchführen. Dies ist im Hinblick auf die parallele Verarbeitung eindeutig ein ineffizienter Ansatz. Aber die eigentliche Frage ist, ob jedes Problem geteilt und überwunden werden kann ? Definitiv NEIN! Es gibt jedoch Probleme, die häufig eine Art Array, Sammlung oder Gruppierung von Daten beinhalten, die für diesen Ansatz besonders geeignet sind. Übrigens gibt es Probleme, die möglicherweise keine Datensammlung verwenden, die jedoch optimiert werden können, um die Strategie für parallele Programmierung zu verwenden. Welche Art von Berechnungsproblemen für die parallele Verarbeitung oder Diskussion über parallele Algorithmen geeignet sind, geht über den Rahmen dieses Artikels hinaus. Sehen wir uns ein kurzes Beispiel für die Anwendung des Fork/Join-Frameworks an.

Ein kurzes Beispiel

Dies ist ein sehr einfaches Beispiel, um Ihnen eine Vorstellung davon zu geben, wie Parallelität in Java mit dem Fork/Join-Framework implementiert werden kann.

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();
   }
}

Schlussfolgerung

Dies ist eine knappe Beschreibung der parallelen Programmierung und wie sie in Java unterstützt wird. Es ist eine wohlbekannte Tatsache, dass N zu haben Kerne werden nicht alles N machen mal schneller. Nur ein Teil der Java-Anwendungen nutzt diese Funktion effektiv. Paralleler Programmiercode ist ein schwieriger Rahmen. Darüber hinaus müssen effektive parallele Programme Themen wie Lastausgleich, Kommunikation zwischen parallelen Tasks und dergleichen berücksichtigen. Es gibt einige Algorithmen, die besser für die parallele Ausführung geeignet sind, viele jedoch nicht. An Unterstützung mangelt es der Java-API jedenfalls nicht. Wir können immer an den APIs basteln, um herauszufinden, was am besten passt. Viel Spaß beim Programmieren 🙂