<div class="page"> <div class="cover text-center"> <img class="mx-auto" src=/itb/images/logo_mislata.png alt="logo"> # Sincronització de fils <div class="text-end fit-content ms-auto my-3 mt-auto pt-3"> <p><strong>Autor:</strong> Joan Puigcerver Ibáñez</p> <p><strong>Correu electrònic:</strong> j.puigcerveribanez@edu.gva.es</p> <p><strong>Curs:</strong> 2024/2025</p> </div> <div> <p class="fw-bold mb-0">Llicència: BY-NC-SA</p> <p class="d-none d-md-block">(Reconeixement - No Comercial - Compartir Igual)</p> <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/deed.ca" target="_blank"> <img class="mx-auto" src="/itb/images/license.png" alt="Licence"/> </a> </div><!--license--> </div><!--cover--> </div><!--page--> {:toc} ## Introducció Els fils es poden comunicar intercanviant informació a través dels objectes emmagatzemats a la memòria principal. Com que els fils pertanyen a un procés, poden accedir tots als objectes del mateix que hi haja en memòria. Aquest mètode de comunicació és molt eficient. Compartir la memòria pels fils pot produir resultats erronis si els fils hi accedeixen alhora. Per solucionar aquests problemes s'utilitza necessitem establir mecanismes per sincronitzar i controlar l'accés als recursos. ## Problemes derivats de la concurrència ### Condició de carrera __Condició de carrera__: es defineix així quan el resultat de l'execució d'un programa depèn de lordre concret en què es realitzen els accessos a memòria. _Exemple_: Suposem que tenim dos fils que realitzen una operació sobre la mateixa variable `comptador`, que inicialment té el valor `10`. - __Fil1__: `comptador++`; - __Fil2__: `comptador--`; En una execucó correcta, primer el __Fil1__ sumaria i despés el __Fil2__ restaria o viceversa, però el resultat sempre seria el mateix. Una __operació atòmica__ és aquella que en codi màquina només es tradueix en una única instrucció. El problema és que __++__ i __--__ no són operacions atòmiques i es tradueixen en codi màquina de la manera següent: <div class="row"> <div class="col"> - __`i++`__: ``` registreX = comptador; registreX = registreX + 1; comptador = registreX; ``` </div> <div class="col"> - __`i--`__: ``` registreX = comptador; registreX = registreX - 1; comptador = registreX; ``` </div> </div> Es podria donar el cas on aquestes instruccions s'executen en el següent ordre: ``` Fil1: registre1 = comptador (registre1 = 10) Fil1: registre1 = registre1 + 1 (registre1 = 11) Fil2: registre2 = comptador (registre2 = 10) Fil2: registre2 = registre2 - 1 (registre2 = 9) Fil1: comptador = registre1 (comptador = 11) Fil2: comptador = registre2 (comptador = 9) ``` Com es pot veure, el resulta és erroni. ### Inconsistència de memòria La __inconsistència de memòria__ es produeix quan diferents fils tenen una visió diferent del que hauria de ser la mateixa dada. Pot passar des del no alliberament de dades obsoletes fins al desbordament d'un buffer («buffer overflow»). _Exemple_: Suposem que tenim dos fils i la variable `comptador` que inicialment té el valor 0. - __Fil1__: `comptador++`; - __Fil2__: `System.out.print(comptador)`; Si les dues sentències s'executaren al mateix fil, primer la sumaria i després s'imprimiria amb el valor 1. Si aquestes instruccions s'executen en fils diferents no hi ha garantia que primer es sume i que després s'imprimisca, de manera que el valor imprès podria ser 0, cosa que és un resultat no desitjat. ### Inanició (starvation) La __inanició__ és el fenomen que passa quan a un procés o fil se li denega l'accés a un recurs compartit pel fet que altres processos o fils sempre prenen en control d'aquest recurs abans per diferents motius. Això es pot produir si un fil té una prioritat inferior a la resta de fils. _Exemple_: Imagina que estàs en la cua d'un mercat per comprar, on es dóna prioritat a la gent més major per poder comprar abans. En el moment que tú arribes, comencen a arribar molta gent més major que tu tota la estona, sense parar. Si no pots comprar menjar mai, en un temps estaràs famolenc. En aquest exemple, tu eres un fil amb prioritat baixa i la gent major son fils amb prioritat alta. ### Interbloqueig (deadlock) L'__interbloqueig__ es produeix quan dos o més processos o fils es troben esperant indefinidament per un esdeveniment que ha de generar un altre procés o fil que està bloquejat o que es troba esperant un recurs que té assignat un altre procés o fil que està bloquejat. _Exemple_: Susposem que tenim 2 fils __F1__ i __F2__, i tenim dos recursos (__R1__ i __R2__). Els dos fils necessiten tindre la propietat dels dos recursos per poder treballar. La seqüencia d'instruccions de cada fils és: <div class="row"> <div class="col"> - __F1__: ``` Petició R1 Petició R2 Instruccions Desbloquejar R2 Desbloquejar R1 ``` </div> <div class="col"> - __F2__: ``` Petició R2 Petició R1 Instruccions Desbloquejar R1 Desbloquejar R2 ``` </div> </div> Si els fils s'executen de manera paral·lela i executen la primera instrucció alhora es bloquejarien entre ells, ja que cada fil està esperant a obtindre el recurs que té en propietat l'altre fil, que no alliberarà fins que acabe. ![](/itb/DAM-PSP/UD2/img/deadlock.png){.center}{height=200} ### Bloqueig actiu (livelock) El __bloqueig actiu__ és molt similar a un _interbloqueig_, però aquest cas, el bloqueig es dóna perquè els fils involucrats van canviant el seu estat per desbloquejar-se a la vegada, tornant a bloquejar-se mútuament. Aquest bloqueig es dóna en algoritmes per previndre el _deadlock_. _Exemple_: Imagineu-se que aneu per la vorera d'un carrer, i vegeu algú que s'apropa en sentit contrari a tu. Els dos aneu pegats a la pared de les cases, pensant que l'altra persona s'apartarà. En el moment que esteu molt a prop, els dos s'aparteu cap al carrer, per deixar que l'altra persona passe. Com els dos s'heu apartat, s'heu tornat a bloquejar entre vosaltres i decidiu a la vegada tornar al lloc original. Si cap dels dos trenca el bucle, entreu en el que es coneix com bloqueig actiu. ![](/itb/DAM-PSP/UD2/img/livelock.png){.center} ## Mecanismes de sincronització Els problemes anteriorment esmentats ocorren pel fet que diversos fils s'executen concurrentment i no es té control sobre el seu ordre, cosa que pot desembocar en una ordenació i uns resultats no dessitjats. Per solucionar-ho, s'ha de controlar que els accessos a dades compartits es realitzin de manera ordenada o __síncrona__. Quan estiguen executant codi independent, que s'executin en paral·lel lliurement, s'anomena execució __asíncrona__. ### Condicions de Bernstein Les condicions de Berstein defineixen les condicions que permeten esbrinar si dos segments de codi ($S_1$ i $S_2$) es poden executar en paral·lel de manera asíncrona en diferents fils. Aquestes condicions permeten fer una anàlisi de dependència. Si les dues instruccions són independents, es poden executar en paral·lel. - __Dependència de flux__: Les variables d'entrada de $S_2$ han de ser diferents de les variables d'eixida de $S_1$. En cas contrari, $S_2$ depén de $S_1$. _Example_: $$ S_1: \textcolor{red}{C} = A + B \\ S_2: D = \textcolor{red}{C} + E $$ - __Antidependència__: Les variables d'entrada de $S_1$ han de ser diferents de les variables d'eixida de $S_2$. En cas contrari, $S_2$ depén de $S_1$. És el cas contrari al punt anterior. _Example_: $$ S_1: D = \textcolor{red}{C} + E \\ S_2: \textcolor{red}{C} = A + B $$ - __Dependència d'eixida__: Totes les variables d'eixida de $S_1$ han de ser diferents de les variables d'eixida de $S_2$. En cas contrari els dos segments podrien escriure al mateix lloc provocant resultats inesperats. _Example_: $$ S_1: \textcolor{red}{C} = A + B \\ S_2: \textcolor{red}{C} = D + E $$ El codi que no complisca aquestes condicions s'ha d'executar de manera síncrona. ### Operacions atòmiques Com s'ha exposat anteriorment, una __operació atòmica__ és aquella que es tradueix com una única instrucció i s'executarà completament sense ser interrompuda. _Exemple_: Imagina que fas un Bizum de 10€ a un conegut. Aquesta operació ha de restar 10€ del teu compte i transferir-los al compte de l'altra persona. Aquesta operació d'ha de realitzar com una única instrucció o podrien haver problemes de consistència en les dades. ### Secció crítica Una __secció crítica__ és aquella en què s'accedeix a variables i recursos compartits de manera ordenada, per la qual cosa les instruccions han d'executar-se de manera síncrona. Aquest concepte es pot aplicar tant a processos com a fils si accedeixen dades o recursos compartits. - Si ja hi ha un procés o fil executant la secció crítica i un altre vol executar-la, aquest últim es bloquejarà fins que el primer acabe amb la secció crítica. - D'aquesta manera s'estableix un abans-després en l'ordre d'execució de seccions crítiques, així els canvis són visibles des de la resta de processos o fils. _Exemple_: Una secció crítica podria englobal les instruccions per realitzar canvis en un compte bancari. Un fil ha d'esperar a que acabe una transacció en un compte per poder fer canvis en el mateix. La secció crítica té un problema i aquest és dissenyar un protocol d'execució de la secció crítica. Perquè estiga ben dissenyat ha de complir: - __Exclusió mútua__. Si un procés o fil està executant la secció crítica, cap altre pot passar a executar-la. - __Progrés__. Si no hi ha cap procés o fil executant la secció crítica i n'hi ha diversos esperant a executar-la, algun ha de començar. No poden estar esperant tots si cap no és a la secció crítica. - __Espera limitada__. Hi ha d'haver un nombre limitat de processos o fils que entren a la secció crítica quan un procés o fil demana executar-la. En cas contrari, es podria produir inanició. Resumint, el codi corresponent a la secció crítica ha d'acabar en un temps determinat i cal evitar la inanició. Per implementar la secció crítica hi ha diferents mecanismes, la implementació dels quals depèn de cada SO. Java proveeix aquests mecanismes, però ocultant al programador aquestes diferències que hi ha a nivell de SO. Els mecanismes són __objectes sincronitzats__, __semàfors__ i els __monitors__. ### Semàfors (semaphores) Els __semàfors__ són mecanismes que permeten que un nombre limitat de processos accedisquen de manera ordenada a un recurs. Es representa amb una variable sencera, el valor de la qual és el nombre de llocs disponibles al recurs compartit i una cua on emmagatzemar els processos bloquejats que esperen l'accés. A Java, s'utilitza la classe [`Semaphore`](https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/Semaphore.html). A la inicialització es proporciona un valor que serà el nombre de llocs disponibles. Hi ha dues operacions atòmiques per accedir i modificar el valor del semàfor: - __[`adquire()`](https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/Semaphore.html#acquire())__: Mitjançant aquesta operació es disminueix el nombre de buits en un, si el valor és menor que zero és que no hi ha lloc i el procés s'hauria de bloquejar fins que hi haja lloc. En cas que el valor siga negatiu, indica quans fils hi han en espera. - __[`release()`](https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/Semaphore.html#release())__: S'utilitza per avisar que un procés ha acabat d'utilitzar el recurs. Amb aquesta operació s'augmenta un el nombre de llocs disponibles. Si el valor és menor o igual a zero és que hi ha algun procés bloquejat esperant i caldrà despertar-lo. El procés que es desperta és aleatori i depèn de la implementació del semàfor i del SO on s'execute. Els __MUTEX__ (**MUT**ual **EX**clusion) són semàfors binaris que sols permeten que un fil tinga accés a un recurs. Podem crear un semàfor binari amb `new Semaphore(1)`. ### Monitors Un __monitor__ és un objecte o mòdul que es pot utilitzar d'una manera segura per diversos fils i proporciona un mecanisme de exclusió mútua: sols un fil pot accedir a aquest. recurs. Es podria dir que els monitors són com un __forrellat (lock)__ que permet l'accés o no al recurs. El seu funcionament és molt similar al dels semàfors binaris, però amb més simplicitat, ja que el programador no cal que faça explicita l'adquisició i alliberament del monitor. Els semàfors depenen que el programador introdueixi les sentències a l'ordre correcte per no bloquejar el sistema. A Java tots els objectes derivats de la classe __Object__(pràcticament tots), tenen associats dos monitors: un afecta la pròpia classe i un altre que afecta els mètodes definits com __`synchronized`__. Per utilitzar un monitor a Java s'utilitza la paraula clau `synchronized` sobre una regió del codi. D'aquesta manera aquesta secció s'executarà com si fora una __secció crítica__. `synchronized` es pot utilitzar en mètodes i en blocs sincronitzats. #### Mètodes sincronitzats Un mètode declarat `synchronized` garanteix que no es puga executar de manera concurrent a un altre mètode declarat `sinchronized` del mateix objecte. Quan un fil invoca un mètode `synchronized` intenta prendre el «lock» de l'objecte a què pertanya el mètode. Si està lliure l'adquireix i executa el mètode. Si el «lock» està ocupat, el fil se suspendrà fins que s'allibere el «lock». En cas que el mètode siga `static synchronized`, el «lock» serà de la classe i afectarà tots els mètodes `static synchronized` d'aquesta classe. Per crear un mètode sincronitzat cal afegir la paraula `synchronized` a la seua declaració. És important saber que els constructors per defecte són síncrons, de manera que no es poden marcar amb `synchronized`. - Quan un fil pren el «lock» d'un objecte pot accedir a un altre mètode `synchronized` d'aquest objecte sense necessitat d'alliberar el «lock». - Hi ha un «lock» per cada instància de l'objecte. - Quan s'estenga (`extends`) una classe i se sobreescriga un mètode `synchronized`, no és obligatori que el mètode nou siga synchronized. Si es vol que el mètode de la subclasse siga sincronitzat, cal especificar-ho. #### Blocs sincronitzats Els mètodes sincronitzats utilitzen un monitor que afecta tot l'objecte corresponent de manera que es bloquegen tots els mètodes sincronitzats. Utilitzant aquest monitor, es podrien fer ús de mètodes no sincronitzats mentre un fil tinga el «lock» dels mètodes sincronitzats de manera paral·lela. Si volem adquirir el «lock» de tota la instància, hem d'utilitzar un bloc `synchronized`. ```java public static void main(String[] args){ ... synchronized(object){ ... } } ``` #### wait() i notify() Les eines de sincronització anteriors utilitzen els mètodes `wait()` i `notify()` internament per realitzar la sincronització de fils. Quan un fil executa el mètode `wait()`, quedarà en estat __suspés__ i pararà la seua execució fins que niga notificat. Quan un fil executa el mètode `notify()`, notificarà a un fil (el qual no es pot triar) que pot continuar la seua execució. També existeix el mètode `notifyAll()`, que notificarà tots els fils que estiguen esperant. Tots aquests mètodes estan vinculats a un monitor en concret. Aquests mètodes són útils si es vol esperar fins que una condició es complisca per poder continuar amb l'execució del fil. ## Exemples en Java ### CopyShop #### Explicació Aquest programa simula una copisteria. La copisteria té 3 impresores que es poden utilitzar al mateix temps. S'han creat 5 clients, cadascun en un fil distint. Cada client realitza les següents accions: - Mira si hi ha alguna impresora lliure. - Si hi ha alguna, l'adquireix. - Si no hi ha cap, espera. - Quan aconsegueix accedir a una impresora lliure, imprimeix tots els documents. - Quan acaba d'imprimir, allibera la impresora que estava utilitzant. ```java shop.getSemaphore().acquire(); Printer printer = shop.getFreePrinter(); System.out.printf("%s ha obtingut l'impresora: %s\n", this.name, printer.getModel()); for (Document d : documents) { printer.printDocument(d); } System.out.printf("%s ha alliberat l'impresora: %s\n", this.name, printer.getModel()); shop.releasePrinter(printer); shop.getSemaphore().release(); ``` El mecanisme per gestionar les impresores lliures o ocupades es realitza mitjançant un __semàfor__ en la classe `CopyShop`, que s'ha creat amb el número de impresores que disposa: ```java public CopyShop() { freePrinters = new ArrayList<>(); usedPrinters = new ArrayList<>(); freePrinters.add(new Printer("HP DeskJet 2755e", 100)); freePrinters.add(new Printer("Brother MFC-J1010DW", 120)); freePrinters.add(new Printer("Epson Workforce Pro WF-3820", 50)); // Semàfor que permet tants clients com impresores en la tenda semaphore = new Semaphore(freePrinters.size()); } ``` A més, podem observar que els mètodes `getFreePrinter()` i `releasePrinter()` s'han definit com `synchronized`. Això s'ha fet perquè els dos mètodes modifiquen les llistes `freePrinters` i `usedPrinters`, i volem evitar que diversos fils puguen modificar les llistes a la vegada i ens evitem problemes derivats de la concurrència. ```java public synchronized Printer getFreePrinter(){ Printer p = freePrinters.remove(0); usedPrinters.add(p); return p; } public synchronized void releasePrinter(Printer p){ freePrinters.add(p); usedPrinters.remove(p); } ``` #### Codi font - <a href="/itb/DAM-PSP/files/ud2/examples/copyshop/CopyShop.java" download="CopyShop.java">CopyShop.java</a> ```java package ud2.examples.copyshop; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Semaphore; public class CopyShop { private List<Printer> freePrinters; private List<Printer> usedPrinters; private Semaphore semaphore; public CopyShop() { freePrinters = new ArrayList<>(); usedPrinters = new ArrayList<>(); freePrinters.add(new Printer("HP DeskJet 2755e", 100)); freePrinters.add(new Printer("Brother MFC-J1010DW", 120)); freePrinters.add(new Printer("Epson Workforce Pro WF-3820", 50)); // Semàfor que permet tants clients com impresores en la tenda semaphore = new Semaphore(freePrinters.size()); } public Semaphore getSemaphore() { return semaphore; } public synchronized Printer getFreePrinter(){ Printer p = freePrinters.remove(0); usedPrinters.add(p); return p; } public synchronized void releasePrinter(Printer p){ freePrinters.add(p); usedPrinters.remove(p); } public static void main(String[] args) { CopyShop shop = new CopyShop(); List<CustomerThread> customers = new ArrayList<>(); CustomerThread bob = new CustomerThread("Bob", shop); bob.addDocument("PSP Work", 10); bob.addDocument("TFG", 70); customers.add(bob); CustomerThread alice = new CustomerThread("Alice", shop); alice.addDocument("Chopin - Nocturnes, Op. 9: No. 2 in E-Flat Major", 5); alice.addDocument("Chopin - Nocturne in C-sharp minor, Op. posth.", 4); alice.addDocument("Beethoven - Für Elise", 3); alice.addDocument("The Real Book - Volume I", 247); customers.add(alice); CustomerThread patrick = new CustomerThread("Patrick", shop); patrick.addDocument("Don Quijote de la Mancha", 145); customers.add(patrick); CustomerThread pere = new CustomerThread("Pere", shop); pere.addDocument("Tirant lo Blanc", 276); customers.add(pere); CustomerThread mar = new CustomerThread("Mar", shop); mar.addDocument("Joshua Bloch - Effective Java", 50); customers.add(mar); for(CustomerThread customer : customers){ customer.start(); } for(CustomerThread customer : customers){ try { customer.join(); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } ``` - <a href="/itb/DAM-PSP/files/ud2/examples/copyshop/Document.java" download="Document.java">Document.java</a> ```java package ud2.examples.copyshop; public class Document { private String title; private int pages; public Document(String name, int pages) { this.title = name; this.pages = pages; } public String getTitle() { return title; } public int getPages() { return pages; } } ``` - <a href="/itb/DAM-PSP/files/ud2/examples/copyshop/Printer.java" download="Printer.java">Printer.java</a> ```java package ud2.examples.copyshop; public class Printer { private String model; private int delay; public Printer(String model, int delay) { this.model = model; this.delay = delay; } public String getModel() { return model; } public void printDocument(Document d){ try { int milis = d.getPages()*delay; Thread.sleep(milis); System.out.printf("Printer %s: Printed document %s with %d pages (%.2f seconds).\n", model, d.getTitle(), d.getPages(), milis/1000.0 ); } catch (InterruptedException e) { throw new RuntimeException(e); } } } ``` - <a href="/itb/DAM-PSP/files/ud2/examples/copyshop/CustomerThread.java" download="CustomerThread.java">CustomerThread.java</a> ```java package ud2.examples.copyshop; import java.util.ArrayList; import java.util.List; public class CustomerThread extends Thread { private List<Document> documents; private String name; private CopyShop shop; public CustomerThread(String name, CopyShop shop) { this.name = name; this.documents = new ArrayList<>(); this.shop = shop; } public List<Document> getDocuments() { return documents; } public void addDocument(String title, int pages) { documents.add(new Document(title, pages)); } @Override public void run() { try { shop.getSemaphore().acquire(); Printer printer = shop.getFreePrinter(); System.out.printf("%s ha obtingut l'impresora: %s\n", this.name, printer.getModel()); for (Document d : documents) { printer.printDocument(d); } System.out.printf("%s ha alliberat l'impresora: %s\n", this.name, printer.getModel()); shop.releasePrinter(printer); shop.getSemaphore().release(); } catch (InterruptedException e) { throw new RuntimeException(e); } } } ``` ### PitStop #### Explicació Aquest exemple simula una parada en bòxers d'un cotxe de Formula 1. Quan un cotxe para, hi ha una persona encarregada de elevar el cotxe per poder canviar les rodes del cotxe. Cada roda del cotxe és canviada per una persona diferent. Una vegada d'han canviat totes les rodes, es pot tornar a baixar el cotxe i continuar la carrera. En el següent codi, creem un mecànic que s'encarrega de pujar i baixar el cotxe, i altres quatre mecànics que s'encarregen de canviar les rodes. L'ordre de les acciona hauria de ser: - El mecànic alça el cotxe. - Els mècanics de les rodes esperen a que el cotxe estiga alçat. - Els mècanics de les rodes canvien les rodes. - El mecànic espera a que totes les rodes hagen segut canviades. - El mecànic baixa el cotxe. Per realitzar aquestes esperes, s'han definit els següents mètodes en la classe `Car``: ```java public synchronized void raise() throws InterruptedException { Thread.sleep(500); raised = true; notifyAll(); } public synchronized void release() throws InterruptedException { while(!readyToRelease()) wait(); Thread.sleep(500); raised = false; notifyAll(); } public boolean readyToRelease(){ for(Tire t : tires){ if(t.getRemainingKilometers() != 100) return false; } return true; } public synchronized void replaceTire(Tire t) throws InterruptedException{ while(!raised) wait(); Thread.sleep(ThreadLocalRandom.current().nextInt(500, 1000)); t.replace(); notify(); } ``` Si un fil intenta canviar una roda (`replaceTire`), haurà d'esperar fins que el cotxe estiga en alt (`raised`). Això es possible gràcies al mètode `wait()`. Si un altre fil alça el cotxe amb el mètode `raise()`, notificarà a tots els possibles fils que estiguen esperant que continuen la seua execució. Com el valor de la variable `raised` ha canviat, els fils poden continuar la seua execució. El mateix passa quan un fil intenta baixar el cotxe amb `release()`. Fins que no s'hagen canviat totes les rodes no es podrà baixar el cotxe. #### Codi font - <a href="/itb/DAM-PSP/files/ud2/examples/pitstop/PitStop.java" download="PitStop.java">PitStop.java</a> - <a href="/itb/DAM-PSP/files/ud2/examples/pitstop/Car.java" download="Car.java">Car.java</a> - <a href="/itb/DAM-PSP/files/ud2/examples/pitstop/Tire.java" download="Tire.java">Tire.java</a> - <a href="/itb/DAM-PSP/files/ud2/examples/pitstop/Mechanic.java" download="Mechanic.java">Mechanic.java</a> - <a href="/itb/DAM-PSP/files/ud2/examples/pitstop/TireMechanic.java" download="TireMechanic.java">TireMechanic.java</a> - <a href="/itb/DAM-PSP/files/ud2/examples/pitstop/RaiseMechanic.java" download="RaiseMechanic.java">RaiseMechanic.java</a>