Modelul clasic
Ne ocupm n continuare de situaia cnd excludem o memorie comun ca posibilitate de
schimb de informaii ntre procese. Varianta luat n considerare n continuare este ca acest schimb
de informaii s se fac prin transmitere de informaii (mesaje) de la un proces la altul.
Exist o mare varietate de posibiliti de transmitere a mesajelor ntre procese (fire n Java).
Se disting trei grade principale de sincronizare:
1) Lipsa sincronizrii (transmiterea asincron). Sursa emite mesaje fr s se intereseze
dac precedentele mesaje au ajuns sau nu la destinaie. Inconvenientul evident este acela c atunci
cnd la destinaie ajunge un mesaj, destinatarul nu tie dac este sau nu vorba de ultima informaie
plecat de la surs. Similar, atunci cnd transmite un mesaj, sursa nu cunoate cte din precedentele
mesaje au ajuns la destinaie, deci nu cunoate gradul de informare al destinatarului. Transmiterea
asincron poate fi comparat cu corespondena prin pot.
2) Sincronizarea simpl. Dup ce emitorul a transmis un mesaj, el nu va mai trimite altul
(va atepta) pn cnd mesajul va fi recepionat de destinatar. Aceasta presupune c sursa este
informat dac destinatarul a preluat mesajul. Spunem c are loc o ntlnire (un rendez-vous),
imaginndu-ne c emitorul se ntlnete cu destinatarul pentru a-i nmna mesajul. Transmiterea
unor informaii prin fax poate fi ncadrat n acest mod de sincronizare: emitorul primete imediat
o confirmare din partea destinatarului c a primit mesajul.
3) Invocarea la distan (rendez-vous extins). n plus fa de sincronizarea simpl,
emitorul primete nu numai confirmarea de recepie, dar i un rspuns din partea destinatarului;
abia dup aceea emitorul poate transmite un alt mesaj.
n continuare ne vom rezuma la sincronizarea simpl.
Sincronizarea simpl are la baz lucrul cu canale. Afirmm c:
Utilizarea canalelor reprezint modalitatea cea mai natural
pentru rezolvarea problemei sincronizrii.
n prezentarea clasic, un canal are o singur surs i o singur destinaie, operaiile fiind
urmtoarele:
- c ! e cu semnificaia c expresia e este transmis canalului c;
- c ? v cu semnificaia c variabila v primete valoarea asociat canalului c.
Pentru fiecare (nume de) canal, procesul surs i procesul destinaie sunt unic
determinate: sursa este unicul proces ce conine operaia c ! e, iar destinaia este unicul proces n
care apare operaia complementar c ? v.
S observm ns c procesul surs nu cunoate identitatea procesului destinaie i nici
procesul destinaie nu tie de la ce proces primete mesajul. Legtura este considerat
unidirecional. Un canal transmite date, dar nu poate memora date.
Cele dou operaii sunt sincronizate, n sensul c transmisia are loc efectiv numai dac
procesul emitor este gata s transmit, iar procesul de destinaie este gata s recepioneze; n caz
contrar, procesul care ajunge primul la "ntlnire" (rendez-vous) este blocat n ateptarea celuilalt.
Nici un proces nu poate avansa nainte de terminarea transmiterii.
Precizm c expresia transmis prin canal i variabila care o recepioneaz trebuie s aib
acelai tip.
1
Cozile sincrone sunt similare canalelor de tip rendez-vous din CSP i Ada, cu excepia
faptului c mai multe fire pot ncerca s pun n, respectiv s ia din, coad. Orice operaie de
inserare trebuie s atepte operaia corespunztoare de extragere efectuat de alt fir i viceversa.
O coada sincron nu are capacitate, ceea ce exclude multe operaii asupra cozilor. Nu este
permis inserarea n coad a lui null.
Clasa permite o politic de fairness, stabilit prin constructor.
Constructorii clasei sunt urmtorii:
public SynchronousQueue()
creaz o coad sincron pentru care vom alege o politic de fairness punnd true ca
argument; altfel, ordinea de deblocare a firelor nu corespunde neaprat ordinii de blocare.
Pentru ncercrile de a pune/lua cu ateptarea operaiei
complementare sunt disponibile metodele put i take.
Pentru ncercri de a pune/lua imediat sunt disponibile metodele
offer i poll.
detecteaz i ntoarce (prin tergere) capul cozii, ateptnd - dac este necesar - ca alt fir s-l
insereze.
avut loc.
E poll()
ntoarce capul cozii, tergndu-l din coad; dac la momentul invocrii coada este vid,
ntoarce null. Este folosit atunci cnd ncercm s citim din unul sau mai multe canale,
cu alternativ (eventual vid) n caz de eec.
Conducte de canale
Exemplul 1. Generarea primelor n numere prime folosind canale.
F0
2
c0
...
ci-1
Fi
ci
...
cn
Fn+1
Vom lucra cu urmtoarele fire de executare (de tipul Filtru) i canale (de tipul
SynchronousQueue<Integer>):
- firul F0 pompeaz prin canalul c0 numerele naturale 2,3,... ctre firul F1;
- fiecare dintre firele Fi (i=1,...,n) primete valori prin canalul ci-1 (notat n program prin
st) i trimite valori lui Fi+1 prin canalul ci (notat n program prin dr);
- firul Fn+1 primete o valoare prin canalul cn, tiprete mesajul "STOP" i se termin.
Fiecare dintre firele Fi (i=1,...,n) acioneaz astfel:
- memoreaz prima valoare primit, numit valoarea sa de baz;
- "filtreaz" urmtoarele numere pe care le primete, n sensul c transmite la dreapta pe
conduct numrul primit din stnga doar dac acesta nu este divizibil cu valoarea sa de baz.
Valoarea de baz a fiecrui fir Fi (i=1,...,n) este un numr prim i anume al i-lea numr
prim, deoarece este cel mai mic numr natural nedivizibil cu vreun numr prim mai mic dect el.
Pentru terminarea programului, declarm firele Fi (i=0,...,n) ca fiind fire daemon,
astfel nct terminarea firului principal i a firului Fn+1 determin terminarea ntregului program.
import java.util.*; import java.util.concurrent.*;
class Filtru extends Thread {
static int n;
int id; SynchronousQueue<Integer> st,dr;
Filtru(int i, SynchronousQueue<Integer> s,
SynchronousQueue<Integer> d) {
id = i; st = s; dr = d;
}
public void run() {
int k=2; int x,y;
try {
if (id==0) while(true) dr.put(k++);
else if(id==n+1) {
st.take(); System.out.println("STOP");
}
else {
x = st.take(); System.out.print(x + "\t");
while(true) {
y = st.take(); if(y%x != 0) dr.put(y);
}
}
}
catch(InterruptedException e) { }
}
}
class Prime {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("n = "); int n = sc.nextInt();
Filtru.n = n; Filtru[] F = new Filtru[n+2];
SynchronousQueue<Integer> st=null,
dr = new SynchronousQueue<Integer>();
for(int i=0; i<n+2; i++) {
F[i] = new Filtru(i,st,dr);
st = dr; dr = new SynchronousQueue<Integer>();
3
}
for(int i=0; i<n+1; i++) F[i].setDaemon(true);
for(int i=0; i<n+2; i++) F[i].start();
sus
sus
dr
...
st
Fi
dr
...
st
Fn-1
class SumeCanale {
public static void main(String[] args) throws Exception {
System.out.print("n = ");
int n = new Scanner(System.in).nextInt();
Scanner in = new Scanner(new FileInputStream("a.b"));
Filtru.n = n;
Filtru[] F = new Filtru[n];
ArrayList<SynchronousQueue<Integer>> sus =
new ArrayList<SynchronousQueue<Integer>>();
for(int i=0; i<n; i++)
sus.add(new SynchronousQueue<Integer>());
SynchronousQueue<Integer> st=null,
dr = new SynchronousQueue<Integer>();
for(int i=0; i<n; i++) {
F[i] = new Filtru(i,sus.get(i),st,dr);
st = dr; dr = new SynchronousQueue<Integer>();
}
for(int i=0; i<n; i++) F[i].setDaemon(true);
for(int i=0; i<n; i++) F[i].start();
while(in.hasNextInt())
for(int i=0; i<n; i++)
sus.get(i).put(in.nextInt());
Thread.sleep(10); // se astepta afisarea ultimei sume partiale
System.out.println("\nSuma = " + Filtru.suma);
}
Observaie. Diagrama ocuprii firelor are forma unui paralelogram cu laturile paralele
orizontale, "nclinat" spre dreapta (liniile paralele orizontale corespund firelor):
Fir
Fiecare fir F[i] trimite n ordine prin canal mai nti perechea (i,'M') i apoi perechea
(i,'G') pentru a iniia cele dou operaii (de a mnca i de a gndi). Cele dou operaii sunt
validate de firul Control.
Evidena beioarelor disponibile pentru fiecare filozof este asigurat de tabloul b; fiecare
dintre componentele b[i] poate lua numai una dintre valorile 0,1,2, reprezentnd numrul de
beioare pe care le poate ridica filozoful F[i].
Nu este necesar o excludere reciproc asupra beioarelor, deoarece ele sunt folosite numai
n firul Control.
Dac firul Control primete, prin canalul canal, o "comand" (i,'M'), i va da curs
doar dac b[i]=2 :
- n caz afirmativ, va actualiza numrul de beioare disponibile pentru filozoful din stnga i
pentru filozoful din dreapta sa i va marca (prin valoarea boolean mananca[i]) faptul c acum
filozoful F[i] mnnc.
- n caz contrar, ncercarea de a mnca este pierdut; cum ea este urmat de comanda
(i,'G'), trebuie ca aceasta s nu determine vreo aciune.
Dac firul Control primete o "comand" (i,'G'), i va da curs numai dac
mananca[i] are valoarea true. n caz afirmativ, va actualiza numrul de beioare disponibile
pentru filozoful din stnga i pentru filozoful din dreapta sa i va marca (prin valoarea boolean
mananca[i]) faptul c acum filozoful F[i] nu mai mnnc.
Vom declara firul control i firele F[i] ca fire daemon. Drept urmare, terminarea firului
principal (realizat prin citirea unui ir de caractere) va antrena terminarea ntregului program.
Am ales ca firele corespunztoare filozofilor s fie create n constructorul clasei Control.
import java.util.*;
import java.util.concurrent.*;
class Fil_Canale {
public static void main(String[] sss) throws Exception {
Control control = new Control();
control.setDaemon(true); control.start();
System.in.read();
}
}
class C { // Cerere
int i; char c;
C(int ii, char cc) { i = ii; c = cc; }
}
6
}
}
catch(InterruptedException e) { }
C[i]
canal
Control
Firul Control primete pe rnd prin canal cte o cerere i, dac este posibil rezolvarea ei,
o duce la ndeplinire. n particular vor exista cereri de deschidere irosite/euate/pierdute, fapt de
care trebuie inut cont n primul rnd la cererea de nchidere corespunztoare.
Aciunile cititorilor, scriitorilor i procesului de control se execut concurent. Fiecare
aciune a unui cititor sau scriitor const n a transmite intenia sa de a deschide cartea i apoi de a o
nchide.
Fiecare cerere este o pereche (i,s) unde i este numrul de ordine la cititorului sau
scriitorului, iar s este unul dintre urmtoarele iruri de caractere:
- "cdesch" pentru deschidere pentru citire;
- "cinch" pentru nchidere pentru citire;
- "sdesch" pentru deschidere pentru scriere;
- "sinch" pentru nchidere pentru citire.
Procesul Control mai ine evidena cererilor de deschidere/nchidere prin intermediul
tablourilor citeste i scrie de dimensiuni nc respectiv ns, unde de exemplu citeste[i] este
true dac i numai dac cititorul i a deschis cu succes cartea, dar nu a nchis-o nc. Aceasta este
necesar deoarece cererile de deschidere care nu pot fi satisfcute sunt considerate euate (nu sunt
rencercate) i deci o cerere de nchidere trebuie validat doar dac cititorul/scriitorul a deschis cu
succes cartea.
Variabila nrcit ine evidena numrului de cititori activi, iar variabila boolean sactiva
va avea valoarea true dac i numai dac un scriitor are acces la carte.
import java.util.*; import java.util.concurrent.*;
class B {
static SynchronousQueue<C> canal = new SynchronousQueue<C>(true);
static void delay(int i) {
try { Thread.sleep( (int) (i*Math.random()) ); }
catch(InterruptedException e) { }
}
}
class C {
// Cerere
int i; String s;
C(int ii, String ss) { i = ii; s = ss; }
8
}
class Cititor extends Thread {
int id;
Cititor(int i) { id = i; }
10
else;
}
else if(Ob.s.equals("sdesch")) {
if( !sactiva && (nrcit==0) ) {
System.out.print("S" + Ob.i + "(\t");
sactiva = true; scrie[Ob.i] = true;
}
else System.out.print("~S" + Ob.i + "(\t");
}
else if(Ob.s.equals("sinch") && scrie[Ob.i] ) {
System.out.print("S" + Ob.i + ")\t");
sactiva = false; scrie[Ob.i] = false;
}
else;
}
}
catch(InterruptedException e) { }
}
class CitScr_Canale {
public static void main(String[] sss) throws Exception {
int nc = 8, ns = 5, i;
Cititor[] C = new Cititor[nc];
for(i=0; i<nc; i++) C[i] = new Cititor(i);
Scriitor[] S = new Scriitor[ns];
for(i=0; i<ns; i++) S[i] = new Scriitor(i);
Control control = new Control(nc,ns);
for(i=0; i<nc; i++) C[i].start();
for(i=0; i<ns; i++) S[i].start();
control.setDaemon(true); control.start();
}
}
Clasa Exchanger
Tot n pachetul java.util.concurrent apare clasa:
public class Exchanger<tip> extends Object
Un obiect de tipul Exchanger reprezint un punct de sincronizare la care dou fire se pot
racorda pentru a schimba ntre ele elemente de tipul tip. Fiecare fir ofer un obiect ca argument
la invocarea metodei exchange; cnd ambele fire ajung la "ntlnire", are loc interschimbarea
obiectelor oferite.
Un astfel de obiect poate fi privit ca o form bidirecional a lui SynchronousQueue.
Aplicaiile se regsesc n algoritmii genetici i n proiectarea conductelor (canalelor).
Observaie (privind consistena memoriei). Pentru fiecare pereche de fire ce schimb
ntre ele cu succes obiecte, aciunile premergtoare lui exchange() au loc naintea celor ce
urmeaz unui return din invocarea exchange() corespunztoare din cellalt fir.
10
Metoda:
public tip exchange(tip x) throws InterruptedException
ateapt un alt fir s ajung la punctul de schimb (afar de cazul cnd firul curent este n starea
"ntrerupt") i apoi are loc transferul bidirecional. Dac un alt fir ateapt deja la punctul de
schimb, executarea sa este reluat i are loc schimbul. Dac niciun fir nu ateapt, firul curent
devine "adormit" pn cnd fie alt fir ajunge la punctul de schimb, fie alt fir ntrerupe firul curent.
n cadrul schimbului, valoarea trimis este x, iar valoarea primit este cea returnat de
metod.
Dac firul curent este n starea "ntrerupt" cnd s-a ajuns la exchange() sau este
ntrerupt n timp ce ateapt schimbul, este lansat excepia InterruptedException i firul
iese din starea "ntrerupt".
Menionm c metoda are i o variant ce prevede o ateptare limitat n timp.
Exemplul 5. Revenim la problema celor dou tablouri s i t care n final trebuie s
conin cele mai mici, respectiv cele mai mari valori din totalitatea elementelor lor.
Soluia anterioar, cea care folosea semafoare, presupunea c ambele procese au acces la
ambele tablouri. n soluia care urmeaz, care folosete clasa Exchange, fiecare fir cunoate doar
propriul tablou.
Cele dou fire se ateapt pn cnd i calculeaz maximul, respectiv minimul
elementelor din tabloul propriu, dup care i interschimb cele dou valori. Firul corespunztor
lui s se termin cnd primete o valoare mai mic dect maximul elementelor proprii; firul
corespunztor lui t se termin cnd primete o valoare mai mare dect minimul elementelor
proprii.
import java.util.*; import java.util.concurrent.*;
class MinMaxEx {
public static void main(String[] www) throws Exception {
Exchanger schimb = new Exchanger<Integer>();
S FirS = new S(schimb); T FirT = new T(schimb);
FirS.start(); FirT.start();
FirS.join(); FirT.join();
FirS.scrie(); System.out.println(); FirT.scrie();
}
}
class S extends Thread {
int[] s; int ns; Exchanger schimb;
S(Exchanger e) {
schimb = e;
Scanner sc = new Scanner(System.in);
System.out.print("ns = "); ns = sc.nextInt();
s = new int[ns];
System.out.print("Dati cele ns=" + ns +" elemente: ");
for(int i=0; i<ns; i++) s[i] = sc.nextInt();
}
public void run() {
try {
Integer max = 0, min_t; int poz = -1;
while(true) {
11
12
max = Integer.MIN_VALUE;
for(int i=0; i<ns; i++)
if(s[i]>max) { max = s[i]; poz = i; }
min_t = (Integer) schimb.exchange(max);
System.out.println("S : am primit " + min_t);
if(min_t>max) break;
else s[poz] = min_t;
}
}
catch(Exception e) { }
}
void scrie() {
for(int i=0; i<ns; i++) System.out.print("\t" + s[i]);
}
}
class T extends Thread {
int[] t; int nt; Exchanger schimb;
T(Exchanger e) {
schimb = e;
Scanner sc = new Scanner(System.in);
System.out.print("nt = "); nt = sc.nextInt();
t = new int[nt];
System.out.print("Dati cele nt=" + nt +" elemente: ");
for(int i=0; i<nt; i++) t[i] = sc.nextInt();
}
public void run() {
try {
Integer min = 0, max_s; int poz = -1;
while(true) {
min = Integer.MAX_VALUE;
for(int i=0; i<nt; i++)
if(t[i]<min) { min = t[i]; poz = i; }
max_s = (Integer) schimb.exchange(min);
System.out.println("T : am primit " + max_s);
if(max_s<min) break;
else t[poz] = max_s;
}
}
catch(Exception e) { }
}
void scrie() {
for(int i=0; i<nt; i++) System.out.print("\t" + t[i]);
}
}
12