Sprievodca po krádeži práce v Jave

1. Prehľad

V tomto výučbe sa pozrieme na koncept kradnutia práce v Jave.

2. Čo je to kradnutie práce?

Práce s krádežou boli zavedené v Jave s cieľom zníženie sporu v aplikáciách s viacerými vláknami. To sa deje pomocou rámca fork / join.

2.1. Prístup Rozdeľuj a panuj

V rámci fork / join problémy alebo úlohy sa rekurzívne členia na čiastkové úlohy. Čiastkové úlohy sa potom riešia individuálne a výsledné výsledky sa kombinujú:

Výsledok vyriešiť (Problémový problém) {if (problém je malý) priamo vyriešiť problém iný {rozdeliť problém na samostatné časti vidlica nové čiastkové úlohy na riešenie každej časti spojiť všetky čiastkové úlohy zostaviť výsledok z čiastkových výsledkov}}

2.2. Pracovné vlákna

Členená úloha sa rieši pomocou pracovných vlákien poskytovaných fondom vlákien. Každé pracovné vlákno bude mať čiastkové úlohy, za ktoré je zodpovedné. Sú uložené v dvojitých radoch (deques).

Každé pracovné vlákno získava čiastkové úlohy zo svojho archívu nepretržitým vysúvaním čiastkovej úlohy z hornej časti dekády. Keď je deque pracovného vlákna prázdne, znamená to, že všetky čiastkové úlohy boli odstránené a dokončené.

V tomto okamihu si pracovné vlákno náhodne vyberie vlákno spoločného vlákna, z ktorého môže „ukradnúť“ prácu. Potom použije prístup FIFO (first-in-first-out) na prevzatie čiastkových úloh z chvosta dekády obete.

3. Implementácia vidlice / spojenia rámca

Skupinu vlákien, ktorá kradne prácu, môžeme vytvoriť buď pomocou ForkJoinPool triedy alebo Exekútori trieda:

ForkJoinPool commonPool = ForkJoinPool.commonPool (); ExecutorService workStealingPool = Executors.newWorkStealingPool ();

The Exekútori trieda má preťažené newWorkStealingPool metóda, ktorá berie celočíselný argument predstavujúci úroveň paralelizmu.

Executors.newWorkStealingPool je abstrakcia ForkJoinPool.commonPool. Rozdiel je iba v tom Executors.newWorkStealingPool vytvára fond v asynchrónnom režime a ForkJoinPool.commonPool nie.

4. Synchrónne vs asynchrónne oblasti vlákien

ForkJoinPool.commonPool používa konfiguráciu frontu last-in, first-out (LIFO), zatiaľ čo Executors.newWorkStealingPool používa prístup FIFO (first-in, first-out).

Podľa Doug Lea má prístup FIFO oproti LIFO tieto výhody:

  • Znižuje spory tým, že krádeže prevádzkujú na opačnej strane ako vlastníci
  • Využíva vlastnosť rekurzívnych algoritmov delenia a dobývania na včasné generovanie „veľkých“ úloh

Druhý bod vyššie znamená, že je možné ďalej rozdeliť staršiu ukradnutú úlohu pomocou vlákna, ktoré ju ukradlo.

Podľa dokumentácie Java, nastavenie asyncMode do pravda môžu byť vhodné na použitie s úlohami v štýle udalosti, ktoré sa nikdy nespájajú.

5. Pracovný príklad - hľadanie prvočísel

Použijeme príklad nájdenia prvočísel zo zbierky čísel na znázornenie výpočtový čas, výhody rámca pre krádež práce. Ukážeme tiež rozdiely medzi používaním synchrónnych a asynchrónnych oblastí vlákien.

5.1. Problém s prvočíslami

Nájsť prvočísla zo zbierky čísel môže byť výpočtovo nákladný proces. Je to dané hlavne veľkosťou zbierky čísel.

The Základné čísla trieda nám pomáha nájsť prvočísla:

verejná trieda PrimeNumbers rozširuje RecursiveAction {private int lowerBound; private int upperBound; súkromná int zrnitosť; statický konečný zoznam GRANULARITIES = Arrays.asList (1, 10, 100, 1000, 10 000); súkromné ​​AtomicInteger noOfPrimeNumbers; PrimeNumbers (int lowerBound, int upperBound, int granularity, AtomicInteger noOfPrimeNumbers) {this.lowerBound = lowerBound; this.upperBound = upperBound; this.granularity = granularity; this.noOfPrimeNumbers = noOfPrimeNumbers; } // iné konštruktory a metódy súkromné ​​Zoznam podúloh () {Zoznam podúloh = nový ArrayList (); for (int i = 1; i <= this.upperBound / granularity; i ++) {int upper = i * granularity; int lower = (horná - zrnitosť) + 1; subTasks.add (nové PrimeNumbers (dolné, horné, noOfPrimeNumbers)); } vrátiť čiastkové úlohy; } @Override protected void compute () {if (((upperBound + 1) - lowerBound)> granularity) {ForkJoinTask.invokeAll (subTasks ()); } else {findPrimeNumbers (); }} void findPrimeNumbers () {for (int num = lowerBound; num <= upperBound; num ++) {if (isPrime (num)) {noOfPrimeNumbers.getAndIncrement (); }}} public int noOfPrimeNumbers () {return noOfPrimeNumbers.intValue (); }}

Niekoľko dôležitých vecí, ktoré si treba uvedomiť o tejto triede:

  • Predlžuje sa RekurzívnaAkcia, ktorý nám umožňuje implementovať vypočítať metóda používaná pri výpočte úloh pomocou fondu vlákien
  • Rekurzívne rozkladá úlohy na čiastkové úlohy na základe zrnitosť hodnotu
  • Konštruktéri berú nižšie a horný viazané hodnoty, ktoré riadia rozsah čísel, pre ktoré chceme určiť prvočísla
  • Umožňuje nám to určiť prvočísla pomocou skupiny vlákien kradnúcich prácu alebo jedného vlákna

5.2. Riešenie problému rýchlejšie pomocou združených vlákien

Poďme určiť prvočísla spôsobom s jedným vláknom a tiež pomocou skupín krádeží pracovných vlákien.

Najprv sa pozrime na jednovláknový prístup:

PrimeNumbers prvočísla = nové PrimeNumbers (10 000); primes.findPrimeNumbers ();

A teraz, ForkJoinPool.commonPool prístup:

PrimeNumbers prvočísla = nové PrimeNumbers (10 000); ForkJoinPool pool = ForkJoinPool.commonPool (); pool.invoke (prvočísla); pool.shutdown ();

Na záver sa pozrieme na Executors.newWorkStealingPool prístup:

PrimeNumbers prvočísla = nové PrimeNumbers (10 000); int paralelizmus = ForkJoinPool.getCommonPoolParallelism (); ForkJoinPool stealer = (ForkJoinPool) Executors.newWorkStealingPool (paralelizmus); stealer.invoke (prvočísla); stealer.shutdown ();

Používame vzývať metóda ForkJoinPool triedy na odovzdávanie úloh do fondu vlákien. Táto metóda berie v prípade podtried triedy RekurzívnaAkcia. Pomocou Java Microbench Harness porovnávame tieto rôzne prístupy navzájom, pokiaľ ide o priemerný čas na operáciu:

# Spustenie dokončené. Celkový čas: 00:04:50 Benchmark Mode Cnt Skóre Chyba Jednotky PrimeNumbersUnitTest.Benchmarker.commonPoolBenchmark priem. 20 119,885 ± 9,917 ms / op PrimeNumbersUnitTest.Benchmarker.newWorkStealingPoolBenchmark priem. 20 119,791 ± 7,811 ms / op PrimeNumbersUnittest.Benchmark9,99,9,9 ms / op

Je jasné, že oboje ForkJoinPool.commonPool a Executors.newWorkStealingPool nám umožňujú určiť prvočísla rýchlejšie ako pri prístupe s jedným vláknom.

Rámec fork / join pool nám umožňuje rozdeliť úlohu na čiastkové úlohy. Zbierku 10 000 celých čísel sme rozdelili na dávky 1 - 100, 101 - 200, 201 - 300 a tak ďalej. Potom sme určili prvočísla pre každú dávku a sprístupnili celkový počet prvočísiel s našou noOfPrimeNumbers metóda.

5.3. Krádež práce na výpočet

Vďaka fondu synchrónnych vlákien ForkJoinPool.commonPool vloží vlákna do spoločného fondu, pokiaľ úloha stále prebieha.Výsledkom je, že úroveň krádeže práce nezávisí od úrovne podrobnosti úlohy.

Asynchrónny Executors.newWorkStealingPoolje riadenejšie, čo umožňuje závisieť úroveň kradnutia práce od úrovne podrobnosti úlohy.

Získame úroveň práce pri krádeži pomocou getStealCount z ForkJoinPool trieda:

long steals = forkJoinPool.getStealCount ();

Stanovenie počtu krádeží práce pre Executors.newWorkStealingPool a ForkJoinPool.commonPool nám dáva odlišné správanie:

Executors.newWorkStealingPool -> Granularity: [1], Steals: [6564] Granularity: [10], Steals: [572] Granularity: [100], Steals: [56] Granularity: [1000], Steals: [60] Granularity : [10 000], Kradnutia: [1] ForkJoinPool.commonPool -> Zoradenie: [1], Zrnitosť: [6923] Zrnitosť: [10], Zrnitosť: [7540] Zrnitosť: [100], Zrnitosť: [7605] Zrnitosť: [1 000], kradnutia: [7681] Zrnitosť: [10 000], kradnutia: [7681]

Keď sa zrnitosť zmení z jemnej na hrubú (1 až 10 000) pre Executors.newWorkStealingPool, úroveň kradnutia práce klesá. Počet krádeží je preto taký, keď sa úloha nerozloží (zrnitosť 10 000).

The ForkJoinPool.commonPool má iné správanie. Úroveň krádeže práce je vždy vysoká a zmena granulárnosti úloh ju veľmi neovplyvňuje.

Technicky vzaté, naším príkladom prvočísel je ten, ktorý podporuje asynchrónne spracovanie úloh v štýle udalosti. Je to tak preto, lebo naša implementácia nepresadzuje spájanie výsledkov.

Dá sa urobiť prípad Executors.newWorkStealingPool ponúka najlepšie využitie zdrojov pri riešení problému.

6. Záver

V tomto článku sme sa zaoberali krádežou práce a tým, ako ju aplikovať pomocou rámca fork / join. Pozreli sme sa tiež na príklady krádeže práce a na to, ako môže zlepšiť čas spracovania a využitie zdrojov.

Celý zdrojový kód príkladu je ako vždy k dispozícii na serveri GitHub.


$config[zx-auto] not found$config[zx-overlay] not found