This shows you the differences between two versions of the page.
apd:laboratoare:07 [2021/11/18 22:45] florin.mihalache [Future și CompletableFuture] |
apd:laboratoare:07 [2023/10/08 16:33] (current) dorinel.filip Move |
||
---|---|---|---|
Line 1: | Line 1: | ||
===== Laboratorul 7 - Modelul Replicated Workers ===== | ===== Laboratorul 7 - Modelul Replicated Workers ===== | ||
- | Responsabili: Radu Ciobanu, Carina Deaconu, Florin Mihalache | + | Documentația de laborator s-a mutat la [[https://mobylab.docs.crescdi.pub.ro/docs/parallelAndDistributed/introduction|această adresă]]. |
- | + | ||
- | În programarea paralelă, modelul **Replicated Workers** (sau **Thread Pool**) este un design pattern folosit pentru obținerea de concurență în execuția unui program. În acest model, există două componente principale: un **pool** de sarcini de executat (task-uri), reprezentat în general ca o coadă, și un grup de **workeri** (care sunt în general thread-uri). Fiecare worker din modelul replicated workers are următorul comportament: | + | |
- | + | ||
- | <code> | + | |
- | while (1) | + | |
- | ia un task din pool | + | |
- | execută task-ul | + | |
- | adaugă zero sau mai multe task-uri rezultate în pool | + | |
- | </code> | + | |
- | + | ||
- | Așa cum se poate observa în pseudocodul de mai sus, un worker ia un task din pool și îl execută. Ca urmare a execuției unui task, este posibil să se genereze task-uri adiționale, pe care worker-ul le adaugă în pool, după care repetă pașii precedenți. | + | |
- | + | ||
- | Modelul Replicated Workers este util atunci când: | + | |
- | * nu se cunoaște în avans dimensiunea problemei, adică numărul de pași care se vor executa | + | |
- | * există multe task-uri de dimensiuni mici care trebuie executate și este mai eficient să se refolosească aceleași thread-uri decât să se creeze unele noi la fiecare pas | + | |
- | * există posibilitatea ca thread-urile să fie dezechilibrate (de exemplu, unele core-uri sunt mai ocupate, sau chiar diferite între ele, și o împărțire egală a sarcinilor de calcul ar duce la întârzieri în program). | + | |
- | + | ||
- | O reprezentare grafică a modelului replicated workers poate fi observată în figura de mai jos. | + | |
- | + | ||
- | {{:apd:laboratoare:replicatedworkers.png?direct&600|}} | + | |
- | + | ||
- | ==== ExecutorService ==== | + | |
- | + | ||
- | [[https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ExecutorService.html|ExecutorService]] este o interfață în Java ce permite executarea de task-uri asincrone, în background, în mod concurent, pe baza modelului Replicated Workers. De exemplu, putem avea nevoie să trimitem un număr de cereri, dar ar fi ineficient să le trimitem pe rând, secvențial, așteptând după fiecare să se termine. Soluția ar fi să lucrăm asincron, adică să trimitem o cerere, să nu așteptăm „după ea” (deci să se trimită în background) și să folosim thread-uri pentru a împărți numărul de cereri și a trimite mai multe deodată (concurent). Acesta ar fi cazul când nu știm în avans dimensiunea problemei pe care o rezolvăm, și fie avem nevoie de toate soluțiile problemei (pentru că trebuie să trimitem toate cererile), fie nu vrem să găsim toate soluțiile, ci minim una. | + | |
- | + | ||
- | În continuare, se prezintă un exemplu de program care folosește ExecutorService pentru a afișa toate fișierele dintr-un director (inclusiv din fiecare subdirector în parte). Pentru că nu știm în avans câte fișiere și directoare va trebui să analizăm, putem spune că nu cunoaștem dimensiunea problemei. | + | |
- | + | ||
- | <code java> | + | |
- | import java.io.File; | + | |
- | import java.util.concurrent.ExecutorService; | + | |
- | import java.util.concurrent.Executors; | + | |
- | import java.util.concurrent.atomic.AtomicInteger; | + | |
- | + | ||
- | public class Example1 { | + | |
- | public static void main(String[] args) { | + | |
- | AtomicInteger inQueue = new AtomicInteger(0); | + | |
- | ExecutorService tpe = Executors.newFixedThreadPool(4); | + | |
- | + | ||
- | inQueue.incrementAndGet(); | + | |
- | tpe.submit(new MyRunnable("files", tpe, inQueue)); | + | |
- | } | + | |
- | } | + | |
- | + | ||
- | class MyRunnable implements Runnable { | + | |
- | String path; | + | |
- | ExecutorService tpe; | + | |
- | AtomicInteger inQueue; | + | |
- | + | ||
- | public MyRunnable(String path, ExecutorService tpe, AtomicInteger inQueue) { | + | |
- | this.path = path; | + | |
- | this.tpe = tpe; | + | |
- | this.inQueue = inQueue; | + | |
- | } | + | |
- | + | ||
- | @Override | + | |
- | public void run() { | + | |
- | File file = new File(path); | + | |
- | if (file.isFile()) { | + | |
- | System.out.println(file.getPath()); | + | |
- | } else if (file.isDirectory()) { | + | |
- | File[] files = file.listFiles(); | + | |
- | + | ||
- | if (files != null) { | + | |
- | for (File f : files) { | + | |
- | inQueue.incrementAndGet(); | + | |
- | tpe.submit(new MyRunnable(f.getPath(), tpe, inQueue)); | + | |
- | } | + | |
- | } | + | |
- | } | + | |
- | + | ||
- | int left = inQueue.decrementAndGet(); | + | |
- | if (left == 0) { | + | |
- | tpe.shutdown(); | + | |
- | } | + | |
- | } | + | |
- | } | + | |
- | </code> | + | |
- | + | ||
- | Așa cum se poate observa în codul de mai sus, instanțierea unui ExecutorService cu 4 workeri se realizează în thread-ul principal. Prin metoda //submit()//, se introduce un task nou în pool, corespunzător directorului părinte. Metoda //submit()// poate primi un obiect ce implementează interfața Runnable și conține metoda //run()// sau unul care implementează interfața Callable și conține metoda //call()//. Diferența dintre ele este că //run()// returnează void, iar //call()// returnează un obiect și poate arunca și o excepție. Cu alte cuvinte, Callable este potrivit când ne interesează rezultatul final și eventualele erori la executarea task-ului. În cadrul laboratorului, vom lucra cu interfața Runnable. | + | |
- | + | ||
- | În exemplul de mai sus, clasa noastră ce reprezintă un task se numește MyRunnable. În metoda run, se verifică dacă avem de-a face cu un fișier sau cu un director. Dacă lucrăm pe un fișier, doar se printează numele acestuia. Dacă lucrăm cu un director, înseamnă că trebuie create task-uri noi, pentru fiecare fișier sau subdirector în parte conținut de directorul părinte. Astfel, se creează câte o instanță de MyRunnable pentru fiecare din ele și se adaugă în pool-ul de ExecutorService. | + | |
- | + | ||
- | În cazul exemplului prezentat în acest laborator, ne trebuie toate soluțille problemei (toate fișierele și subdirectoarele), dar nu știm dinainte dimensiunea problemei, așa că o metodă prin care ne putem opri este să ținem evidența task-urilor din pool și să finalizăm execuția atunci când acesta se golește. Altfel, programul nostru va rula la infinit. Modul prin care se finalizează execuția unui ExecutorService poate fi observat în codul de mai sus. Mai precis, se folosește metoda //shutdown()// pentru a opri ExecutorService-ul din a primi task-uri noi. Dacă ne-ar fi interesat o singură soluție a problemei noastre (și nu toate, ca în exemplul de mai sus), am fi putut opri execuția în momentul în care ajungeam la acea soluție. | + | |
- | + | ||
- | ==== ForkJoinPool ==== | + | |
- | + | ||
- | O altă metodă de a implementa modelul Replicated Workers în Java este framework-ul Fork/Join, care folosește o abordare //divide et impera//, ceea ce înseamnă că întâi are locul procesul de împărțire (Fork) recursivă a task-ului inițial în subtask-uri independente mai mici, până când acestea sunt suficient de mici cât să poată fi executate asincron. După aceea, urmează partea de colectare recursivă a rezultatelor (Join) într-un singur rezultat (în cazul unui task care nu returnează un rezultat propriu-zis, pur și simplu se așteaptă ca toate subtask-urile să se termine de executat). | + | |
- | + | ||
- | Pentru o execuție paralelă eficientă, framework-ul Fork/Join folosește un pool de thread-uri numit [[https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ForkJoinPool.html|ForkJoinPool]], care gestionează thread-uri worker de tipul ForkJoinWorkerThread. ForkJoinPool este o implementare de ExecutorService care gestionează thread-uri de tip worker, care pot executa câte un singur task la un moment dat de timp. În implementarea sa, fiecare thread are propria sa coadă de task-uri, dar, atunci când aceasta se golește, worker-ul poate să „fure” task-uri din coada altui worker sau din pool-ul global de task-uri. | + | |
- | + | ||
- | Un task care va fi executat folosind framework-ul Fork/Join poate fi definit în două moduri, în funcție de valoarea de retur. Dacă task-ul nu trebuie să returneze nimic, un task se definește prin moștenirea clasei RecursiveAction. Dacă task-ul returnează o valoare de tip //V//, atunci trebuie să se moștenească clasa RecursiveTask<V> pentru definirea unui task. Ambele clase părinte au o metodă numită //compute()// în care se definește logica unui task (echivalentul metodei //run()// de la ExecutorService). | + | |
- | + | ||
- | Pentru a adăuga task-uri care trebuie executate de workeri, se poate folosi metoda //invoke()//, care creează un task și îi așteaptă rezultatul, sau combinația de metode //fork()// și //join()//. Pentru al doilea caz, metoda //fork()// trimite un task în pool, dar nu îl marchează spre execuție, acest lucru făcându-se explicit prin intermediul metodei //join()//. Atât //invoke()//, cât și //join()//, returnează o valoare de tipul //V// pentru un task de tip RecursiveTask. | + | |
- | + | ||
- | Pentru că, în cazul unui task care nu returnează nimic, se poate folosi RecursiveTask<V>, prezentăm mai jos un exemplu complet de implementare care folosește RecursiveTask, pentru aceeași problemă ca în exemplul de ExecutorService. | + | |
- | + | ||
- | <code java> | + | |
- | import java.io.File; | + | |
- | import java.util.ArrayList; | + | |
- | import java.util.List; | + | |
- | import java.util.concurrent.ForkJoinPool; | + | |
- | import java.util.concurrent.RecursiveTask; | + | |
- | + | ||
- | public class Example2 { | + | |
- | public static void main(String[] args) { | + | |
- | ForkJoinPool fjp = new ForkJoinPool(4); | + | |
- | fjp.invoke(new MyTask("files")); | + | |
- | fjp.shutdown(); | + | |
- | } | + | |
- | } | + | |
- | + | ||
- | class MyTask extends RecursiveTask<Void> { | + | |
- | private final String path; | + | |
- | + | ||
- | public MyTask(String path) { | + | |
- | this.path = path; | + | |
- | } | + | |
- | + | ||
- | @Override | + | |
- | protected Void compute() { | + | |
- | File file = new File(path); | + | |
- | if (file.isFile()) { | + | |
- | System.out.println(file.getPath()); | + | |
- | return null; | + | |
- | } else if (file.isDirectory()) { | + | |
- | File[] files = file.listFiles(); | + | |
- | List<MyTask> tasks = new ArrayList<>(); | + | |
- | if (files != null) { | + | |
- | for (File f : files) { | + | |
- | MyTask t = new MyTask(f.getPath()); | + | |
- | tasks.add(t); | + | |
- | t.fork(); | + | |
- | } | + | |
- | } | + | |
- | for (MyTask task : tasks) { | + | |
- | task.join(); | + | |
- | } | + | |
- | } | + | |
- | + | ||
- | return null; | + | |
- | } | + | |
- | } | + | |
- | </code> | + | |
- | + | ||
- | În exemplul de mai sus, se pot observa ambele moduri de a crea și submite spre execuție un task nou. În //main()//, se folosește //invoke()//, care se va bloca până când toate task-urile s-au terminat, deci până este sigur să apelăm //shutdown()//. În schimb, în interiorul unui task, se adaugă toate task-urile folosind //fork()//, și apoi se așteaptă finalizarea lor folosind //join()//. | + | |
- | + | ||
- | ==== Future și CompletableFuture ==== | + | |
- | === Future === | + | |
- | ''Future<V>'' reprezintă o interfață în Java, cu ajutorul căreia putem crea task-uri de tip promisiuni, care să fie executate în mod asincron, ele oferind niște rezultate. După ce execuția task-urilor este gata, rezultatele task-urilor pot fi prelucrate. | + | |
- | + | ||
- | Folosind Future și CompletableFuture (despre care vom vorbi mai jos), putem executa operații intensiv computaționale sau care au o durată destul de mare. Exemple: descărcare de fișiere mari, manipulare de structuri mari de date, servicii web, etc. | + | |
- | + | ||
- | Interfața Future conține următoarele metode: | + | |
- | * ''get()'' - se așteaptă terminarea unui task, apoi se obține rezultatul execuției task-ului respectiv | + | |
- | * ''isDone()'' - se verifică dacă s-a terminat execuția task-ului | + | |
- | * ''cancel()'' - se întrerupe execuția unui task | + | |
- | * ''isCancelled()'' - se verifică dacă execuția unui task a fost întreruptă | + | |
- | + | ||
- | Putem folosi Future împreună cu ExecutorService, care se ocupă de execuția task-urilor, folosind metoda submit, care primește ca parametru un obiect de tip Callable, care reprezintă task-ul care va fi executat de către ExecutorService și al cărui rezultat va fi încapsulat într-un obiect de tip Future, mai precis într-un obiect de tip FutureTask, care reprezintă o clasă ce implementează interfața Future. | + | |
- | + | ||
- | Exemplu: | + | |
- | <code java> | + | |
- | public class DoubleCalculator { | + | |
- | private final ExecutorService executorService = Executors.newCachedThreadPool(); | + | |
- | + | ||
- | public Future<Integer> calculate(int input) { | + | |
- | return executorService.submit(() -> { | + | |
- | Thread.sleep(1000); | + | |
- | return 2 * input; | + | |
- | }); | + | |
- | } | + | |
- | + | ||
- | public void shutdown() { | + | |
- | executorService.shutdown(); | + | |
- | } | + | |
- | } | + | |
- | + | ||
- | public class Main { | + | |
- | public static void main(String[] args) throws ExecutionException, InterruptedException { | + | |
- | DoubleCalculator doubleCalculator = new DoubleCalculator(); | + | |
- | Future<Integer> future = doubleCalculator.calculate(10); | + | |
- | + | ||
- | while(!future.isDone()) { | + | |
- | System.out.println("Calculating..."); | + | |
- | Thread.sleep(300); | + | |
- | } | + | |
- | + | ||
- | Integer result = future.get(); | + | |
- | System.out.println(result); | + | |
- | + | ||
- | doubleCalculator.shutdown(); | + | |
- | } | + | |
- | } | + | |
- | </code> | + | |
- | + | ||
- | De asemenea, putem folosi ForkJoinPool în lucrul cu Future, având în vedere faptul că ''RecursiveTask<V>'' implementează interfața Future, cu ajutorul căreia putem prelucra rezultatele task-urilor. | + | |
- | + | ||
- | Exemplu: | + | |
- | <code java> | + | |
- | public class FibonacciCalculator extends RecursiveTask<Integer> { | + | |
- | private final int n; | + | |
- | + | ||
- | public FibonacciCalculator(int n) { | + | |
- | this.n = n; | + | |
- | } | + | |
- | + | ||
- | @Override | + | |
- | protected Integer compute() { | + | |
- | if (n == 1) { | + | |
- | return 0; | + | |
- | } | + | |
- | + | ||
- | if (n == 2) { | + | |
- | return 1; | + | |
- | } | + | |
- | + | ||
- | FibonacciCalculator first = new FibonacciCalculator(n - 1); | + | |
- | FibonacciCalculator second = new FibonacciCalculator(n - 2); | + | |
- | + | ||
- | first.fork(); | + | |
- | second.fork(); | + | |
- | + | ||
- | return first.join() + second.join(); | + | |
- | } | + | |
- | } | + | |
- | + | ||
- | public class Main { | + | |
- | public static void main(String[] args) throws ExecutionException, InterruptedException { | + | |
- | ForkJoinPool forkJoinPool = new ForkJoinPool(); | + | |
- | FibonacciCalculator calculator = new FibonacciCalculator(10); | + | |
- | forkJoinPool.execute(calculator); | + | |
- | System.out.println(calculator.get()); | + | |
- | } | + | |
- | } | + | |
- | </code> | + | |
- | === CompletableFuture === | + | |
- | ''CompletableFuture<V>'' reprezintă o clasă care implementează interfața Future. Ce aduce această clasă în plus este o funcționalitate pentru terminarea (completarea) unui task de tip Future, prin metoda ''complete(V value)'', prin care se pune valoarea creată în cadrul unui obiect de tip CompletableFuture, valoare care va putea fi accesată, după terminarea execuției task-ului, folosind metoda ''get()''. | + | |
- | + | ||
- | În mod similar ca la Future, putem folosi ExecutorService împreună cu CompletableFuture. | + | |
- | + | ||
- | Exemplu: | + | |
- | <code java> | + | |
- | public class DoubleCalculator { | + | |
- | private final ExecutorService executorService = Executors.newCachedThreadPool(); | + | |
- | + | ||
- | public Future<Integer> calculate(int input) { | + | |
- | CompletableFuture<Integer> task = new CompletableFuture<>(); | + | |
- | executorService.submit(() -> { | + | |
- | Thread.sleep(1000); | + | |
- | task.complete(2 * input); | + | |
- | return null; | + | |
- | }); | + | |
- | + | ||
- | return task; | + | |
- | } | + | |
- | + | ||
- | public void shutdown() { | + | |
- | executorService.shutdown(); | + | |
- | } | + | |
- | } | + | |
- | + | ||
- | public class Main { | + | |
- | public static void main(String[] args) throws InterruptedException, ExecutionException { | + | |
- | DoubleCalculator doubleCalculator = new DoubleCalculator(); | + | |
- | Future<Integer> future = doubleCalculator.calculate(10); | + | |
- | + | ||
- | Integer result = future.get(); | + | |
- | System.out.println(result); | + | |
- | + | ||
- | doubleCalculator.shutdown(); | + | |
- | } | + | |
- | } | + | |
- | </code> | + | |
- | + | ||
- | Putem să folosim CompletableFuture într-un mod mai elegant, fără să mai fie nevoie să folosim în mod explicit ExecutorService. În acest caz, putem folosi metodele ''runAsync'' și ''supplyAsync''. Diferențele dintre aceste două metode sunt următoarele: | + | |
- | * ''runAsync'' primește ca parametru un Runnable, iar ''supplyAsync'' primește ca parametru un ''Supplier<T>'', care reprezintă o funcție care nu primește niciun parametru și întoarce un rezultat de tip T | + | |
- | * ''runAsync'' întoarce un ''CompletableFuture<Void>'', ceea ce înseamnă că task-ul nu va întoarce un rezultat, iar ''supplyAsync'' întoarce un ''CompletableFuture<T>'', în acest caz putând accesa rezultatul oferit în urma execuției task-ului | + | |
- | + | ||
- | Exemple: | + | |
- | <code java> | + | |
- | int n = 10; | + | |
- | CompletableFuture<Integer> calculatorTask = CompletableFuture.supplyAsync(() -> n * 2); | + | |
- | System.out.println(calculatorTask.get()); | + | |
- | + | ||
- | CompletableFuture<Void> printTask = CompletableFuture.runAsync(() -> System.out.println("Running...")); | + | |
- | printTask.get(); | + | |
- | </code> | + | |
- | + | ||
- | Un avantaj pe care îl prezintă CompletableFuture este faptul că putem să aplicăm operații pentru prelucrarea rezultatului unui task, cu scopul să obținem un Future cu un rezultat final sau cu o acțiune finală, și să facem execuții înlănțuite de task-uri. | + | |
- | + | ||
- | Când dorim să aplicăm operații pentru prelucrarea rezultatului unui task, putem folosi următoarele metode: | + | |
- | * ''thenApply'' - aplică o funcție de tip Function (funcție care întoarce un rezultat) pe un ''CompletableFuture<V>'' și întoarce un CompletableFuture<T>, care conține un rezultat | + | |
- | * ''thenAccept'' - aplică o funcție de tip Consumer (funcție de tip void), și întoarce un ''CompletableFuture<Void>'' | + | |
- | + | ||
- | Dacă nu dorim să prelucrăm rezultatul unui task (posibil să nu ne intereseze), ci doar să apelăm funcții în lanț, în acest caz putem folosi ''thenRun'', care aplică o funcție de tip Consumer și care întoarce un ''CompletableFuture<Void>''. | + | |
- | + | ||
- | Exemple de ''thenApply'', ''thenAccept'' și ''thenRun'': | + | |
- | <code java> | + | |
- | CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> "Hello"); | + | |
- | CompletableFuture<String> f1 = cf1.thenApply(s -> s + " World"); | + | |
- | System.out.println(f1.get()); | + | |
- | + | ||
- | CompletableFuture<String> cf2 = CompletableFuture.supplyAsync(() -> "Hello"); | + | |
- | CompletableFuture<Void> f2 = cf2.thenAccept(s -> System.out.println("Computation returned: " + s)); | + | |
- | f2.get(); | + | |
- | + | ||
- | CompletableFuture<String> cf3 = CompletableFuture.supplyAsync(() -> "Hello"); | + | |
- | CompletableFuture<Void> f3 = cf3.thenRun(() -> System.out.println("Computation finished.")); | + | |
- | f3.get(); | + | |
- | </code> | + | |
- | + | ||
- | Dacă dorim să executăm mai multe CompletableFuture în lanț, în acest caz putem folosi metoda ''thenCompose'', care execută o funcție de tip Function și întoarce un ''CompletableFuture<T>''. | + | |
- | + | ||
- | Exemplu: | + | |
- | <code java> | + | |
- | CompletableFuture<String> cf4 = CompletableFuture.supplyAsync(() -> "Hello") | + | |
- | .thenCompose(s -> CompletableFuture.supplyAsync(() -> s + " World")) | + | |
- | .thenCompose(s -> CompletableFuture.supplyAsync(() -> s + "!")); | + | |
- | System.out.println(cf4.get()); | + | |
- | </code> | + | |
- | + | ||
- | Putem folosi CompletableFuture împreună cu ExecutorService, de exemplu pentru căutarea unui fișier într-un director, care conține o ierarhie de fișiere și directoare: | + | |
- | <code java> | + | |
- | class Main { | + | |
- | public static void main(String[] args) throws ExecutionException, InterruptedException { | + | |
- | ExecutorService tpe = Executors.newFixedThreadPool(4); | + | |
- | CompletableFuture<String> completableFuture = new CompletableFuture<>(); | + | |
- | tpe.submit(new MyRunnable(tpe, "files", "somefile.txt", completableFuture)); | + | |
- | + | ||
- | System.out.println(completableFuture.get()); | + | |
- | } | + | |
- | } | + | |
- | + | ||
- | public class MyRunnable implements Runnable { | + | |
- | private final ExecutorService tpe; | + | |
- | private final String path; | + | |
- | private final String filename; | + | |
- | private final CompletableFuture<String> completableFuture; | + | |
- | + | ||
- | public MyRunnable( | + | |
- | ExecutorService tpe, | + | |
- | String path, | + | |
- | String filename, | + | |
- | CompletableFuture<String> completableFuture | + | |
- | ) { | + | |
- | this.tpe = tpe; | + | |
- | this.path = path; | + | |
- | this.filename = filename; | + | |
- | this.completableFuture = completableFuture; | + | |
- | } | + | |
- | + | ||
- | @Override | + | |
- | public void run() { | + | |
- | File file = new File(path); | + | |
- | if (file.isFile()) { | + | |
- | if (file.getName().equals(filename)) { | + | |
- | completableFuture.complete(file.getAbsolutePath()); | + | |
- | tpe.shutdown(); | + | |
- | } | + | |
- | } else if (file.isDirectory()) { | + | |
- | var files = file.listFiles(); | + | |
- | if (files != null) { | + | |
- | for (var f : files) { | + | |
- | Runnable t = new MyRunnable(tpe, f.getPath(), filename, completableFuture); | + | |
- | tpe.submit(t); | + | |
- | } | + | |
- | } | + | |
- | } | + | |
- | } | + | |
- | } | + | |
- | </code> | + | |
- | ==== Exerciții ==== | + | |
- | + | ||
- | <note tip> | + | |
- | **Hint** | + | |
- | + | ||
- | În acest laborator, veți lucra cu {{:apd:laboratoare:lab7-skel.zip|această arhivă}}, pe care o veți utiliza să paralelizați trei probleme pe baza modelului Replicated Workers, folosind ExecutorService și ForkJoinPool: | + | |
- | * găsirea căilor dintre nodurile unui graf | + | |
- | * colorarea unui graf -> găsiți detalii adiționale {{:apd:laboratoare:graph_coloring.pdf|aici}} | + | |
- | * problema damelor -> găsiți detalii adiționale {{:apd:laboratoare:queens_problem.pdf|aici}}. | + | |
- | </note> | + | |
- | - Paralelizați găsirea căilor între două noduri pe baza scheletului oferit (în pachetul //task1//) folosind ExecutorService. | + | |
- | - Paralelizați problema colorării unui graf pe baza scheletului oferit (în pachetul //task2//) folosind ExecutorService. | + | |
- | - Paralelizați problema damelor pe baza scheletului oferit (în pachetul //task3//, cu soluțiile //[(2, 1), (4, 2), (1, 3), (3, 4)]// și //[(3, 1), (1, 2), (4, 3), (2, 4)]//) folosind ExecutorService. | + | |
- | - Paralelizați găsirea căilor între două noduri pe baza scheletului oferit (în pachetul //task4//) folosind ForkJoinPool. | + | |
- | - Paralelizați problema colorării unui graf pe baza scheletului oferit (în pachetul //task5//) folosind ForkJoinPool. | + | |
- | - Paralelizați problema damelor pe baza scheletului oferit (în pachetul //task6//) folosind ForkJoinPool. | + |