KEMBAR78
JavaCro'15 - Java parallelization - Zlatko Sirotić | PDF
Stranica 1ISTRA TECHJavaCro 2015 – Java paralelizacija
JAVA PARALELIZACIJA
Zlatko Sirotić, univ.spec.inf.
ISTRA TECH d.o.o.
Pula
Stranica 2ISTRA TECHJavaCro 2015 – Java paralelizacija
Neki autorovi radovi
zadnjih godina
- HrOUG 2014: Nasljeđivanje je dobro, naročito višestruko -
Eiffel, C++, Scala, Java 8
- CASE 2014: Trebaju li nam distribuirane baze
u vrijeme oblaka?
- JavaCro 2014: Da li postoji samo jedna "ispravna" arhitektura
web poslovnih aplikacija
- HrOUG 2013: Transakcije i Oracle - baza, Forms, ADF
- CASE 2013: Što poslije Pascala? Pa … Scala!
- HrOUG 2012: Ima neka loša veza (priča o
in-doubt distribuiranim transakcijama)
- CASE 2012b: Konkurentno programiranje u Javi i Eiffelu
- CASE 2012a: Utjecaj razvoja μP na programiranje
Stranica 3ISTRA TECHJavaCro 2015 – Java paralelizacija
Uvod
 Gotovo sva današnja računala imaju više CPU-a, u obliku
višejezgrenih procesora, ili imaju više procesora.
 Takva računala mogu paralelno izvršavati dva (ili više)
nezavisna programa ili dijelove istog programa.
 U drugom slučaju, ako su dijelovi programa nezavisni,
programeri mogu pisati kod kao da nema paralelnosti.
 No, najčešće su dijelovi programa međusobno zavisni, jer
čitaju / pišu u isto memorijsko područje, pa je moguće da
rezultat izračuna ovisi o redoslijedu izvršenja programskih
instrukcija iz različitih dijelova programa.
 Zbog toga je potrebna sinkronizacija dijelova programa.
Sinkronizacija traži posebne programske tehnike, koje se
obično zovu imenom konkurentno programiranje.
Stranica 4ISTRA TECHJavaCro 2015 – Java paralelizacija
Teme
 Konkurentno programiranje i operacijski sustavi (kratko)
 Utjecaj razvoja mikroprocesora
na konkurentno programiranje (kratko)
 Sinkronizacijski algoritmi i mehanizmi (kratko)
 Konkurentno programiranje u Javi (kratko)
 Usporedba Java 5/6 Executora
i Java 7 ForkJoin frameworka,
na jednom jednostavnom primjeru
Stranica 5ISTRA TECHJavaCro 2015 – Java paralelizacija
Konkurentno programiranje
i operacijski sustavi
 Proces operacijskog sustava ima sljedeće elemente:
Stranica 6ISTRA TECHJavaCro 2015 – Java paralelizacija
Konkurentno programiranje
i operacijski sustavi
 Zamjena (swapping) procesa koji se izvršavaju na CPU-u
naziva se zamjena konteksta (context switch).
 Proces P1 treba biti zamijenjen procesom P2. Program
raspoređivač postavlja stanje procesa P1 na spreman i snima
njegov kontekst u memoriju, kako bi ga poslije mogao
"probuditi" i omogućiti da nastavi sa istog mjesta:
Stranica 7ISTRA TECHJavaCro 2015 – Java paralelizacija
Konkurentno programiranje
i operacijski sustavi
 Dobro je da se paralelno mogu izvršavati i dijelovi istog
programa – dretve ili niti (threads).
 Procesi koji su sastavljeni od više dretvi zovu se višedretveni
(multithreaded).
 Dretve jednog procesa dijele adresni prostor tog procesa,
tj. jedna dretva vidi podatke druge dretve.
 Zamjena konteksta dretvi u pravilu se izvodi nekoliko puta
brže od zamjene konteksta procesa. Zbog toga svi današnji
moderni OS nisu samo višeprocesni, nego i višedretveni.
 Dretve se mogu dodjeljivati procesorima isto kao i procesi.
Npr. u računalnom sustavu sa četiri CPU-a, sustav može u
određenom trenutku paralelno izvršavati četiri različita
procesa, ali može izvršavati i četiri dretve istog procesa.
Stranica 8ISTRA TECHJavaCro 2015 – Java paralelizacija
Konkurentno programiranje
i operacijski sustavi
 Dretve dijele globalnu memoriju (programski kod i globalne
podatke) i gomilu, ali imaju vlastiti stog i kontekst dretve:
Stranica 9ISTRA TECHJavaCro 2015 – Java paralelizacija
Utjecaj razvoja mikroprocesora
na konkurentno programiranje
 Mooreov zakon: Broj tranzistora na mikroprocesoru
udvostručuje se otprilike svake dvije godine.
 Mooreov zakon i dalje vrijedi. No, radni takt procesora
prestao je rasti oko 2005. Razlog za to je veliko povećanje
potrošnje struje na velikim brzinama.
Stranica 10ISTRA TECHJavaCro 2015 – Java paralelizacija
Utjecaj razvoja mikroprocesora
na konkurentno programiranje
 Slika pokazuje kako se eksponencijalno povećava jaz
između performansi CPU-a i glavne memorije (DRAM):
 Moguće je napraviti RAM (SRAM) koji bi bio brz skoro kao
registri procesora, ali bi bio puno skuplji. A, bolje je imati
više sporije glavne memorije nego manje brže glavne
memorije, jer je magnetski disk puno sporiji.
Stranica 11ISTRA TECHJavaCro 2015 – Java paralelizacija
Utjecaj razvoja mikroprocesora
na konkurentno programiranje
 Kod (hardverskog) multithreadinga su duplirani samo neki
elementi CPU-a, npr. procesorski registri. Zbog toga su
performanse do oko 1,3 puta veće. Povećanje broja
tranzistora počelo se koristiti za smještanje više CPU-a
(jezgri) u jedan procesorski čip. Primjer sustava sa dva
dvojezgrena procesora (svaka jezgra podržava dvije dretve):
Stranica 12ISTRA TECHJavaCro 2015 – Java paralelizacija
Utjecaj razvoja mikroprocesora
na konkurentno programiranje
 Amdahlov zakon: ako je u nekom programu proporcija
dijelova programa koji se mogu paralelno izvršavati
jednaka p, onda se povećanjem broja CPU-a može dobiti ovo
povećanje:
 Npr. ako imamo 10 procesora i p = 90% programa, onda je
maksimalno povećanje brzine 5,26 puta.
Stranica 13ISTRA TECHJavaCro 2015 – Java paralelizacija
Sinkronizacijski algoritmi i
mehanizmi
 Lokoti, tj. blokirajuća sinkronizacija
 Neblokirajući sinkronizacijski mehanizmi,
bez lokota (lock-free)
 Softverska, hardverska i hibridna transakcijska memorija
(STM, HTM, hibridna TM)
Pritom se koriste sljedeće vrste lokota:
 test-and-set (TAS) lokoti
 test-and-test-and-set (TATAS) lokoti
 lokoti temeljeni na redovima (queue-based locks)
 hijerarhijski lokoti
 lokoti tipa čitatelj-pisac (reader-writer locks)
Stranica 14ISTRA TECHJavaCro 2015 – Java paralelizacija
Sinkronizacijski algoritmi i
mehanizmi
 Točnost konkuretnog programa ovisi o međusobnom
redoslijedu izvođenja naredbi dva (ili više) programa. Taj se
problem uobičajeno race conditions.
 Da bismo riješili taj problem, dretve moramo sinkronizirati.
Sinkronizacija se zasniva na ideji da dretve komuniciraju
jedna sa drugom kako bi se "dogovorile" o sekvenci akcija.
 Dretve mogu komunicirati na dva načina:
- korištenjem djeljive memorije (shared memory): dretve
komuniciraju čitajući i pišući u zajednički dio memorije; ova
tehnika je dominanta i bit će korištena u nastavku;
- slanjem poruka (message-passing): dretve međusobno
komuniciraju porukama.
Stranica 15ISTRA TECHJavaCro 2015 – Java paralelizacija
Sinkronizacijski algoritmi i
mehanizmi
 Operacija test-and-set (TAS) bila je osnovna operacija za
sinkronizaciju u mikroprocesorima 1990-tih godina.
 Praktički svaki mikroprocesor dizajniran poslije 2000. godine
podržava jaču operaciju compare-and-swap (CAS), ili njoj
ekivalentnu. No, CAS i CASD (Compare-and-Swap-Double)
nisu novost – bile su dio IBM 370 arhitekture od 1970!
 Zanimljiv je pojam broj konsenzusa (consensus number), a
to je maksimalni broj procesa za koje određena primitivna
operacija (konsenzus objekt) može implementirati problem
konsenzusa.
 Atomarni registri imaju broj konsenzusa 1, TAS ima ima broj
konsenzusa 2, a CAS ima beskonačni broj konsenzusa.
CAS je osnova (današnjeg) konkurentnog programiranja.
Stranica 16ISTRA TECHJavaCro 2015 – Java paralelizacija
Sinkronizacijski algoritmi i
mehanizmi
 Danas barem tri mikroprocesora podržavaju HTM:
Vega firme Azul Systems (već 10 godina)
BlueGene/Q firme IBM (4 godine)
Intel Haswell (2 godine).
 HTM koristi tzv. MESI protokol:
- Modified: linija cache-a
je modificirana;
- Exclusive: linija nije modificirana,
i nijedan drugi procesor ju nema;
- Shared: linija nije modificirana,
ali drugi procesori ju mogu imati;
- Invalid: linija ne sadrži suvisle
podatke.
Stranica 17ISTRA TECHJavaCro 2015 – Java paralelizacija
Konkurentno programiranje
u Javi
 Svaka Java aplikacija koristi dretve. Kada se starta JVM,
on kreira posebne dretve, npr. za GC (garbage collection), uz
main dretvu.
 Kada koristimo Java AWT ili Swing framework, oni kreiraju
posebnu dretvu za upravljanje GUI-em.
 Kada koristimo servlete ili RMI, oni kreiraju pričuvu (pool)
dretvi. Zato, kada koristimo te frameworke, moramo biti
upoznati sa konkurentnošću u Javi.
 Svaki takav framework uvodi u našu aplikaciju konkurentnost
na implicitan način, te moramo znati napraviti da mješavina
našeg koda i frameworkovog koda bude sigurna u
višedretvenom radu.
Stranica 18ISTRA TECHJavaCro 2015 – Java paralelizacija
Konkurentno programiranje
u Javi
 U Javi verzije 5, koja se pojavila 2004. godine, uvedeno je
dosta novina na drugim područjima (generičke klase, bolje
kolekcije i dr.), ali i na području konkurentnog programiranja.
 Kroz novi paket java.util.concurrent uvedene su novosti:
- Locks (ReentrantLock, ReadWriteLock...);
- Conditions;
- Atomic variables;
- Executors (thread pools, scheduling);
- Futures;
- Concurrent Collections;
- Synchronizers (Semaphores, Barriers...);
- System enhancements.
Stranica 19ISTRA TECHJavaCro 2015 – Java paralelizacija
Konkurentno programiranje
u Javi
 U Java verziji 6, koja je izašla 2006., nije se pojavilo ništa
revolucionarno, uglavnom su se "iznutra" poboljšale
biblioteke, tj. riješili bugovi ili poboljšale performanse.
 U Java verziji 7, koja je izašla 2011., u području
konkurentnog programiranja najveća novost je
Fork/Join Framework.
 U Java verziji 8, koja je izašla 2014., u području
konkurentnog programiranja najveća novost je
Streams (lambda izrazi i default metode su, na neki način,
posljedica uvođenja Streams-a, ali su korisne i zasebno).
Stranica 20ISTRA TECHJavaCro 2015 – Java paralelizacija
Konkurentno programiranje
u Javi
 Na temelju onoga što čitamo i čujemo, mogli bismo zaključiti
da je najvažnija nova mogućnost u Javi 8 lambda izraz
(ili kraće, lambda).
 Inače, lambda izraz je (u Javi) naziv za metodu bez imena.
U pravilu je ta metoda funkcija, a ne procedura.
Zato možemo reći i da lambda izraz je anonimna funkcija,
koja se može javiti kao parametar (ili povratna vrijednost)
druge funkcije (koja je, onda, funkcija višeg reda).
 U Javi 8 pojavile su se i tzv. default metode u Java sučeljima
(interfaces). One, zapravo, predstavljaju uvođenje
višestrukog nasljeđivanja implementacije u Javu.
 Međutim, lambda izrazi i default metode su, na neki način,
posljedica uvođenja treće važne mogućnosti u Javi 8, a to su
Streamsi, koji nadograđuju dosadašnje Java kolekcije.
Stranica 21ISTRA TECHJavaCro 2015 – Java paralelizacija
Usporedba Java 5/6 Executora
i Java 7 ForkJoin frameworka
 Kako je već rečeno, u Javi 5 su kroz paket java.util.concurrent
uvedeni executori (tj. sučelja Executor, ExecutorService,
Callable, Future, klase Executors, ThreadPoolExecutor,
FutureTask i dr.)
 Executori, za razliku od direktnog rada s klasom Thread,
pomažu da se programeri koncentriraju na kreiranje zadataka
koji će se poslati executoru na izvršavanje, a optimizaciju
izvršavanja rade executori, koji koriste thread pool.
 Executori najčešće kreiraju fiksni thread pool:
ExecutorService ex = Executors.newFixedThreadPool(4);
 Može se kreirati i cached thread pool, kod kojeg se
automatski kreira onoliko Java dretvi koliko je potrebno:
… = Executors.newCachedThreadPool;
 Moguće je napraviti i thread pool sa samo jednom dretvom:
… = Executors.newSingleThreadExecutor;
Stranica 22ISTRA TECHJavaCro 2015 – Java paralelizacija
Usporedba Java 5/6 Executora
i Java 7 ForkJoin frameworka
 Executorima se može predati zadatak koji je instanca klase
sučelja Runnable.
 Ako je potrebno da zadatak vrati rezultat, onda se zadatak
kreira kao instanca klase sučelja Callable. Budući da Callable
(kao i Runnable) zadatak može izvršavati asinkrono, kod
asinkronog izvršavanja potrebna je i instanca klase sučelja
Future za dobijanje statusa i rezultata rada Callable.
 U našem ćemo primjeru koristi Callable i Future.
 Zadatak je: naći koliko ima prim (prostih) brojeva među
prvih N (npr. 10 000 000) prirodnih brojeva (N zadajemo).
 Naravno, želimo zadatak riješiti kroz paralelno programiranje,
tako da maksimalno koristimo procesorske resurse. Naravno,
cilj je postići minimalno vrijeme za izvršenje zadatka.
Stranica 23ISTRA TECHJavaCro 2015 – Java paralelizacija
Usporedba Java 5/6 Executora
i Java 7 ForkJoin frameworka
 Pitanje je kako podijeliti zadatak na podzadatke.
 Npr., ako je zadatak pretraživati prim brojeve do 10 milijuna,
možemo kreirati 10 podzadataka, od kojih će svaki
pretraživati milijun brojeva, ili kreirati 10 000 podzadataka,
od kojih će svaki pretraživati tisuću brojeva … itd.
 Zapravo, prvo pitanje je koliko Java dretvi kreirati?
Možda onoliko koliko ima podzadataka?
To najčešće ne bi bilo dobro!
 Java VM ne radi baš idealno sa prevelikim brojem Java dretvi
(možda je par tisuća već previše). Jedan od razloga je i
trošenje memorije za stack (svake) Java dretve.
 Osim toga, budući da su Java dretve najčešće realizirane
kroz dretve operacijskog sustava, zamjena konteksta dretvi
nije baš bez troškova (iako se kaže da je puno "jeftinija" od
zamjene konteksta procesa operacijskog sustava).
Stranica 24ISTRA TECHJavaCro 2015 – Java paralelizacija
Usporedba Java 5/6 Executora
i Java 7 ForkJoin frameworka
 Kako odrediti (relativno) optimalan broj Java dretvi?
 Ako je problem računski vrlo intenzivan, tj. uglavnom ovisi o
procesoru, a ne o I/O (Input/Output) operacijama, onda broj
Java dretvi možemo postaviti tako da bude jednak (ili malo
veći) broju HW (hardverskih) dretvi.
 Ako je problem IO intenzivan, onda se može kreirati više Java
dretvi nego što ima HW dretvi. Za (približno) određivanje broja
Java dretvi može se koristiti jednostavna formula:
broj_Java_dretvi =
broj_HW_dretvi / (1 – koeficijent_blokiranja)
 Koeficijent blokiranja (blocking coefficient) je broj između 0 i 1
i nije ga lako odrediti. Računski intenzivni problemi imaju
koeficijent koji se približava nuli, pa je tada preporučeni broj
Java dretvi jednak ili nešto malo veći od broja HW dretvi.
Stranica 25ISTRA TECHJavaCro 2015 – Java paralelizacija
Usporedba Java 5/6 Executora
i Java 7 ForkJoin frameworka
 IO intenzivni problemi mogu imati koeficijent koji se približava
jedinici. Ako je koeficijent blokiranja npr. 50%, onda je po ovoj
formuli preporučeni broj Java dretvi duplo veći od broja HW
dretvi.
 No, sada treba odrediti kako podijeliti problem, tj. koliko ćemo
imati podzadataka. Svaki podzadatak radit će konkurentno,
pa ih svakako treba biti barem onoliko koliko ima Java dretvi.
 Ako bi broj podzadataka bio potpuno jednak broju Java dretvi,
time bi se ignorirala priroda problema koji treba rješavati.
Naime, u tom slučaju bi dijelovi programa trebali biti savršeno
dobro izbalansirani.
 U našem zadatku, ako se broj dijelova naivno odredi tako da
se raspon brojeva podijeli sa brojem dretvi, zanemaruje se
važna činjenica da je lakše naći prim brojeve u donjim
dijelovima raspona brojeva, nego u gornjim dijelovima.
Stranica 26ISTRA TECHJavaCro 2015 – Java paralelizacija
Usporedba Java 5/6 Executora
i Java 7 ForkJoin frameworka
 U općenitom slučaju može se primijeniti relativno jednostavna
tehnika: broj podzadataka treba biti dovoljno velik da se
iskoriste postojeće Java dretve, tj. ne smije se desiti da
neke Java dretve ostanu neiskorištene.
 Dakle, broj podzadataka svakako mora biti veći od broja Java
dretvi. Naravno, nije lako odrediti (a ponekad niti moguće)
koliki točno treba biti taj broj – najčešće treba
eksperimentirati. Uglavnom se pokazuje da se kod početnog
povećanja broja podzadataka dobije značajno povećanje
performansi, a sa daljnjim povećanjem broja, povećanje
performansi je sve manje (performanse se mogu i smanjiti).
 Iza slike slijedi program, koji ima ove ulazne parametre:
- number: gornja granica do koje se traže prim brojevi;
- poolSize: broj Java dretvi;
- numberOfParts: broj podzadataka.
Stranica 27ISTRA TECHJavaCro 2015 – Java paralelizacija
Usporedba Java 5/6 Executora
i Java 7 ForkJoin frameworka
 Pronalaženje prim brojeva na μP sa 8 HW dretvi (4 jezgre * 2)
- prikaz efekta mijenjanja broja Java dretvi (PoolSize, os x)
i broja podzadataka (različite krivulje):
Stranica 28ISTRA TECHJavaCro 2015 – Java paralelizacija
Usporedba Java 5/6 Executora
i Java 7 ForkJoin frameworka
import java.util.concurrent.ExecutorService; ...
public class ExecutorPrimeFinder { ...
public static void main(final String[] args) {
if (args.length < 3) {
System.out.println("Usage: number poolSize numberOfParts");
} else {
final int number = Integer.parseInt(args[0]);
final int poolSize = Integer.parseInt(args[1]);
final int numberOfParts = Integer.parseInt(args[2]);
ExecutorPrimeFinder task = new ExecutorPrimeFinder();
final long startTime = System.nanoTime();
final long numberOfPrimes =
task.countPrimes(number, poolSize, numberOfParts);
final long endTime = System.nanoTime();
System.out.printf("Number of primes under %d is %dn",
number, numberOfPrimes, numberOfParts);
System.out.println("Time (seconds) taken is " +
(endTime - startTime) / 1.0e9);
}}}
Stranica 29ISTRA TECHJavaCro 2015 – Java paralelizacija
Usporedba Java 5/6 Executora
i Java 7 ForkJoin frameworka
protected int countPrimes
(final int number, final int poolSize, final int numberOfParts)
{ int count = 0;
try {
final List<Callable<Integer>> partitions =
new ArrayList<Callable<Integer>>();
final int chunksPerPartition = number / numberOfParts;
for(int i = 0; i < numberOfParts; i++) {
final int lower = (i * chunksPerPartition) + 1;
final int upper = (i == numberOfParts - 1) ?
number : lower + chunksPerPartition - 1;
partitions.add(new Callable<Integer>() {
public Integer call() {
return countPrimesInRange(lower, upper);
}
});
} ...
Stranica 30ISTRA TECHJavaCro 2015 – Java paralelizacija
Usporedba Java 5/6 Executora
i Java 7 ForkJoin frameworka
protected int countPrimes
(final int number, final int poolSize, final int numberOfParts)
{
...
final ExecutorService executorPool =
Executors.newFixedThreadPool(poolSize);
final List<Future<Integer>> resultFromParts =
executorPool.invokeAll(partitions,10000,TimeUnit.SECONDS);
executorPool.shutdown();
for(final Future<Integer> result : resultFromParts) {
count += result.get();
}
} catch(Exception ex) { throw new RuntimeException(ex); }
return count;
}
Stranica 31ISTRA TECHJavaCro 2015 – Java paralelizacija
Usporedba Java 5/6 Executora
i Java 7 ForkJoin frameworka
public int countPrimesInRange(final int lower, final int upper)
{
int total = 0;
for(int i = lower; i <= upper; i++) {
if (isPrime(i)) total++;
}
return total;
}
private boolean isPrime(final int number) {
if (number <= 1) return false;
if (number == 2) return true;
if (number % 2 == 0) return false;
for(int i = 3; i <= Math.sqrt(number); i = i + 2) {
if (number % i == 0) return false;
}
return true;
}
Stranica 32ISTRA TECHJavaCro 2015 – Java paralelizacija
Usporedba Java 5/6 Executora
i Java 7 ForkJoin frameworka
 Kako je već rečeno, u Javi 7 je uveden ForkJoin framework,
koji je, zapravo, specijalna verzija executora.
 Mark Reinhold, Chief Architect, Java Platform Group u
"Divide and Conquer Parallelism with the Fork/Join
Framework" (07.2011):
The fork/join framework minimizes per-task overhead
for compute-intensive tasks
– Not recommended for tasks that mix CPU and I/O activity
A portable way to express many parallel algorithms
– Code is independent of execution topology
– Reasonably efficient for a wide range of core counts
– Library-managed parallelism
Stranica 33ISTRA TECHJavaCro 2015 – Java paralelizacija
Usporedba Java 5/6 Executora
i Java 7 ForkJoin frameworka
 Mark Reinhold, Chief Architect, Java Platform Group u
"Divide and Conquer Parallelism with the Fork/Join
Framework" (07.2011):
No silver bullet - Many point solutions:
- Divide & conquer (fork/join)
- Work queues + thread pools
- Bulk data operations (select / map / reduce)
- Actors
- Software transactional memory (STM)
- GPU-based SIMD-style computation
Stranica 34ISTRA TECHJavaCro 2015 – Java paralelizacija
Usporedba Java 5/6 Executora
i Java 7 ForkJoin frameworka
 "Divide and Conquer" ("Divide et impera", "Podijeli pa vladaj"
ili "Zavadi pa vladaj" – stara rimska poslovica).
 Implementaciju ExecutorService sučelja u slučaju
ForkJoin frameworka radi klasa ForkJoinPool.
 Tipično se ForkJoinPool instanci šalje samo jedan zadatak
(task), a onda ForkJoinPool instanca i zadatak zajedno
primjenjuju tehniku Divide and Conquer.
 Broj Java dretvi u poolu se može zadati eksplicitno ili
implicitno:
-- broj Java dretvi se zadaje eksplicitno (u ovom slučaju 8)
ForkJoinPool fjPool = new ForkJoinPool(8);
-- ili implicitno (na računalu sa 8 HW dretvi, bit će ih 8)
-- framework koristi metodu Runtime.availableProcessors()
ForkJoinPool fjPool = new ForkJoinPool();
Stranica 35ISTRA TECHJavaCro 2015 – Java paralelizacija
Usporedba Java 5/6 Executora
i Java 7 ForkJoin frameworka
 Zadatak (task) treba biti instanca podklase apstraktne klase
ForkJoinTask, preciznije podklasa apstraktne podklase klase
ForkJoinTask, a najčešće su to RecursiveTask (u primjeru)
ili RecursiveAction.
 ForkJoinTask ima puno metoda, ali najvažnije su:
compute(), fork(), join()
 Pseudo kod za compute:
if (podzadatakJeDovoljnoMali()) {
rijesiPodzadatak();
} else {
MojForkJoinTask lijevaPolovica = ...
MojForkJoinTask desnaPolovica = ...
lijevaPolovica.fork(); -- stavi u queue
desnaPolovica.compute(); -- radi
lijevaPolovica.join(); -- čekaj završetak
}
Stranica 36ISTRA TECHJavaCro 2015 – Java paralelizacija
Usporedba Java 5/6 Executora
i Java 7 ForkJoin frameworka
 Za razliku od većine ExecutorService implementacija, kod
ForkJoin frameworka svaka Java dretva u ForkJoinPool-u
ima svoj red (queue) podzadataka na kojima radi.
 Metoda fork() stavlja ForkJoinTask u red tekuće Java dretve.
 Inicijalno je zauzeta samo jedna Java dretva - kada joj
pošaljemo (cijeli) zadatak. Dretva tada počinje dijeliti zadatak
u dva podzadatka, pa prvi podzadatak (lijevi) stavi u red, a
drugi podzadatak (desni) pokuša izvršiti (i tako rekurzivno):
Stranica 37ISTRA TECHJavaCro 2015 – Java paralelizacija
Usporedba Java 5/6 Executora
i Java 7 ForkJoin frameworka
 Ključna značajka ForkJoin frameworka je Work Stealing
(krađa posla). Java dretve kradu posao (podzadatak)
drugoj Java dretvi iz njenog reda podzadataka, i stavljaju
ga u svoj red (nešto što većina nas ne bi nikad napravila ).
 Vrlo je važan redoslijed stavljanja podzadataka u red.
Podzadaci koji se prvi stavljaju u red trebaju predstavljati
veći dio posla. Npr. na početku imamo jedan zadatak, koji
pokriva 100% posla. Njega dijelimo u dva podzadatka, svaki
po (otprilike) 50% posla. Prvi stavljamo u red, a drugi
obrađujemo, pri čemu drugi opet dijelimo u polovice, itd.
 One Java dretve koje nemaju posla (tj. njihov red je prazan),
kradu posao drugim Java dretvama, i to tako da uzmu posao
(podzadatak) koji je najstariji u redu, a to je istovremeno i
najveći posao iz reda druge Java dretve.
 Sljedeća slika pokazuje ta događanja, sa 4 Java dretve.
Stranica 38ISTRA TECHJavaCro 2015 – Java paralelizacija
Usporedba Java 5/6 Executora
i Java 7 ForkJoin frameworka
Stranica 39ISTRA TECHJavaCro 2015 – Java paralelizacija
Usporedba Java 5/6 Executora
i Java 7 ForkJoin frameworka
 Kada bi se zadatak mogao savršeno dijeliti na jednake
polovice, ne bi niti bilo potrebe za ForkJoin frameworkom,
mogli bismo koristiti i standardne executore.
 No, to je u praksi rijetko tako. Npr., u našem slučaju je lakše
pretraživati prim brojeve po donjoj polovici raspona brojeva,
nego gornjoj (jer gornja polovica sadrži veće brojeve).
 Zato je važno podijeliti posao na dovoljno veliki broj
podzadataka, tako da (sve do samog kraja) niti jedna Java
dretva ne ostane bez posla (tj. treba biti tako da jedna
dretva uvijek može ukrasti posao drugoj dretvi).
 Dijeljenje na podzadatke se, za razliku od primjera sa običnim
executorima, radi implicitno – ne zadaje se broj podzadataka,
već se određuje kada je podzadatak dovoljno mali.
 Nažalost, ne postoji opća metoda kojom se ispravno određuje
veličina tog malog podzadatka – to se radi eksperimentalno.
Stranica 40ISTRA TECHJavaCro 2015 – Java paralelizacija
Usporedba Java 5/6 Executora
i Java 7 ForkJoin frameworka
 Podzadatak na kojem se poziva metoda join() može tada biti
već gotov, jer ga je možda ukrala druga Java dretva, i već
napravila. Ili može biti ukraden, ali ga druga dretva upravo
radi, pa tada treba čekati da završi. Treći je slučaj da
podzadatak nije ukraden i tada ga radi tekuća dretva.
 Poziv join() metode u compute() metodi treba biti jedna od
zadnjih radnji, iza poziva fork() metode i rekurzivnog poziva
compute() metode. Budući da je to jako važno,
postoji metoda invokeAll(a2, a1); koja zamjenjuje niz
a1.fork(); a2.compute(); a1.join();
 Kako je već rečeno, zadatak (task) treba biti instanca
podklase apstraktnih klasa RecursiveAction ili RecursiveTask.
 RecursiveAction se koristi onda kada nije potrebno
vraćati rezultat, a RecursiveTask kada je potrebno vraćati
rezultat (kao u našem primjeru).
Stranica 41ISTRA TECHJavaCro 2015 – Java paralelizacija
Usporedba Java 5/6 Executora
i Java 7 ForkJoin frameworka
import java.util.concurrent.ForkJoinPool;
...
public class ForkJoinPrimeFinderTask
extends RecursiveTask<Integer>
{ private static int threshold;
private int start;
private int end;
public ForkJoinPrimeFinderTask
(final int theStart, final int theEnd)
{ start = theStart; end = theEnd; }
public static void main(final String[] args) {
if (args.length < 1 || args.length > 3) {
System.out.println("Usage: number poolSize threshold
OR number poolSize OR number");
return;
}
...
}
Stranica 42ISTRA TECHJavaCro 2015 – Java paralelizacija
Usporedba Java 5/6 Executora
i Java 7 ForkJoin frameworka
public static void main(final String[] args) { ...
final int number = Integer.parseInt(args[0]);
ForkJoinPrimeFinderTask task =
new ForkJoinPrimeFinderTask(1, number);
ForkJoinPool fjPool;
if (args.length == 2 || args.length == 3) {
fjPool = new ForkJoinPool(Integer.parseInt(args[1]));
} else { fjPool = new ForkJoinPool(); }
if (args.length == 3) {
threshold = Integer.parseInt(args[2]);
} else { threshold = 100;
final long startTime = System.nanoTime();
final long numberOfPrimes = fjPool.invoke(task);
final long endTime = System.nanoTime();
System.out.printf("Number of primes under %d is %dn",
number, numberOfPrimes);
System.out.println("Time (seconds) taken is " +
(endTime - startTime) / 1.0e9);
}
Stranica 43ISTRA TECHJavaCro 2015 – Java paralelizacija
Usporedba Java 5/6 Executora
i Java 7 ForkJoin frameworka
protected Integer compute() {
if (end - start <= threshold) {
return countPrimesInRange(start, end);
} else {
int halfWay = ((end - start) / 2) + start;
ForkJoinPrimeFinderTask t1 =
new ForkJoinPrimeFinderTask(start, halfWay);
ForkJoinPrimeFinderTask t2 =
new ForkJoinPrimeFinderTask(halfWay + 1, end);
t1.fork();
int count2 = t2.compute();
int count1 = t1.join();
return count1 + count2;
}
}
Stranica 44ISTRA TECHJavaCro 2015 – Java paralelizacija
Usporedba Java 5/6 Executora
i Java 7 ForkJoin frameworka
-- ovo je isto kao u primjeru Executora
public int countPrimesInRange(final int lower, final int upper)
{
int total = 0;
for(int i = lower; i <= upper; i++) {
if (isPrime(i)) total++;
}
return total;
}
private boolean isPrime(final int number) {
if (number <= 1) return false;
if (number == 2) return true;
if (number % 2 == 0) return false;
for(int i = 3; i <= Math.sqrt(number); i = i + 2) {
if (number % i == 0) return false;
}
return true;
}
Stranica 45ISTRA TECHJavaCro 2015 – Java paralelizacija
Usporedba Java 5/6 Executora
i Java 7 ForkJoin frameworka
ExecutorPrimeFinder 10000000 1 10000 Number of primes under 10000000 is 664579
Time (seconds) taken is 12,70
ExecutorPrimeFinder 10000000 2 10000
Time (seconds) taken is 6,60
ExecutorPrimeFinder 10000000 4 10000
Time (seconds) taken is 3,20
ExecutorPrimeFinder 10000000 8 10000
Time (seconds) taken is 3,18
ExecutorPrimeFinder 10000000 16 10000
Time (seconds) taken is 3,17
ForkJoinPrimeFinderTask 10000000 1 1000 Number of primes under 10000000 is 664579
Time (seconds) taken is 14,41
ForkJoinPrimeFinderTask 10000000 2 1000
Time (seconds) taken is 7,36
ForkJoinPrimeFinderTask 10000000 4 1000
Time (seconds) taken is 3,91
ForkJoinPrimeFinderTask 10000000 8 1000
Time (seconds) taken is 3,28
ForkJoinPrimeFinderTask 10000000 16 1000
Time (seconds) taken is 3,25
ForkJoinPrimeFinderTask 10000000 16 100
Time (seconds) taken is 2,93
Stranica 46ISTRA TECHJavaCro 2015 – Java paralelizacija
Zaključak
 Mooreov zakon vrijedi i dalje. No, radni takt procesora
praktički je prestao rasti oko 2005. godine. Umjesto
povećanja brzine, proizvođači povećavaju broj CPU-a
(jezgri) na jednom mikroprocesorskom čipu.
 Dok smo kod jednojezgrenih procesora povećanjem takta
procesora dobili linearno povećanje brzine programa, kod
višejezgrenih procesora program najčešće moramo pisati
drugačije da bismo iskoristili raspoložive jezgre, a razlog za
to objašnjava Amdahlov zakon.
 Nažalost, konkurentne programe nije lako pisati. Iako dretve
rade paralelno, moramo investirati veliki trud da
implementiramo tehnike koje ih sprečavaju da na loš način
utječu jedna na drugu.
Stranica 47ISTRA TECHJavaCro 2015 – Java paralelizacija
Zaključak
 Zato je izmišljena transakcijska memorija (TM), koja može
biti softverska (STM), hardverska (HTM) ili hibridna.
Postoje implementacije STM-a na razini jezika (npr. jezik
Clojure) ili na razini biblioteka (npr. jezik Scala).
 Što se tiče HTM-a, danas postoje barem tri mikroprocesora
koja ju podržavaju - Azul Systems Vega, IBM BlueGene/Q,
Intel Haswell.
 Postoje i neka softverska rješenja, koja omogućavaju
relativno jednostavno i sigurno konkurentno
programiranje u Javi (preciznije, paralelno programiranje).
Između ostalih, to su executori i ForkJoin framework (koji je
specijalna vrsta executora). Nažalost, nisu svi zadaci prikladni
za paralelizaciju pomoću tih rješenja.

JavaCro'15 - Java parallelization - Zlatko Sirotić

  • 1.
    Stranica 1ISTRA TECHJavaCro2015 – Java paralelizacija JAVA PARALELIZACIJA Zlatko Sirotić, univ.spec.inf. ISTRA TECH d.o.o. Pula
  • 2.
    Stranica 2ISTRA TECHJavaCro2015 – Java paralelizacija Neki autorovi radovi zadnjih godina - HrOUG 2014: Nasljeđivanje je dobro, naročito višestruko - Eiffel, C++, Scala, Java 8 - CASE 2014: Trebaju li nam distribuirane baze u vrijeme oblaka? - JavaCro 2014: Da li postoji samo jedna "ispravna" arhitektura web poslovnih aplikacija - HrOUG 2013: Transakcije i Oracle - baza, Forms, ADF - CASE 2013: Što poslije Pascala? Pa … Scala! - HrOUG 2012: Ima neka loša veza (priča o in-doubt distribuiranim transakcijama) - CASE 2012b: Konkurentno programiranje u Javi i Eiffelu - CASE 2012a: Utjecaj razvoja μP na programiranje
  • 3.
    Stranica 3ISTRA TECHJavaCro2015 – Java paralelizacija Uvod  Gotovo sva današnja računala imaju više CPU-a, u obliku višejezgrenih procesora, ili imaju više procesora.  Takva računala mogu paralelno izvršavati dva (ili više) nezavisna programa ili dijelove istog programa.  U drugom slučaju, ako su dijelovi programa nezavisni, programeri mogu pisati kod kao da nema paralelnosti.  No, najčešće su dijelovi programa međusobno zavisni, jer čitaju / pišu u isto memorijsko područje, pa je moguće da rezultat izračuna ovisi o redoslijedu izvršenja programskih instrukcija iz različitih dijelova programa.  Zbog toga je potrebna sinkronizacija dijelova programa. Sinkronizacija traži posebne programske tehnike, koje se obično zovu imenom konkurentno programiranje.
  • 4.
    Stranica 4ISTRA TECHJavaCro2015 – Java paralelizacija Teme  Konkurentno programiranje i operacijski sustavi (kratko)  Utjecaj razvoja mikroprocesora na konkurentno programiranje (kratko)  Sinkronizacijski algoritmi i mehanizmi (kratko)  Konkurentno programiranje u Javi (kratko)  Usporedba Java 5/6 Executora i Java 7 ForkJoin frameworka, na jednom jednostavnom primjeru
  • 5.
    Stranica 5ISTRA TECHJavaCro2015 – Java paralelizacija Konkurentno programiranje i operacijski sustavi  Proces operacijskog sustava ima sljedeće elemente:
  • 6.
    Stranica 6ISTRA TECHJavaCro2015 – Java paralelizacija Konkurentno programiranje i operacijski sustavi  Zamjena (swapping) procesa koji se izvršavaju na CPU-u naziva se zamjena konteksta (context switch).  Proces P1 treba biti zamijenjen procesom P2. Program raspoređivač postavlja stanje procesa P1 na spreman i snima njegov kontekst u memoriju, kako bi ga poslije mogao "probuditi" i omogućiti da nastavi sa istog mjesta:
  • 7.
    Stranica 7ISTRA TECHJavaCro2015 – Java paralelizacija Konkurentno programiranje i operacijski sustavi  Dobro je da se paralelno mogu izvršavati i dijelovi istog programa – dretve ili niti (threads).  Procesi koji su sastavljeni od više dretvi zovu se višedretveni (multithreaded).  Dretve jednog procesa dijele adresni prostor tog procesa, tj. jedna dretva vidi podatke druge dretve.  Zamjena konteksta dretvi u pravilu se izvodi nekoliko puta brže od zamjene konteksta procesa. Zbog toga svi današnji moderni OS nisu samo višeprocesni, nego i višedretveni.  Dretve se mogu dodjeljivati procesorima isto kao i procesi. Npr. u računalnom sustavu sa četiri CPU-a, sustav može u određenom trenutku paralelno izvršavati četiri različita procesa, ali može izvršavati i četiri dretve istog procesa.
  • 8.
    Stranica 8ISTRA TECHJavaCro2015 – Java paralelizacija Konkurentno programiranje i operacijski sustavi  Dretve dijele globalnu memoriju (programski kod i globalne podatke) i gomilu, ali imaju vlastiti stog i kontekst dretve:
  • 9.
    Stranica 9ISTRA TECHJavaCro2015 – Java paralelizacija Utjecaj razvoja mikroprocesora na konkurentno programiranje  Mooreov zakon: Broj tranzistora na mikroprocesoru udvostručuje se otprilike svake dvije godine.  Mooreov zakon i dalje vrijedi. No, radni takt procesora prestao je rasti oko 2005. Razlog za to je veliko povećanje potrošnje struje na velikim brzinama.
  • 10.
    Stranica 10ISTRA TECHJavaCro2015 – Java paralelizacija Utjecaj razvoja mikroprocesora na konkurentno programiranje  Slika pokazuje kako se eksponencijalno povećava jaz između performansi CPU-a i glavne memorije (DRAM):  Moguće je napraviti RAM (SRAM) koji bi bio brz skoro kao registri procesora, ali bi bio puno skuplji. A, bolje je imati više sporije glavne memorije nego manje brže glavne memorije, jer je magnetski disk puno sporiji.
  • 11.
    Stranica 11ISTRA TECHJavaCro2015 – Java paralelizacija Utjecaj razvoja mikroprocesora na konkurentno programiranje  Kod (hardverskog) multithreadinga su duplirani samo neki elementi CPU-a, npr. procesorski registri. Zbog toga su performanse do oko 1,3 puta veće. Povećanje broja tranzistora počelo se koristiti za smještanje više CPU-a (jezgri) u jedan procesorski čip. Primjer sustava sa dva dvojezgrena procesora (svaka jezgra podržava dvije dretve):
  • 12.
    Stranica 12ISTRA TECHJavaCro2015 – Java paralelizacija Utjecaj razvoja mikroprocesora na konkurentno programiranje  Amdahlov zakon: ako je u nekom programu proporcija dijelova programa koji se mogu paralelno izvršavati jednaka p, onda se povećanjem broja CPU-a može dobiti ovo povećanje:  Npr. ako imamo 10 procesora i p = 90% programa, onda je maksimalno povećanje brzine 5,26 puta.
  • 13.
    Stranica 13ISTRA TECHJavaCro2015 – Java paralelizacija Sinkronizacijski algoritmi i mehanizmi  Lokoti, tj. blokirajuća sinkronizacija  Neblokirajući sinkronizacijski mehanizmi, bez lokota (lock-free)  Softverska, hardverska i hibridna transakcijska memorija (STM, HTM, hibridna TM) Pritom se koriste sljedeće vrste lokota:  test-and-set (TAS) lokoti  test-and-test-and-set (TATAS) lokoti  lokoti temeljeni na redovima (queue-based locks)  hijerarhijski lokoti  lokoti tipa čitatelj-pisac (reader-writer locks)
  • 14.
    Stranica 14ISTRA TECHJavaCro2015 – Java paralelizacija Sinkronizacijski algoritmi i mehanizmi  Točnost konkuretnog programa ovisi o međusobnom redoslijedu izvođenja naredbi dva (ili više) programa. Taj se problem uobičajeno race conditions.  Da bismo riješili taj problem, dretve moramo sinkronizirati. Sinkronizacija se zasniva na ideji da dretve komuniciraju jedna sa drugom kako bi se "dogovorile" o sekvenci akcija.  Dretve mogu komunicirati na dva načina: - korištenjem djeljive memorije (shared memory): dretve komuniciraju čitajući i pišući u zajednički dio memorije; ova tehnika je dominanta i bit će korištena u nastavku; - slanjem poruka (message-passing): dretve međusobno komuniciraju porukama.
  • 15.
    Stranica 15ISTRA TECHJavaCro2015 – Java paralelizacija Sinkronizacijski algoritmi i mehanizmi  Operacija test-and-set (TAS) bila je osnovna operacija za sinkronizaciju u mikroprocesorima 1990-tih godina.  Praktički svaki mikroprocesor dizajniran poslije 2000. godine podržava jaču operaciju compare-and-swap (CAS), ili njoj ekivalentnu. No, CAS i CASD (Compare-and-Swap-Double) nisu novost – bile su dio IBM 370 arhitekture od 1970!  Zanimljiv je pojam broj konsenzusa (consensus number), a to je maksimalni broj procesa za koje određena primitivna operacija (konsenzus objekt) može implementirati problem konsenzusa.  Atomarni registri imaju broj konsenzusa 1, TAS ima ima broj konsenzusa 2, a CAS ima beskonačni broj konsenzusa. CAS je osnova (današnjeg) konkurentnog programiranja.
  • 16.
    Stranica 16ISTRA TECHJavaCro2015 – Java paralelizacija Sinkronizacijski algoritmi i mehanizmi  Danas barem tri mikroprocesora podržavaju HTM: Vega firme Azul Systems (već 10 godina) BlueGene/Q firme IBM (4 godine) Intel Haswell (2 godine).  HTM koristi tzv. MESI protokol: - Modified: linija cache-a je modificirana; - Exclusive: linija nije modificirana, i nijedan drugi procesor ju nema; - Shared: linija nije modificirana, ali drugi procesori ju mogu imati; - Invalid: linija ne sadrži suvisle podatke.
  • 17.
    Stranica 17ISTRA TECHJavaCro2015 – Java paralelizacija Konkurentno programiranje u Javi  Svaka Java aplikacija koristi dretve. Kada se starta JVM, on kreira posebne dretve, npr. za GC (garbage collection), uz main dretvu.  Kada koristimo Java AWT ili Swing framework, oni kreiraju posebnu dretvu za upravljanje GUI-em.  Kada koristimo servlete ili RMI, oni kreiraju pričuvu (pool) dretvi. Zato, kada koristimo te frameworke, moramo biti upoznati sa konkurentnošću u Javi.  Svaki takav framework uvodi u našu aplikaciju konkurentnost na implicitan način, te moramo znati napraviti da mješavina našeg koda i frameworkovog koda bude sigurna u višedretvenom radu.
  • 18.
    Stranica 18ISTRA TECHJavaCro2015 – Java paralelizacija Konkurentno programiranje u Javi  U Javi verzije 5, koja se pojavila 2004. godine, uvedeno je dosta novina na drugim područjima (generičke klase, bolje kolekcije i dr.), ali i na području konkurentnog programiranja.  Kroz novi paket java.util.concurrent uvedene su novosti: - Locks (ReentrantLock, ReadWriteLock...); - Conditions; - Atomic variables; - Executors (thread pools, scheduling); - Futures; - Concurrent Collections; - Synchronizers (Semaphores, Barriers...); - System enhancements.
  • 19.
    Stranica 19ISTRA TECHJavaCro2015 – Java paralelizacija Konkurentno programiranje u Javi  U Java verziji 6, koja je izašla 2006., nije se pojavilo ništa revolucionarno, uglavnom su se "iznutra" poboljšale biblioteke, tj. riješili bugovi ili poboljšale performanse.  U Java verziji 7, koja je izašla 2011., u području konkurentnog programiranja najveća novost je Fork/Join Framework.  U Java verziji 8, koja je izašla 2014., u području konkurentnog programiranja najveća novost je Streams (lambda izrazi i default metode su, na neki način, posljedica uvođenja Streams-a, ali su korisne i zasebno).
  • 20.
    Stranica 20ISTRA TECHJavaCro2015 – Java paralelizacija Konkurentno programiranje u Javi  Na temelju onoga što čitamo i čujemo, mogli bismo zaključiti da je najvažnija nova mogućnost u Javi 8 lambda izraz (ili kraće, lambda).  Inače, lambda izraz je (u Javi) naziv za metodu bez imena. U pravilu je ta metoda funkcija, a ne procedura. Zato možemo reći i da lambda izraz je anonimna funkcija, koja se može javiti kao parametar (ili povratna vrijednost) druge funkcije (koja je, onda, funkcija višeg reda).  U Javi 8 pojavile su se i tzv. default metode u Java sučeljima (interfaces). One, zapravo, predstavljaju uvođenje višestrukog nasljeđivanja implementacije u Javu.  Međutim, lambda izrazi i default metode su, na neki način, posljedica uvođenja treće važne mogućnosti u Javi 8, a to su Streamsi, koji nadograđuju dosadašnje Java kolekcije.
  • 21.
    Stranica 21ISTRA TECHJavaCro2015 – Java paralelizacija Usporedba Java 5/6 Executora i Java 7 ForkJoin frameworka  Kako je već rečeno, u Javi 5 su kroz paket java.util.concurrent uvedeni executori (tj. sučelja Executor, ExecutorService, Callable, Future, klase Executors, ThreadPoolExecutor, FutureTask i dr.)  Executori, za razliku od direktnog rada s klasom Thread, pomažu da se programeri koncentriraju na kreiranje zadataka koji će se poslati executoru na izvršavanje, a optimizaciju izvršavanja rade executori, koji koriste thread pool.  Executori najčešće kreiraju fiksni thread pool: ExecutorService ex = Executors.newFixedThreadPool(4);  Može se kreirati i cached thread pool, kod kojeg se automatski kreira onoliko Java dretvi koliko je potrebno: … = Executors.newCachedThreadPool;  Moguće je napraviti i thread pool sa samo jednom dretvom: … = Executors.newSingleThreadExecutor;
  • 22.
    Stranica 22ISTRA TECHJavaCro2015 – Java paralelizacija Usporedba Java 5/6 Executora i Java 7 ForkJoin frameworka  Executorima se može predati zadatak koji je instanca klase sučelja Runnable.  Ako je potrebno da zadatak vrati rezultat, onda se zadatak kreira kao instanca klase sučelja Callable. Budući da Callable (kao i Runnable) zadatak može izvršavati asinkrono, kod asinkronog izvršavanja potrebna je i instanca klase sučelja Future za dobijanje statusa i rezultata rada Callable.  U našem ćemo primjeru koristi Callable i Future.  Zadatak je: naći koliko ima prim (prostih) brojeva među prvih N (npr. 10 000 000) prirodnih brojeva (N zadajemo).  Naravno, želimo zadatak riješiti kroz paralelno programiranje, tako da maksimalno koristimo procesorske resurse. Naravno, cilj je postići minimalno vrijeme za izvršenje zadatka.
  • 23.
    Stranica 23ISTRA TECHJavaCro2015 – Java paralelizacija Usporedba Java 5/6 Executora i Java 7 ForkJoin frameworka  Pitanje je kako podijeliti zadatak na podzadatke.  Npr., ako je zadatak pretraživati prim brojeve do 10 milijuna, možemo kreirati 10 podzadataka, od kojih će svaki pretraživati milijun brojeva, ili kreirati 10 000 podzadataka, od kojih će svaki pretraživati tisuću brojeva … itd.  Zapravo, prvo pitanje je koliko Java dretvi kreirati? Možda onoliko koliko ima podzadataka? To najčešće ne bi bilo dobro!  Java VM ne radi baš idealno sa prevelikim brojem Java dretvi (možda je par tisuća već previše). Jedan od razloga je i trošenje memorije za stack (svake) Java dretve.  Osim toga, budući da su Java dretve najčešće realizirane kroz dretve operacijskog sustava, zamjena konteksta dretvi nije baš bez troškova (iako se kaže da je puno "jeftinija" od zamjene konteksta procesa operacijskog sustava).
  • 24.
    Stranica 24ISTRA TECHJavaCro2015 – Java paralelizacija Usporedba Java 5/6 Executora i Java 7 ForkJoin frameworka  Kako odrediti (relativno) optimalan broj Java dretvi?  Ako je problem računski vrlo intenzivan, tj. uglavnom ovisi o procesoru, a ne o I/O (Input/Output) operacijama, onda broj Java dretvi možemo postaviti tako da bude jednak (ili malo veći) broju HW (hardverskih) dretvi.  Ako je problem IO intenzivan, onda se može kreirati više Java dretvi nego što ima HW dretvi. Za (približno) određivanje broja Java dretvi može se koristiti jednostavna formula: broj_Java_dretvi = broj_HW_dretvi / (1 – koeficijent_blokiranja)  Koeficijent blokiranja (blocking coefficient) je broj između 0 i 1 i nije ga lako odrediti. Računski intenzivni problemi imaju koeficijent koji se približava nuli, pa je tada preporučeni broj Java dretvi jednak ili nešto malo veći od broja HW dretvi.
  • 25.
    Stranica 25ISTRA TECHJavaCro2015 – Java paralelizacija Usporedba Java 5/6 Executora i Java 7 ForkJoin frameworka  IO intenzivni problemi mogu imati koeficijent koji se približava jedinici. Ako je koeficijent blokiranja npr. 50%, onda je po ovoj formuli preporučeni broj Java dretvi duplo veći od broja HW dretvi.  No, sada treba odrediti kako podijeliti problem, tj. koliko ćemo imati podzadataka. Svaki podzadatak radit će konkurentno, pa ih svakako treba biti barem onoliko koliko ima Java dretvi.  Ako bi broj podzadataka bio potpuno jednak broju Java dretvi, time bi se ignorirala priroda problema koji treba rješavati. Naime, u tom slučaju bi dijelovi programa trebali biti savršeno dobro izbalansirani.  U našem zadatku, ako se broj dijelova naivno odredi tako da se raspon brojeva podijeli sa brojem dretvi, zanemaruje se važna činjenica da je lakše naći prim brojeve u donjim dijelovima raspona brojeva, nego u gornjim dijelovima.
  • 26.
    Stranica 26ISTRA TECHJavaCro2015 – Java paralelizacija Usporedba Java 5/6 Executora i Java 7 ForkJoin frameworka  U općenitom slučaju može se primijeniti relativno jednostavna tehnika: broj podzadataka treba biti dovoljno velik da se iskoriste postojeće Java dretve, tj. ne smije se desiti da neke Java dretve ostanu neiskorištene.  Dakle, broj podzadataka svakako mora biti veći od broja Java dretvi. Naravno, nije lako odrediti (a ponekad niti moguće) koliki točno treba biti taj broj – najčešće treba eksperimentirati. Uglavnom se pokazuje da se kod početnog povećanja broja podzadataka dobije značajno povećanje performansi, a sa daljnjim povećanjem broja, povećanje performansi je sve manje (performanse se mogu i smanjiti).  Iza slike slijedi program, koji ima ove ulazne parametre: - number: gornja granica do koje se traže prim brojevi; - poolSize: broj Java dretvi; - numberOfParts: broj podzadataka.
  • 27.
    Stranica 27ISTRA TECHJavaCro2015 – Java paralelizacija Usporedba Java 5/6 Executora i Java 7 ForkJoin frameworka  Pronalaženje prim brojeva na μP sa 8 HW dretvi (4 jezgre * 2) - prikaz efekta mijenjanja broja Java dretvi (PoolSize, os x) i broja podzadataka (različite krivulje):
  • 28.
    Stranica 28ISTRA TECHJavaCro2015 – Java paralelizacija Usporedba Java 5/6 Executora i Java 7 ForkJoin frameworka import java.util.concurrent.ExecutorService; ... public class ExecutorPrimeFinder { ... public static void main(final String[] args) { if (args.length < 3) { System.out.println("Usage: number poolSize numberOfParts"); } else { final int number = Integer.parseInt(args[0]); final int poolSize = Integer.parseInt(args[1]); final int numberOfParts = Integer.parseInt(args[2]); ExecutorPrimeFinder task = new ExecutorPrimeFinder(); final long startTime = System.nanoTime(); final long numberOfPrimes = task.countPrimes(number, poolSize, numberOfParts); final long endTime = System.nanoTime(); System.out.printf("Number of primes under %d is %dn", number, numberOfPrimes, numberOfParts); System.out.println("Time (seconds) taken is " + (endTime - startTime) / 1.0e9); }}}
  • 29.
    Stranica 29ISTRA TECHJavaCro2015 – Java paralelizacija Usporedba Java 5/6 Executora i Java 7 ForkJoin frameworka protected int countPrimes (final int number, final int poolSize, final int numberOfParts) { int count = 0; try { final List<Callable<Integer>> partitions = new ArrayList<Callable<Integer>>(); final int chunksPerPartition = number / numberOfParts; for(int i = 0; i < numberOfParts; i++) { final int lower = (i * chunksPerPartition) + 1; final int upper = (i == numberOfParts - 1) ? number : lower + chunksPerPartition - 1; partitions.add(new Callable<Integer>() { public Integer call() { return countPrimesInRange(lower, upper); } }); } ...
  • 30.
    Stranica 30ISTRA TECHJavaCro2015 – Java paralelizacija Usporedba Java 5/6 Executora i Java 7 ForkJoin frameworka protected int countPrimes (final int number, final int poolSize, final int numberOfParts) { ... final ExecutorService executorPool = Executors.newFixedThreadPool(poolSize); final List<Future<Integer>> resultFromParts = executorPool.invokeAll(partitions,10000,TimeUnit.SECONDS); executorPool.shutdown(); for(final Future<Integer> result : resultFromParts) { count += result.get(); } } catch(Exception ex) { throw new RuntimeException(ex); } return count; }
  • 31.
    Stranica 31ISTRA TECHJavaCro2015 – Java paralelizacija Usporedba Java 5/6 Executora i Java 7 ForkJoin frameworka public int countPrimesInRange(final int lower, final int upper) { int total = 0; for(int i = lower; i <= upper; i++) { if (isPrime(i)) total++; } return total; } private boolean isPrime(final int number) { if (number <= 1) return false; if (number == 2) return true; if (number % 2 == 0) return false; for(int i = 3; i <= Math.sqrt(number); i = i + 2) { if (number % i == 0) return false; } return true; }
  • 32.
    Stranica 32ISTRA TECHJavaCro2015 – Java paralelizacija Usporedba Java 5/6 Executora i Java 7 ForkJoin frameworka  Kako je već rečeno, u Javi 7 je uveden ForkJoin framework, koji je, zapravo, specijalna verzija executora.  Mark Reinhold, Chief Architect, Java Platform Group u "Divide and Conquer Parallelism with the Fork/Join Framework" (07.2011): The fork/join framework minimizes per-task overhead for compute-intensive tasks – Not recommended for tasks that mix CPU and I/O activity A portable way to express many parallel algorithms – Code is independent of execution topology – Reasonably efficient for a wide range of core counts – Library-managed parallelism
  • 33.
    Stranica 33ISTRA TECHJavaCro2015 – Java paralelizacija Usporedba Java 5/6 Executora i Java 7 ForkJoin frameworka  Mark Reinhold, Chief Architect, Java Platform Group u "Divide and Conquer Parallelism with the Fork/Join Framework" (07.2011): No silver bullet - Many point solutions: - Divide & conquer (fork/join) - Work queues + thread pools - Bulk data operations (select / map / reduce) - Actors - Software transactional memory (STM) - GPU-based SIMD-style computation
  • 34.
    Stranica 34ISTRA TECHJavaCro2015 – Java paralelizacija Usporedba Java 5/6 Executora i Java 7 ForkJoin frameworka  "Divide and Conquer" ("Divide et impera", "Podijeli pa vladaj" ili "Zavadi pa vladaj" – stara rimska poslovica).  Implementaciju ExecutorService sučelja u slučaju ForkJoin frameworka radi klasa ForkJoinPool.  Tipično se ForkJoinPool instanci šalje samo jedan zadatak (task), a onda ForkJoinPool instanca i zadatak zajedno primjenjuju tehniku Divide and Conquer.  Broj Java dretvi u poolu se može zadati eksplicitno ili implicitno: -- broj Java dretvi se zadaje eksplicitno (u ovom slučaju 8) ForkJoinPool fjPool = new ForkJoinPool(8); -- ili implicitno (na računalu sa 8 HW dretvi, bit će ih 8) -- framework koristi metodu Runtime.availableProcessors() ForkJoinPool fjPool = new ForkJoinPool();
  • 35.
    Stranica 35ISTRA TECHJavaCro2015 – Java paralelizacija Usporedba Java 5/6 Executora i Java 7 ForkJoin frameworka  Zadatak (task) treba biti instanca podklase apstraktne klase ForkJoinTask, preciznije podklasa apstraktne podklase klase ForkJoinTask, a najčešće su to RecursiveTask (u primjeru) ili RecursiveAction.  ForkJoinTask ima puno metoda, ali najvažnije su: compute(), fork(), join()  Pseudo kod za compute: if (podzadatakJeDovoljnoMali()) { rijesiPodzadatak(); } else { MojForkJoinTask lijevaPolovica = ... MojForkJoinTask desnaPolovica = ... lijevaPolovica.fork(); -- stavi u queue desnaPolovica.compute(); -- radi lijevaPolovica.join(); -- čekaj završetak }
  • 36.
    Stranica 36ISTRA TECHJavaCro2015 – Java paralelizacija Usporedba Java 5/6 Executora i Java 7 ForkJoin frameworka  Za razliku od većine ExecutorService implementacija, kod ForkJoin frameworka svaka Java dretva u ForkJoinPool-u ima svoj red (queue) podzadataka na kojima radi.  Metoda fork() stavlja ForkJoinTask u red tekuće Java dretve.  Inicijalno je zauzeta samo jedna Java dretva - kada joj pošaljemo (cijeli) zadatak. Dretva tada počinje dijeliti zadatak u dva podzadatka, pa prvi podzadatak (lijevi) stavi u red, a drugi podzadatak (desni) pokuša izvršiti (i tako rekurzivno):
  • 37.
    Stranica 37ISTRA TECHJavaCro2015 – Java paralelizacija Usporedba Java 5/6 Executora i Java 7 ForkJoin frameworka  Ključna značajka ForkJoin frameworka je Work Stealing (krađa posla). Java dretve kradu posao (podzadatak) drugoj Java dretvi iz njenog reda podzadataka, i stavljaju ga u svoj red (nešto što većina nas ne bi nikad napravila ).  Vrlo je važan redoslijed stavljanja podzadataka u red. Podzadaci koji se prvi stavljaju u red trebaju predstavljati veći dio posla. Npr. na početku imamo jedan zadatak, koji pokriva 100% posla. Njega dijelimo u dva podzadatka, svaki po (otprilike) 50% posla. Prvi stavljamo u red, a drugi obrađujemo, pri čemu drugi opet dijelimo u polovice, itd.  One Java dretve koje nemaju posla (tj. njihov red je prazan), kradu posao drugim Java dretvama, i to tako da uzmu posao (podzadatak) koji je najstariji u redu, a to je istovremeno i najveći posao iz reda druge Java dretve.  Sljedeća slika pokazuje ta događanja, sa 4 Java dretve.
  • 38.
    Stranica 38ISTRA TECHJavaCro2015 – Java paralelizacija Usporedba Java 5/6 Executora i Java 7 ForkJoin frameworka
  • 39.
    Stranica 39ISTRA TECHJavaCro2015 – Java paralelizacija Usporedba Java 5/6 Executora i Java 7 ForkJoin frameworka  Kada bi se zadatak mogao savršeno dijeliti na jednake polovice, ne bi niti bilo potrebe za ForkJoin frameworkom, mogli bismo koristiti i standardne executore.  No, to je u praksi rijetko tako. Npr., u našem slučaju je lakše pretraživati prim brojeve po donjoj polovici raspona brojeva, nego gornjoj (jer gornja polovica sadrži veće brojeve).  Zato je važno podijeliti posao na dovoljno veliki broj podzadataka, tako da (sve do samog kraja) niti jedna Java dretva ne ostane bez posla (tj. treba biti tako da jedna dretva uvijek može ukrasti posao drugoj dretvi).  Dijeljenje na podzadatke se, za razliku od primjera sa običnim executorima, radi implicitno – ne zadaje se broj podzadataka, već se određuje kada je podzadatak dovoljno mali.  Nažalost, ne postoji opća metoda kojom se ispravno određuje veličina tog malog podzadatka – to se radi eksperimentalno.
  • 40.
    Stranica 40ISTRA TECHJavaCro2015 – Java paralelizacija Usporedba Java 5/6 Executora i Java 7 ForkJoin frameworka  Podzadatak na kojem se poziva metoda join() može tada biti već gotov, jer ga je možda ukrala druga Java dretva, i već napravila. Ili može biti ukraden, ali ga druga dretva upravo radi, pa tada treba čekati da završi. Treći je slučaj da podzadatak nije ukraden i tada ga radi tekuća dretva.  Poziv join() metode u compute() metodi treba biti jedna od zadnjih radnji, iza poziva fork() metode i rekurzivnog poziva compute() metode. Budući da je to jako važno, postoji metoda invokeAll(a2, a1); koja zamjenjuje niz a1.fork(); a2.compute(); a1.join();  Kako je već rečeno, zadatak (task) treba biti instanca podklase apstraktnih klasa RecursiveAction ili RecursiveTask.  RecursiveAction se koristi onda kada nije potrebno vraćati rezultat, a RecursiveTask kada je potrebno vraćati rezultat (kao u našem primjeru).
  • 41.
    Stranica 41ISTRA TECHJavaCro2015 – Java paralelizacija Usporedba Java 5/6 Executora i Java 7 ForkJoin frameworka import java.util.concurrent.ForkJoinPool; ... public class ForkJoinPrimeFinderTask extends RecursiveTask<Integer> { private static int threshold; private int start; private int end; public ForkJoinPrimeFinderTask (final int theStart, final int theEnd) { start = theStart; end = theEnd; } public static void main(final String[] args) { if (args.length < 1 || args.length > 3) { System.out.println("Usage: number poolSize threshold OR number poolSize OR number"); return; } ... }
  • 42.
    Stranica 42ISTRA TECHJavaCro2015 – Java paralelizacija Usporedba Java 5/6 Executora i Java 7 ForkJoin frameworka public static void main(final String[] args) { ... final int number = Integer.parseInt(args[0]); ForkJoinPrimeFinderTask task = new ForkJoinPrimeFinderTask(1, number); ForkJoinPool fjPool; if (args.length == 2 || args.length == 3) { fjPool = new ForkJoinPool(Integer.parseInt(args[1])); } else { fjPool = new ForkJoinPool(); } if (args.length == 3) { threshold = Integer.parseInt(args[2]); } else { threshold = 100; final long startTime = System.nanoTime(); final long numberOfPrimes = fjPool.invoke(task); final long endTime = System.nanoTime(); System.out.printf("Number of primes under %d is %dn", number, numberOfPrimes); System.out.println("Time (seconds) taken is " + (endTime - startTime) / 1.0e9); }
  • 43.
    Stranica 43ISTRA TECHJavaCro2015 – Java paralelizacija Usporedba Java 5/6 Executora i Java 7 ForkJoin frameworka protected Integer compute() { if (end - start <= threshold) { return countPrimesInRange(start, end); } else { int halfWay = ((end - start) / 2) + start; ForkJoinPrimeFinderTask t1 = new ForkJoinPrimeFinderTask(start, halfWay); ForkJoinPrimeFinderTask t2 = new ForkJoinPrimeFinderTask(halfWay + 1, end); t1.fork(); int count2 = t2.compute(); int count1 = t1.join(); return count1 + count2; } }
  • 44.
    Stranica 44ISTRA TECHJavaCro2015 – Java paralelizacija Usporedba Java 5/6 Executora i Java 7 ForkJoin frameworka -- ovo je isto kao u primjeru Executora public int countPrimesInRange(final int lower, final int upper) { int total = 0; for(int i = lower; i <= upper; i++) { if (isPrime(i)) total++; } return total; } private boolean isPrime(final int number) { if (number <= 1) return false; if (number == 2) return true; if (number % 2 == 0) return false; for(int i = 3; i <= Math.sqrt(number); i = i + 2) { if (number % i == 0) return false; } return true; }
  • 45.
    Stranica 45ISTRA TECHJavaCro2015 – Java paralelizacija Usporedba Java 5/6 Executora i Java 7 ForkJoin frameworka ExecutorPrimeFinder 10000000 1 10000 Number of primes under 10000000 is 664579 Time (seconds) taken is 12,70 ExecutorPrimeFinder 10000000 2 10000 Time (seconds) taken is 6,60 ExecutorPrimeFinder 10000000 4 10000 Time (seconds) taken is 3,20 ExecutorPrimeFinder 10000000 8 10000 Time (seconds) taken is 3,18 ExecutorPrimeFinder 10000000 16 10000 Time (seconds) taken is 3,17 ForkJoinPrimeFinderTask 10000000 1 1000 Number of primes under 10000000 is 664579 Time (seconds) taken is 14,41 ForkJoinPrimeFinderTask 10000000 2 1000 Time (seconds) taken is 7,36 ForkJoinPrimeFinderTask 10000000 4 1000 Time (seconds) taken is 3,91 ForkJoinPrimeFinderTask 10000000 8 1000 Time (seconds) taken is 3,28 ForkJoinPrimeFinderTask 10000000 16 1000 Time (seconds) taken is 3,25 ForkJoinPrimeFinderTask 10000000 16 100 Time (seconds) taken is 2,93
  • 46.
    Stranica 46ISTRA TECHJavaCro2015 – Java paralelizacija Zaključak  Mooreov zakon vrijedi i dalje. No, radni takt procesora praktički je prestao rasti oko 2005. godine. Umjesto povećanja brzine, proizvođači povećavaju broj CPU-a (jezgri) na jednom mikroprocesorskom čipu.  Dok smo kod jednojezgrenih procesora povećanjem takta procesora dobili linearno povećanje brzine programa, kod višejezgrenih procesora program najčešće moramo pisati drugačije da bismo iskoristili raspoložive jezgre, a razlog za to objašnjava Amdahlov zakon.  Nažalost, konkurentne programe nije lako pisati. Iako dretve rade paralelno, moramo investirati veliki trud da implementiramo tehnike koje ih sprečavaju da na loš način utječu jedna na drugu.
  • 47.
    Stranica 47ISTRA TECHJavaCro2015 – Java paralelizacija Zaključak  Zato je izmišljena transakcijska memorija (TM), koja može biti softverska (STM), hardverska (HTM) ili hibridna. Postoje implementacije STM-a na razini jezika (npr. jezik Clojure) ili na razini biblioteka (npr. jezik Scala).  Što se tiče HTM-a, danas postoje barem tri mikroprocesora koja ju podržavaju - Azul Systems Vega, IBM BlueGene/Q, Intel Haswell.  Postoje i neka softverska rješenja, koja omogućavaju relativno jednostavno i sigurno konkurentno programiranje u Javi (preciznije, paralelno programiranje). Između ostalih, to su executori i ForkJoin framework (koji je specijalna vrsta executora). Nažalost, nisu svi zadaci prikladni za paralelizaciju pomoću tih rješenja.