KONKURENTNÉ PROGRAMOVANIE 6. cvičenie: Exekútory
java.util.concurrent Konkurentné kolekcie ConcurrentHashMap, ConcurrentSkipListMap, ConcurrentSkipListSet, CopyOnWriteArrayList, CopyOnWriteArraySet Rady, dvojsmerné rady (zásobník a rad v jednom) Implementácie BlockingQueue, BlockingDeque LinkedBlockingQueue, ArrayBlockingQueue, SynchronousQueue, PriorityBlockingQueue, DelayQueue, LinkedTransferQueue, LinkedBlockingDeque Synchronizéry Semaphore, CountDownLatch, CyclicBarrier, Phaser, Exchanger Exekútory
Spúšťanie úloh vo vlákne Úloha implementácia interfejsu Runnable public interface Runnable { void run(); } Vlákno samostatný vykonávateľ jednej úlohy Runnable úloha = new MojaÚloha(); Thread thread = new Thread(úloha); Thread.start(); spustenie úlohy, t.j. metódy run() v samostatnom vlákne
Exekútory Implementácie interfejsu Executor public interface Executor { void execute(runnable command); } Vykonávateľ jednej alebo viac úloh Každá z úloh bude vykonaná v samostatnom vlákne Executor exekútor = new MôjExekútor(); Runnable úloha1 = new MojaÚloha1(); exekútor.execute(úloha1); Runnable úloha2 = new MojaÚloha2(); exekútor.execute(úloha2);
Exekútor je správca vlákien a úloh Každý exekútor má vlastnú politiku správy vlákien a úloh kedy vlákna vyrobiť koľko vlákien vyrobiť čo spraviť, ak nejaké vlákno skončí s výnimkou/chybou v ktorom vlákne bude úloha vykonaná koľko úloh sa môže vykonávať súčasne koľko úloh môže čakať na vykonanie čo sa má urobiť pred alebo po vykonaní úlohy Interne ide vždy o nejakú spravovanú množinu vlákien (thread pool), ktorým sú prideľované úlohy odoslané exekútoru Vlákna vo vnútri sú typicky znovupoužívané na nové úlohy a.k.a. Workery
ExecutorService Priame implementácie interfejsu Executor v Jave nie sú ExecutorService Rozšírenie interfejsu Executor o ďalšie metódy na pohodlnejšiu prácu s úlohami ukončenie vykonávania úloh shutdown(), shutdownnow() zistenie či začal shutdown - isshutdown() zistenie či sa ukončil shutdown - isterminated() čakanie na dobehnutie úloh po shutdowne - awaittermination(timeout, unit) ďalšie metódy na spúštanie úloh submit, invokeall, invokeany
Implementácie Exekútorov Vytvárajú sa cez statické metódy triedy Executors: newfixedthreadpool(int počet) premenná počet určuje maximálny počet vlákien ak niektoré vlákna zomrú nahradia sa novými newcachedthreadpool() každej úlohe hneď pridelí vlákno, ak nemá žiadne v zásobe, vyrobí hneď nové newsinglethreadpool() exekútor s jediným aktívnym vláknom newscheduledthreadpool() dokáže odložiť začiatok behu úlohy, prípadne opakovať úlohu viac krát náhrada triedy Timer od Javy 5
Nekonečný pool vlákien Legenda: Viac vlákien = lepšia priepustnosť Viaceré úlohy robím súčasne, každý je vybavený hneď Nekonečnosť je ale nebezpečná Náročnosť obsluhy prepínania vlákien rastie Každé vlákno má pamäťové nároky Operačný systém aj JVM zvyčajne obmedzuje počet vlákien pre jeden proces Java skončí s chybou projekt kľakne Testovanie zvyčajne bez problémov, v reálnej prevádzke (s veľa používateľmi) môžeme naraziť na limity
Konečný pool vlákien newfixedthreadpool(počet) je často najlepšia voľba, kde počet je blízko počtu jadier procesora int jadier = Runtime.getRuntime().availableProcessors(); Ak je úloh viac ako počet, ďalšie úlohy čakajú v rade, pokiaľ nejaká počítaná úloha neskončí producers consumers prijímanie úloh spracovanie úloh poolom vlákien Úloha môže skončiť korektne vlákno dostane novú úlohu z čakajúcich Úloha môže zhodiť vlákno napr. výnimkou exekútor vyrobí nové vlákno do poolu a pridelí mu novú úlohu z čakajúcich úloh
Dva typy úloh Runnable = úloha bez návratovej hodnoty public interface Runnable { void run(); } Callable(T) = úloha s návratovou hodnotou typu T public interface Callable<T> { T call() throws Exception; } Callable úlohu viem vyrobiť z Runnable, ak treba call() po dobehnutí vracia null Callable<Object> callableúloha = Executors.callable(runnableÚloha);
Posielanie úloh do ExecutorService execute(runnable úloha) O aktuálnom stave vykonávania úlohy neviem nič možné stavy: čakajúca na vykonanie, vykonávaná, skončená úspešne, skončená s výnimkou Zaslanú úlohu neviem ukončiť ak chcem, iba ak ukončím celý exekútor aj s ostatnými úlohami submit(runnable úloha), submit(callable úloha) Metóda vráti budúci výsledok úlohy zabalený v inštancii typu Future cez ktorú: viem zistiť stav úlohy a aj úlohu ukončiť, po skončení úlohy viem získať výsledok úlohy alebo výnimku, ak ju úloha vyhodila
Callable a Future Implementáciou Callable<T> vyrobíme úlohu, ktorej metóda call() vracia typ T public interface Callable<T> { T call() throws Exception; } public class Faktoriál implements Callable<Long> { private long n; public Faktoriál(long n) {this.n = n} } Long call() throws NumberTooBigException { return n * fakt(n-1); }
Callable a Future Pošleme úlohu do exekútora cez metódu submit() Návratová hodnota metódy submit() je typu Future Obalený budúci výsledok metódy call() private ExecutorService exekútor; Pošlem úlohu do exekútora, ktorý ju spustí vo vlákne. Callable úloha = new Faktoriál(x); Future<Long> budúcivýsledok = exekútor.submit(úloha); // tu robím zatiaľ čokoľvek, alebo nič Long výsledok = budúcivýsledok.get(); Blokovaná operácia. Spím, kým sa výsledok nevypočíta v niektorom z vlákien exekútora.
Future<V> Cez objekt typu Future Viem počkať, kým úloha skončí a zobrať výsledok get() Ak úloha skončí s výnimkou alebo chybou, vyhodí ju zabalenú v ExecutionException Viem zistiť, či už úloha skončila isdone() Viem úlohu zrušiť cancel(boolean prerušiťvlákno) Ak sa úloha ešte nezačala vykonávať, tak sa ani nevykoná Ak sa začala vykonávať a prerušiťvlákno je false, úloha sa nechá dobehnúť Ak sa začala vykonávať a prerušiťvlákno je true, pokúsi sa prerušiť beh vlákna, v ktorom úloha beží Viem zistiť, či úlohu niekto zrušil iscancelled()
Zadanie 1 Stiahnite si z GitHubu poslednú verziu: https://github.com/petergursky/kopr2016 Nasledovný balíček: sk.ics.upjs.kopr2016.cviko06.zadanie Je to program, ktorý sčíta veľkosti súborov v podstromoch podadresárov daného adresára Analýzu každého podstromu vykonajte ako samostatnú Callable úlohu cez exekútor
Hromadné posielanie úloh do ExecutorService List<Future<T>> invokeall(collection<callable<t>> úlohy) Pošlem do exekútora kolekciu úloh typu Callable<T> Exekútor ich postupne pospúšťa v samostatných vláknach Táto metóda blokuje volajúce vlákno, pokiaľ všetky úlohy v kolekcii neskončia T invokeany(collection<callable<t>> úlohy) Táto metóda blokuje volajúce vlákno, pokiaľ niektorá úloha neskončí úspešne (bez vyhodenia výnimky) Vráti výsledok tejto úlohy Ostatné úlohy zruší
CompletionService Umožňuje niečo medzi invokeall() a invokeany() Chcem všetky riešenia úloh, ale nechcem čakať kým všetky skončia Vyhodnocujem riešenie hneď, ako niektorá úloha skončí a počkám na výsledok ďalšej úlohy
CompletionService Návratový typ Callable úloh ExecutorService exekútor = Executors.newFixedThreadPool(4); CompletionService<Long> completionservice = new ExecutorCompletionService<Long>(exekútor); for (int i = 0; i < 50; i++) completionservice.submit(new Faktorial(i)); // úlohy sa postupne vykonávajú Naposielam 50 Callable úloh for (int i = 0; i < 50; i++) { Future<Long> buducifaktorial = completionservice.take(); Long faktorial = buducifaktorial.get(); System.out.println( jeden z faktoriálov je + faktorial); } Blokovane čakám na ľubovoľné vlákno kým neskončí
Zadania 2, 3 a 4 Modifikujte riešenie zadania 1 tak, že 2. Využite metódu invokeall() 3. Využite CompletionService 4. Navrhnite riešenie cez exekútor, pri ktorom sa využijú všetky jadrá procesora rovnomerne bez ohľadu na hĺbku adresárov Vieme tieto úlohy spraviť iba s toľkými vláknami, ako je jadier? Porovnajte tieto prístupy aj s riešením zadania 1
Exekútor ForkJoinPool (od Javy 7) Špeciálny exekútor pre rekurzívne úlohy Využíva návrhový vzor work stealing pre daný počet vlákien Akceptuje špeciálne typy úloh (potomkovia ForkJoinTask) RecursiveTask<T> - úloha s návratovou hodnotou RecursiveAction úloha bez návratovej hodnoty Rekurzívna úloha vyrába nové rekurzívne úlohy toho istého typu a posiela ich exekútoru Úloha typicky čaká na dobehnutie úloh, čo zavolala, aby zosumarizovala výsledky a mohla tiež skončiť Počas čakania je jej odňaté vlákno pre iné úlohy, ktoré majú čo robiť
Exekútor ForkJoinPool Vytváranie cez konštruktor (defaultne toľko vlákien ako jadier) ForkJoinPool forkjoinpool = new ForkJoinPool(); Alternatívne, ak chceme iba jeden pool, použijeme zdieľaný: ForkJoinPool forkjoinpool = ForkJoinPool.commonPool(); Zaslanie úlohy do exekútora Bez čakania na výsledok execute(úloha) S čakaním na ukončenie úlohy invoke(úloha) Zaslanie úlohy s neskorším počkaním na ukončenie úlohy submit(úloha) Neskoršie počkanie na dokončenie úlohy po volaní submit(úloha) T výsledok = úloha.join(), ak úloha je RecursiveTask<T> úloha.join(), ak úloha je RecursiveAction
RecursiveTask pre ForkJoinPool Prekrývame metódu compute() public class MyTask extends RecursiveTask<MyResult> { public MyResult compute() { //výpočet úlohy MyTask podúloha1 = new MyTask(...); podúloha1.fork(); // odošlem podúlohu exekútoru MyTask podúloha2 = new MyTask(...); podúloha2.fork(); // odošlem podúlohu exekútoru MyResult result1 = podúloha1.join(); // čakám na výsledok podúlohy1 // vlákno mi je odňaté, kým nepríde výsledok MyResult result2 = podúloha2.join(); // čakám na výsledok podúlohy2 // vlákno mi je odňaté, kým nepríde výsledok return createresult(result1,result2); }
RecursiveTask pre ForkJoinPool Dedí od abstraktnej ForkJoinTask Implementuje Future, takže s ním vieme administrovať aj monitorovať úlohu Vieme ho vyrobiť aj z Callable a Runnable úloh ForkJoinTask fjtúloha = ForkJoinTask.adapt(úloha) Vieme spustiť veľa úloh a čakať na nich cez invokeall()... kopa ďalších vychytávok
Zasielanie úloh do ForkJoinPool-u Bez čakania na výsledok S čakaním na výsledok S neskorším čakaním na výsledok pomocou forkjointask.join() Z extrerného kódu Z úlohy vo vnútri execute(forkjointask) forkjointask.fork() invoke(forkjointask) forkjointask.invoke() submit(forkjointask) forkjointask.fork()
Zadanie 5 Modifikujte riešenie predošlých zadaní tak, že využijete exekútor ForkJoinPool a úlohu typu RecursiveTask