Estat compartit
En el model d’estat compartit, tots els fils poden llegir i modificar les mateixes dades.
Un exemple de llenguatge que utilitza aquest model és Java.
Tenim dos reptes a resoldre:
- L’accés segur a les dades compartides. Quins mecanismes podem utilitzar per assegurar-nos que els fils accedeixen de forma segura a les dades compartides?
- La coordinació entre els fils. Com podem gestionar l’ordre d’execució dels fils, sincronitzar-los quan calgui i gestionar les seves interaccions?
Accés segur
Les dades compartides només són un problema quan algú hi escriu.
Si l’accés de tots els fils és només de lectura, no hi ha problema. El problema és quan hi ha accés simultani de lectura i escriptura.
Hi ha dues situacions problemàtiques: la interferència de fils i la coherència de dades.
Interferència de fils
Dues operacions correctes poden donar un resultat incorrecte només perquè s’entrellacen.
Passa quan les dues operacions consten de diversos passos, actuen sobre les mateixes dades i les seqüències de passos se superposen. Aquest fenomen s’anomena interferència de fils o race condition.
Per exemple: l’operació d’increment d’un comptador consta de llegir el valor actual, incrementar-lo i escriure el nou valor. Si dues operacions d’increment s’executen simultàniament, el resultat final pot ser erroni.
La solució és sincronitzar l’accés a les dades compartides, fent que cada operació s’executi de forma atòmica.
Coherència de dades
Un fil pot no arribar a veure mai un canvi que un altre fil ja ha fet.
Això es deu al fet que els fils tenen una còpia local de les dades compartides. Dos fils poden actuar sobre les mateixes dades i no veure cap dels canvis que hi fa l’altre.
Per exemple, si un fil canvia una variable booleana de valor i un altre fil llegeix el valor, pot ser que no vegi el canvi. Això es deu al fet que el compilador pot optimitzar el codi, i no llegir el valor de la variable cada vegada que es fa servir, sinó que el guarda en un registre. Això fa que el fil no vegi els canvis que fan els altres fils.
Estratègies
Hi ha quatre estratègies per a gestionar l’accés simultani a dades compartides, i les dues primeres consisteixen a no tenir el problema: no compartir, o compartir només dades que no canvien.
- Confinament. No comparteixis la variable entre fils. Una variable local d’un mètode ja està confinada: viu a la pila del fil que l’executa i cap altre fil hi pot arribar.
- Immutabilitat. Fes que les dades compartides siguin immutables. Tots els camps de la classe han de ser finals.
- Tipus de dades thread-safe. Encapsula les dades compartides en un tipus de dades existent amb seguretat que realitzi la coordinació.
- Sincronització. Utilitza la sincronització per evitar que els fils accedeixin al mateix temps establint zones crítiques de codi: fragments de codi que accedeixen a dades compartides i que s’han d’executar de forma atòmica.
Coordinació
Abans de repartir feina entre fils, cal saber si el problema es deixa partir.
A l’hora de dissenyar el flux del programari concurrent, fes-te aquestes preguntes:
- Cal entendre la solució al problema. Habitualment, parteix de la solució seqüencial, per trobar la concurrent.
- Considera si pot ser paral·lelitzada. Alguns problemes són inherentment seqüencials.
- Pensa en les oportunitats de paral·lelitzar que permeten les dependències entre les dades. Si no hi ha dependències, podem descompondre-les i paral·lelitzar-les.
- Busca els llocs on la solució consumeix més recursos, com a candidats de paral·lelització.
- Descompon en tasques el problema, per veure si aquestes poden ser executades independentment.
Aquests són alguns mecanismes disponibles a diferents llenguatges per a coordinar els fils:
- Crear una o més tasques que aniran executant-se en paral·lel.
- Que un fil esperi que es completi un altre (join).
- Que un fil notifiqui a un altre que ha completat una tasca (notify).
- Que un fil esperi que un altre li notifiqui que ha completat una tasca (wait).
- Que un fil envii un senyal d’interrupció a un altre fil (interrupt).
Vitalitat (liveness) d’un sistema multifil
Un programa concurrent pot ser correcte i no avançar mai.
La vitalitat d’una aplicació (liveness) és la seva capacitat per a executar-se en el temps que toca. Els problemes més habituals que poden desbaratar aquesta vitalitat són:
- El deadlock (interbloqueig): dos o més fils es bloquegen per sempre, esperant l’un per l’altre. Pot passar si dos fils bloquegen recursos que necessiten esperant que estiguin lliures d’altres, que mai ho seran.
- La starvation (inanició): la denegació perpètua dels recursos necessaris per a processar una feina. Un exemple seria l’ús de prioritats, on sempre els fils amb més prioritat són atesos, i els altres mai ho són.
- El livelock és molt semblant al deadlock, però els fils sí que canvien el seu estat, tot i que mai s’arriba a una situació de desbloqueig.
Quan un client fa una petició a un servidor, el servidor ha d’aconseguir l’accés exclusiu als recursos compartits necessaris. L’ús correcte de les zones crítiques permetrà que el sistema tingui una millor vitalitat quan la càrrega de peticions sigui alta.