Estat compartit
El disseny de programari concurrent que comparteix dades es basa en la idea que els fils poden accedir a les mateixes dades, i que aquestes dades poden ser modificades per qualsevol fil.
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
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
La interferència es produeix quan dues operacions, que s'executen en diferents fils, però que actuen sobre les mateixes dades, s'entrellacen. Això vol dir que les dues operacions consten de diversos passos i les seqüències de passos se superposen. Aquest fenomen també s'anomena 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
El problema és que dues operacions, que s'executen en diferents fils, però que actuen sobre les mateixes dades, no veuen els canvis que s'han fet en les dades. Això es deu a que els fils tenen una còpia local de les dades compartides, i no es veuen els canvis que fan els altres fils.
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 a 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
Les possibles estratègies per a gestionar l'accés simultani a dades compartides són: confinament, immutabilitat, tipus thread-safe i sincronització.
- Confinament. No compartiu la variable entre fils.
- Immutabilitat. Feu que les dades compartides siguin immutables. Tots els camps de la classe han de ser finals.
- Tipus de dades thread-safe. Encapsulem les dades compartides en un tipus de dades existent amb seguretat que realitzi la coordinació.
- Sincronització. Utilitzeu 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ó
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
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.