Med fremkomsten af multicore CPU'er i de senere år, er parallel programmering måden at udnytte de nye behandlingsheste fuldt ud. Parallel programmering refererer til den samtidige udførelse af processer på grund af tilgængeligheden af flere behandlingskerner. Dette fører i bund og grund til et enormt løft i programmernes ydeevne og effektivitet i modsætning til lineær enkeltkerneudførelse eller endda multithreading. Fork/Join-rammen er en del af Java concurrency API. Denne ramme gør det muligt for programmører at parallelisere algoritmer. Denne artikel udforsker konceptet med parallel programmering ved hjælp af Fork/Join Framework, der er tilgængeligt i Java.
En oversigt
Parallel programmering har en meget bredere konnotation og er utvivlsomt et stort område, der skal uddybes på få linjer. Sagens kerne er ret enkel, men operationelt meget sværere at opnå. Enkelt sagt betyder parallel programmering at skrive programmer, der bruger mere end én processor til at fuldføre en opgave, det er alt! Gæt hvad; det lyder bekendt, gør det ikke? Det rimer næsten på ideen om multithreading. Men bemærk, at der er nogle vigtige forskelle mellem dem. På overfladen er de ens, men understrømmen er helt anderledes. Faktisk blev multithreading introduceret for at give en slags illusion af parallel bearbejdning uden nogen egentlig parallel eksekvering overhovedet. Hvad multithreading virkelig gør, er, at den stjæler CPU-tomtid og bruger den til sin fordel.
Kort sagt er multithreading en samling af diskrete logiske enheder af opgaver, der kører for at få deres del af CPU-tiden, mens en anden tråd midlertidigt venter på, f.eks. noget brugerinput. Den ledige CPU-tid deles optimalt mellem konkurrerende tråde. Hvis der kun er én CPU, er det tidsdelt. Hvis der er flere CPU-kerner, deles de også hele tiden. Derfor presser et optimalt flertrådet program CPU'ens ydeevne ud af den smarte mekanisme med tidsdeling. I bund og grund er det altid én tråd, der bruger én CPU, mens en anden tråd venter. Dette sker på en subtil måde, så brugeren får en fornemmelse af parallel bearbejdning, hvor bearbejdningen i virkeligheden faktisk foregår hurtigt efter hinanden. Den største fordel ved multithreading er, at det er en teknik til at få mest muligt ud af behandlingsressourcerne. Nu er denne idé ret nyttig og kan bruges i ethvert sæt miljøer, uanset om den har en enkelt CPU eller flere CPU'er. Ideen er den samme.
Parallel programmering betyder på den anden side, at der er flere dedikerede CPU'er, der udnyttes parallelt af programmøren. Denne type programmering er optimeret til et multicore CPU-miljø. De fleste af nutidens maskiner bruger multicore CPU'er. Derfor er parallel programmering ret relevant nu om dage. Selv den billigste maskine er monteret med multicore CPU'er. Se på de håndholdte enheder; selv de er multicore. Selvom alt virker smaskigt med multicore CPU'er, er her også en anden side af historien. Betyder flere CPU-kerner hurtigere eller effektiv databehandling? Ikke altid! Den grådige filosofi om "jo mere jo bedre" gælder ikke for computere og heller ikke i livet. Men de er der, uagtsomt - dual, quad, octa, og så videre. De er der mest fordi vi vil have dem og ikke fordi vi har brug for dem, i hvert fald i de fleste tilfælde. I virkeligheden er det relativt vanskeligt at holde selv en enkelt CPU beskæftiget i daglig computing. Multicores har dog deres anvendelse under særlige omstændigheder, såsom i servere, spil og så videre, eller løsning af store problemer. Problemet med at have flere CPU'er er, at det kræver hukommelse, der skal matche hastigheden med processorkraft, sammen med lynhurtige datakanaler og andet tilbehør. Kort sagt giver flere CPU-kerner i daglig computing ydeevneforbedring, der ikke kan opveje mængden af ressourcer, der er nødvendige for at bruge den. Derfor får vi en underudnyttet dyr maskine, måske kun beregnet til at blive fremvist.
Parallel programmering
I modsætning til multithreading, hvor hver opgave er en diskret logisk enhed af en større opgave, er parallelle programmeringsopgaver uafhængige, og deres udførelsesrækkefølge er ligegyldig. Opgaverne er defineret i henhold til den funktion, de udfører, eller data, der bruges i behandlingen; dette kaldes funktionel parallelisme eller dataparallelisme , henholdsvis. I funktionel parallelisme arbejder hver processor på sin del af problemet, mens i dataparallelisme arbejder processoren på sin del af dataene. Parallel programmering er velegnet til en større problembase, der ikke passer ind i en enkelt CPU-arkitektur, eller det kan være problemet er så stort, at det ikke kan løses på et rimeligt estimat af tid. Som et resultat kan opgaver, når de er fordelt mellem processorer, opnå resultatet relativt hurtigt.
The Fork/Join Framework
Fork/Join Framework er defineret i java.util.concurrent pakke. Det inkluderer flere klasser og grænseflader, der understøtter parallel programmering. Det, det primært gør, er, at det forenkler processen med oprettelse af flere tråde, deres anvendelser og automatiserer mekanismen for procesallokering mellem flere processorer. Den bemærkelsesværdige forskel mellem multithreading og parallel programmering med denne ramme ligner meget det, vi nævnte tidligere. Her er behandlingsdelen optimeret til at bruge flere processorer i modsætning til multithreading, hvor inaktivetiden for den enkelte CPU er optimeret på basis af delt tid. Den ekstra fordel ved denne ramme er at bruge multithreading i et parallelt eksekveringsmiljø. Ingen skade der.
Der er fire kerneklasser i denne ramme:
- ForkJoinTask
: Dette er en abstrakt klasse, der definerer en opgave. Typisk oprettes en opgave ved hjælp af fork() metode defineret i denne klasse. Denne opgave ligner næsten en normal tråd oprettet med Tråden klasse, men er lettere end den. Den mekanisme, den anvender, er, at den muliggør styring af et stort antal opgaver ved hjælp af et lille antal faktiske tråde, der slutter sig til ForkJoinPool . fork() metoden muliggør asynkron udførelse af den påkaldende opgave. join() metoden gør det muligt at vente, indtil den opgave, den kaldes på, endeligt afsluttes. Der er en anden metode, kaldet invoke() , som kombinerer gaflen og deltag operationer i et enkelt opkald. - ForkJoinPool: Denne klasse giver en fælles pulje til at styre udførelse af ForkJoinTask opgaver. Det giver dybest set adgangspunktet for indsendelser fra ikke-ForkJoinTask kunder, samt styring og overvågning af operationer.
- Rekursiv handling: Dette er også en abstrakt udvidelse af ForkJoinTask klasse. Typisk udvider vi denne klasse for at oprette en opgave, der ikke returnerer et resultat eller har et tomrum returtype. compute() metode defineret i denne klasse tilsidesættes for at inkludere beregningskode for opgaven.
- RekursivTask
: Dette er endnu en abstrakt udvidelse af ForkJoinTask klasse. Vi udvider denne klasse for at skabe en opgave, der returnerer et resultat. Og i lighed med ResursiveAction inkluderer den også en beskyttet abstrakt compute() metode. Denne metode tilsidesættes for at inkludere beregningsdelen af opgaven.
Fork/Join Framework-strategien
Denne ramme anvender en rekursiv del-og-hersk strategi for at implementere parallel behandling. Det opdeler grundlæggende en opgave i mindre delopgaver; derefter er hver delopgave yderligere opdelt i delunderopgaver. Denne proces anvendes rekursivt på hver opgave, indtil den er lille nok til at blive håndteret sekventielt. Antag, at vi skal øge værdierne af en matrix af N tal. Dette er opgaven. Nu kan vi opdele arrayet med to og skabe to underopgaver. Opdel hver af dem igen i yderligere to delopgaver, og så videre. På denne måde kan vi anvende en del-og-hersk strategi rekursivt, indtil opgaverne udskilles i et enhedsproblem. Dette enhedsproblem kan derefter udføres parallelt af de flere tilgængelige kerneprocessorer. I et ikke-parallelt miljø var det, vi skulle gøre, at cykle gennem hele arrayet og udføre behandlingen i rækkefølge. Dette er klart en ineffektiv tilgang i lyset af parallel behandling. Men det virkelige spørgsmål er, om ethvert problem kan opdeles og overvindes ? Bestemt NEJ! Men der er problemer, der ofte involverer en form for række, indsamling, gruppering af data, som især passer til denne tilgang. Af den måde, er der problemer, der måske ikke bruger indsamling af data, men kan optimeres til at bruge strategien for parallel programmering. Hvilken type beregningsmæssige problemer er egnede til parallel behandling eller diskussion om parallel algoritme, er uden for denne artikels omfang. Lad os se et hurtigt eksempel på anvendelsen af Fork/Join Framework.
Et hurtigt eksempel
Dette er et meget simpelt eksempel for at give dig en idé om, hvordan du implementerer parallelisme i Java med 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(); } }
Konklusion
Dette er en kortfattet beskrivelse af parallel programmering og hvordan det understøttes i Java. Det er et veletableret faktum, at have N kerner kommer ikke til at gøre alt til N gange hurtigere. Kun en del af Java-applikationer bruger denne funktion effektivt. Parallel programmeringskode er en vanskelig ramme. Desuden skal effektive parallelle programmer tage højde for emner som belastningsbalancering, kommunikation mellem parallelle opgaver og lignende. Der er nogle algoritmer, der passer bedre til parallel udførelse, men mange gør det ikke. Under alle omstændigheder mangler Java API'en ikke sin support. Vi kan altid pille ved API'erne for at finde ud af, hvad der passer bedst. God kodning 🙂