Differences

This shows you the differences between two versions of the page.

Link to this comparison view

apd:laboratoare:07 [2020/12/04 17:36]
florin.mihalache [Exerciții]
apd:laboratoare:07 [2023/10/08 16:33] (current)
dorinel.filip Move
Line 1: Line 1:
-===== Laboratorul 07 - 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ă ​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 WorkersDe 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 termineSoluț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) recursiă 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()//​. +
- +
- +
-==== Exerciții ==== +
- +
-<note tip> +
-**Hint** +
- +
-În acest laborator, veți lucra cu 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 {{:​apd:​laboratoare:​lab7-skel.zip | scheletului}} oferit (în pachetul //task1//) folosind ExecutorService. **(1p)** +
-  - Paralelizați problema colorării unui graf pe baza scheletului oferit (în pachetul //task2//) folosind ExecutorService. **(2p)** +
-  - 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. **(2p)** +
-  - Paralelizați găsirea căilor între două noduri pe baza scheletului oferit (în pachetul //task4//) folosind ForkJoinPool. **(1p)** +
-  - Paralelizați problema colorării unui graf pe baza scheletului oferit (în pachetul //task5//) folosind ForkJoinPool. **(2p)** +
-  - Paralelizați problema damelor pe baza scheletului oferit (în pachetul //task6//) folosind ForkJoinPool**(2p)**+
apd/laboratoare/07.1607096208.txt.gz · Last modified: 2020/12/04 17:36 by florin.mihalache
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0