Documente Academic
Documente Profesional
Documente Cultură
2
3.1. Exemple introductive ............................................................................................................................................... 2
3.1.1. Administrarea tamponată a intrărilor-ieşirelor ................................................................................................. 2
3.1.2. Comanda unui proces industrial ....................................................................................................................... 3
3.2. Noţiune de proces secvenţial ................................................................................................................................... 3
3.2.1. Proces unic. Context ......................................................................................................................................... 3
3.2.2. Relaţii între procese .......................................................................................................................................... 4
3.2.2.1. Mulţimi de procese. Paralelism ................................................................................................................. 4
3.2.2.2. Concurenţa proceselor. Resurse virtuale ................................................................................................... 5
3.2.2.3. Excludere mutuală ..................................................................................................................................... 6
3.3. Sincronizarea proceselor.......................................................................................................................................... 6
3.3.1. Exprimarea şi implementarea restricţiilor de precedare ................................................................................... 6
3.3.2. Probleme de realizare a sincronizării ................................................................................................................ 7
3.3.3. Monitorul – mecanism de sincronizare ............................................................................................................. 8
3.3.3.1. Definiţii...................................................................................................................................................... 8
3.3.3.2. Exemple de utilizare .................................................................................................................................. 9
3.4. Implementarea sincronizării .................................................................................................................................. 10
3.4.1. Probleme-tip ................................................................................................................................................... 10
3.4.2. Administrarea unei resurse partajate .............................................................................................................. 10
3.4.2.1. Alocarea resurselor banalizate ................................................................................................................. 10
3.4.2.2. Modelul cititorului şi redactorului ........................................................................................................... 12
3.4.3. Comunicarea între procese ............................................................................................................................. 13
3.4.3.1. Modelul producătorului şi consumatorului .............................................................................................. 13
3.4.3.2. Primitive de comunicare .......................................................................................................................... 14
3.4.3.3. Aplicaţii : relaţia client-server ................................................................................................................. 16
3.4.4. Administrarea intrărilor-ieşirilor .................................................................................................................... 16
3.4.4.1. Administrarea unui periferic .................................................................................................................... 16
3.4.4.2. Buferizarea imprimării ............................................................................................................................ 17
3.4.5. Sincronizare temporală ................................................................................................................................... 18
3.5. Gestionarea dinamică a proceselor ........................................................................................................................ 19
3.6. Sincronizarea în Windows ..................................................................................................................................... 20
3.6.1. Procese şi fire ................................................................................................................................................. 21
3.6.2. Necesitatea sincronizării ................................................................................................................................. 21
3.6.3. Structura mecanismului de sincronizare în Windows ..................................................................................... 21
3.6.4. Administrarea obiectelor de sincronizare în Windows ................................................................................... 22
3.6.4.1. Excluderea mutuală ................................................................................................................................. 22
3.6.4.2. Evenimentele ........................................................................................................................................... 22
3.6.4.3. Semafoarele ............................................................................................................................................. 22
3.6.4.4. Secţiunile critice ...................................................................................................................................... 22
3.6.4.5. Protejarea accesării variabilelor ............................................................................................................... 23
3.6.4.6. Sincronizarea în MFC .............................................................................................................................. 23
3.6.5. Exemplu de sincronizare în Windows ............................................................................................................ 23
3.6.6. Utilizarea secţiunilor critice în Windows ....................................................................................................... 23
3.6.6.1. Structura RTL_CRITICAL_SECTION ................................................................................................... 24
3.6.6.2. Funcţii API pentru secţiunile critice ........................................................................................................ 26
3.6.6.3. Clase de secţiuni critice ........................................................................................................................... 27
3.6.6.4. Depanarea secţiunilor critice ................................................................................................................... 28
3.7. Exerciţii la capitolul 3 ........................................................................................................................................... 31
tm1 tm2
(memorie) (memorie)
td
(disc)
În realitate, citirea şi scrierea pe disc sunt executate pe acelaşi canal, ceea ce poate impune unele restricții suplimentare privind simultaneitatea
executării lor.
p q p q p q
(2)
p q
(3)
Într-un mod mai riguros ar trebui să adăugăm aici “punctele întreruptible” care pot fi prezente în unele instrucţiuni de lungă durată; o executare a
unei asemenea instrucţiuni poate fi considerată o suită de mai multe acţiuni.
Pentru comoditatea expunerii considerăm, că sfârşitul unei acţiuni şi începutul acţiunii următoare sunt două evenimente distincte, datele cărora sunt
diferite, deşi stările corespunzătoare sunt identice.
2) Alegerea nivelului de observare depinde de finețea fenomenelor, care dorim să le considerăm elementare. Dacă
trebuie să studiem executarea instrucţiunilor în “pipe-line” pe un procesor microprogramat, în calitate de nivel
de observare va fi ales nivelul microinstrucţiunilor, iar contextul va fi completat cu memoria microprogramelor
și registrele interne.
Vom examina realizarea, la nivelul de bază, a unei scheme de execuţie de tipul 2. La fiecare realocare a
procesorului, contextul procesului curent trebuie salvat, pentru a permite reluarea ulterioară a acestei execuţii. Dacă
memoria are o capacitate suficientă pentru a păstra toate segmentele, doar contextul procesorului trebuie salvat. Dacă
memoria principală poate conţine, la un moment de timp dat, doar segmentul procedură şi datele unui singur proces,
aceste segmente de asemenea trebuie salvate pe disc. Această remarcă justifică definiţia operaţională, adesea întâlnită,
a contextului unui proces ca mulţime a informaţiilor care trebuie salvată pentru a permite reluarea ulterioară a
procesului, dacă execuţia acestuia a fost întreruptă.
3.2.2.2. Concurenţa proceselor. Resurse virtuale
Situaţia descrisă de schemele 1 şi 2 nu rezultă dintr-o legătură logică între p şi q, ci doar din existenţa unui singur
procesor. Ea poate fi caracterizată astfel: fie o mulţime de procese contextele cărora au un obiect comun, care poate fi
utilizat la un moment de timp dat de un singur proces. Se va spune în acest caz, că obiectul constituie o resursă critică
pentru procesele date sau că procesele sunt în excludere mutuală (excludere reciprocă sau concurenţă) pentru
utilizarea unei resurse. În situaţia descrisă, procesorul este o resursă critică pentru procesele p şi q.
Observăm că excluderea mutuală a unei resurse conduce la “serializarea” execuţiei proceselor concurente, în cazul
unor acţiuni, care cer această resursă (în exemplul dat, toate acţiunile). Schemele 1 şi 2 diferă doar prin nivelul de fineţe
la care este executată serializarea.
Funcţionarea corectă a unei mulţimi de procese, care participă la îndeplinirea unei lucrări comune, implică relaţii
logice de cooperare (v.3.1.1). Este comod să se separe această cooperare de concurenţa pentru resursele fizice cu
scopul de a simplifica înţelegerea şi aplicarea celor două tipuri de relaţii. Pentru aceasta este folosită noţiunea de
resurse virtuale: fiecărei resurse fizice critice i se asociază tot atâtea copii imaginare (sau virtuale) ale acestei resurse
câte procese concurente solicită utilizarea ei. Suntem nevoiţi să tratăm două probleme distincte:
1) respectarea relaţiilor de cooperare între procesele, care, fiecare posedă (conceptual) resursele fizice solicitate şi
pentru care paralelismul în execuţie nu este restricţionat de competiţia pentru resurse,
2) reglarea problemei de concurenţă pentru resursele fizice printr-o serializare convenabilă a execuţiei proceselor
în cauză. Se va spune în acest caz, că realizăm o alocare a resurselor fizice.
Introducerea resurselor virtuale are o consecinţă foarte importantă pe care o vom ilustra-o prin exemplul proceselor
p şi q, definite în 3.2.2.1. Să ataşăm fiecărui proces un procesor virtual. Conceptual, totul va avea loc ca şi cum
procesele s-ar derula paralel, conform unei scheme, numite logice sau virtuale, analogice schemei 3 din fig.3.2. Cu toate
acestea, trebuie de menţionat, că această schemă logică reprezintă doar o notaţie compactă pentru mulţimea schemelor
reale posibile şi că ele sunt obligatoriu de forma 1 sau 2 din considerentele unicităţii procesorului. Pentru o schemă
Excluderea mutuală constă în extinderea pentru secvenţa de acţiuni a proprietăţii de indivizibilitate a acţiunilor
nivelului de bază (acţiuni atomare). Posibilitatea executării unor acţiuni atomare se află la baza mecanismelor care
realizează sincronizarea.
aut(ps) : f:=true
A fost introdusă variabila de stare f pentru a exprima condiţia “procesul p a terminat acţiunea scriere(a)”.◄
Exemplul 3.7: var n : integer init 0;
procesul pi
<debut_i>;
n:=n+1
ps: <continuare_i>
aut(psi): n=N (i=1,...,N);
Variabila de stare n este în acest caz numărul procesului sosit în punctul de rendez-vous.◄
Problema ar fi uşor de rezolvat, dacă fiecare proces ar dispune de un ceas propriu: ar fi suficient să se introducă în contor valoarea t şi să se aştepte
întreruperea la trecerea prin zero. Realizarea primitivei suspendare cu ajutorul unui ceas unic comun presupune ataşarea fiecărui proces a unui ceas
virtual.
procesul 1 procesul 2
copie
date date
stivă stivă
procesul fiu
procesul părinte
Fig.4.4. Crearea proceselor cu ajutorul instrucţiunii fork
if (fork() == 0) if (fork() == 0)
codul procesului fiu codul procesului fiu
else else
codul procesului părinte codul procesului părinte
#include <windows.h>
#include <iostream.h>
void main()
{
DWORD res;
// eliberăm obiectul
cout<<"Acum eliberăm obiectul\n"; cout.flush();
ReleaseMutex(mutex);
}
// închidem descriptorul
CloseHandle(mutex);
}
Pentru a controla modul de funcţionare a mecanismului de excludere mutuală se vor lansa două instanţe ale acestei
aplicaţii. Prima instanţă va ocupa obiectul imediat şi-l va elibera doar peste 10 secunde. Numai după aceasta instanţa a
doua va reuşi să ocupe obiectul. În acest exemplu obiectul de sincronizare este folosit pentru sincronizarea proceselor,
din care cauză în mod obligatoriu trebuie să aibă nume.
3.6.6. Utilizarea secţiunilor critice în Windows
În acest caz secţiunile critice sunt utilizate pentru a permite la un moment de timp dat accesul la unele date
importante unui singur fir al aplicaţiei, celelalte fire fiind blocate. De exemplu, fie variabila m_pObject şi câteva fire,
care apelează metodele obiectului referit de m_pObject. Presupunem că această variabilă din timp în timp îşi poate
schimba valoarea, valoarea 0 nu este interzisă. Fie următorul fragment de cod:
// Firul #2
void Proc2(IObject *pNewObject)
{
if (m_pObject)
delete m_pObject;
m_pObject = pNewObject;
}
În acest exemplu există pericolul potenţial de apelare m_pObject->SomeMethod() după ce obiectul a fost distrus
cu ajutorul delete m_pObject, deoarece în sistemele de operare cu multitasking controlat execuţia oricărui fir poate fi
întreruptă în cel mai neconvenabil (pentru firul dat) moment şi să înceapă execuţia unui alt fir. Pentru exemplul nostru
momentul nedorit este atunci când firul #1 a testat deja m_pObject, dar nu a reuşit să apeleze SomeMethod().
Execuţia firului #1 a fost întreruptă şi a început execuţia firului #2. Iar firul #2 reuşise deja să apeleze destructorul
obiectului. Ce se va întâmpla atunci când firului #1 i se va acorda din nou timp de procesor şi va fi apelat
SomeMethod() al unui obiect deja inexistent? Greu de presupus.
Aici ne vin în ajutor secţiunile critice. Să modificăm exemplul de mai sus.
// Firul #1
void Proc1()
{
::EnterCriticalSection(&m_lockObject);
if (m_pObject)
m_pObject->SomeMethod();
::LeaveCriticalSection(&m_lockObject);
}
// Firul #2
void Proc2(IObject *pNewObject)
{
::EnterCriticalSection(&m_lockObject);
if (m_pObject)
delete m_pObject;
m_pObject = pNewObject;
::LeaveCriticalSection(&m_lockObject);
}
// Încă Firul #1
void Proc2()
{
::EnterCriticalSection(&m_lock);
// ...
::LeaveCriticalSection(&m_lock);
}
Într-adevăr, secţiunile critice sunt destinate să protejeze datele la accesarea din câteva fire. Utilizarea multiplă a
uneia şi aceeaşi secţiuni critice de un singur fir nu va genera eroare, ceea ce este normal. Trebuie doar să avem grijă ca
numărul de apeluri ::EnterCriticalSection() şi ::LeaveCriticalSection() să coincidă şi totul va fi în regulă.
Câmpul OwningThread conţine 0 în cazul secţiunilor critice libere sau identificatorul unic al firului-posesor. Acest
câmp este testat, dacă la apelul ::EnterCriticalSection() câmpul LockCount, după incrementarea cu o unitate, a
devenit mai mare ca 0. Dacă OwningThread coiincide cu identificatorul unic al firului curent, atunci valoarea lui
RecursionCount creşte cu o unitate şi ::EnterCriticalSection() este returnat imediat. În caz contrar
::EnterCriticalSection() va aştepta până firul, care posedă secţiunea critică, va apela ::LeaveCriticalSection() de un
număr necesar de ori. Câmpul LockSemaphore este folosit, dacă este necesar să se aştepte până secţiunea critică este
eliberată. Dacă LockCount este mai mare ca 0 şi OwningThread nu coiincide cu identificatorul unic al firului curent,
atunci firul blocat crează un obiect al nucleului – eveniment – şi apelează ::WaitForSingleObject(LockSemaphore).
Firul posesor, după decrementarea câmpului RecursionCount, testează acest câmp şi dacă valoarea lui este 0, iar
LockCount este mai mare ca 0, constată că există minimum un fir în aşteptarea momentului când LockSemaphore se
va afla în starea “sosit”. Pentru aceasta firul-posesor apelează ::SetEvent() şi un fir oarecare (doar unul) dintre cele
blocate este deblocat şi are acces la datele critice.
Windows NT/2000 generează o excepţie, dacă încercarea de a crea un eveniment a eşuat. Aceasta este just atât
pentru funcţiile ::Enter/LeaveCriticalSection(), cât şi pentru ::InitializeCriticalSectionAndSpinCount() cu bitul cel
mai semnificativ al parametrului SpinCount setat. În cazul sistemului de operare WindowsXP creatorii nucleului au
procedat un pic altfel. Aici, funcţiile ::Enter/LeaveCriticalSection() în loc să genereze o excepţie, dacă nu pot crea un
eveniment propriu, vor folosi un obiect global, unic pentru toţi, creat în avans. În aşa mod, în caz de deficienţă
catastrofală a resurselor de sistem, programul sub comanda lui WindowsXP “şchiopătează” un timp oarecare mai
departe. Într-adevăr, este foarte complicat să scrii programe, care ar fi în stare să continue lucrul după ce
RtlpWaitForCriticalSection(pcs);
}
pcs->OwningThread = (HANDLE)::GetCurrentThreadId();
pcs->RecursionCount = 1;
}
return TRUE;
}
class CScopeLock
{
LPCRITICAL_SECTION m_pCS;
public:
CScopeLock(LPCRITICAL_SECTION pCS) : m_pCS(pCS) { Lock(); }
CScopeLock(CLock& lock) : m_pCS(&lock.m_CS) { Lock(); }
~CScopeLock() { Unlock(); }
void Lock() { ::EnterCriticalSection(m_pCS); }
void Unlock() { ::LeaveCriticalSection(m_pCS); }
};
Clasele CLock şi CAutoLock sunt utilizate, de obicei, pentru sincronizarea accesării variabilelor clasei, iar
CScopeLock este destinat, în special, pentru a fi utilizată în proceduri. Compilatorul singur va avea grijă să apeleze
::LeaveCriticalSection() prin intermediul destructorului. Urmează un exemplu de folosire a CScopeLock.
CAutoLock m_lockObject;
CObject *m_pObject;
void Proc1()
{
CScopeLock lock(m_ lockObject); // apelarea lock.Lock();
if (!m_pObject)
return; // apelarea lock.Unlock();
m_pObject->SomeMethod();
// apelarea lock.Unlock();
}
3.6.6.4. Depanarea secţiunilor critice
Depanarea secţiunilor critice este o ocupaţie foarte interesantă, dar şi dificilă. Poţi căuta ore şi chiar zile în şir cauza
apariţiei unei probleme. Erorile, legate de secţiunile critice sunt de două tipuri: de realizare şi de arhitectură. Erorile de
realizare pot fi depistate relativ uşor şi, de regulă, sunt generate de utilizarea incorectă (lipsa perechii) a apelurilor
::EnterCriticalSection() şi ::LeaveCriticalSection(). Urmează un fragment de cod în care este omis apelul
::EnterCriticalSection().
// În procedură se presupune, că m_lockObject.Lock(); a fost deja apelat
void Pool()
{
for (int i = 0; i < m_vectSinks.size(); i++)
{
m_lockObject.Unlock();
m_vectSinks[i]->DoSomething();
m_lockObject.Lock();
}
}
Apelul ::LeaveCriticalSection() fără ::EnterCriticalSection() va conduce la faptul că chiar primul apel
::EnterCriticalSection() va stopa execuţia firului pentru totdeauna.
În fragmentul de cod de mai jos lipseşte apelul ::LeaveCriticalSection():
// Firul #2
void Proc2()
{
::EnterCriticalSection(&m_lock2);
// ...
::EnterCriticalSection(&m_lock1);
// ...
::LeaveCriticalSection(&m_lock1);
// ...
::LeaveCriticalSection(&m_lock2);
}
Pot să apară probleme şi în cazul copierii unor secţiuni critice. Este greu de presupus că codul de mai jos a fost scris
de un programator sănătos:
CRITICAL_SECTION sec1;
CRITICAL_SECTION sec2;
// ...
sec1 = sec2;
Din atribuirea de mai sus este dificil să obţii foloase. Dar fragmentul următor poate fi adesea întâlnit:
struct SData
{
CLock m_lock;
DWORD m_dwSmth;
} m_data;
// Firul #2
void IObject2::SomeMethod()
{
// Apelăm metoda obiectului #1 din firul obiectului #2
m_pObject1->Proc2();
}
// Firul #2
void IObject1::Proc2()
{
// Încercăm să intrăm în secţiunea critică a obiectului #1
m_lockObject.Lock();
// Aici nu vom ajunge niciodată
m_lockObject.Unlock();
}
Dacă în acest exemplu nu ar fi avut loc comutarea firelor, toate apelurile ar fi avut loc în firul obiectului #1 şi nu am
fi avut probleme. Exemple de acest gen stau la baza tehnologiei compartimentului COM (apartments). Nu sunt
recomandate apelurile obiectelor, dacă au avut loc intrări în secţiunile critice. Primul exemplu din acest subparagraf va
fi rescris astfel:
// Firul #1
void Proc1()
{
m_lockObject.Lock();
CComPtr<IObject> pObject(m_pObject); // apelarea pObject->AddRef();
m_lockObject.Unlock();
if (pObject)
pObject->SomeMethod();
}
B
A
Rolul monitorului sens_unic este să garanteze că toate trenurile, angajate la un moment de timp dat pe porţiunea cu
un singur drum, circulă în aceeaşi direcţie.
1) Să se elaboreze programul monitorului sens_unic, presupunând că numărul trenurilor prezente pe porţiunea cu
un singur drum, nu este limitat.
2) Aceeaşi problema, dar pentru un număr limitat N de trenuri.
3) Examinaţi în ambele cazuri riscul de blocare şi mijloacele de evitare ale acestora.
Exerciţiul 3.5. Cinci filozofi stau la o masă conform schemei de mai jos.
Filosoful 0
Filosoful 4 Filosoful 1
Filosoful 3 Filosoful 2
Fiecare, schematic prezentat printr-un proces pi, (i=0..4), se comportă conform programului:
ciclu
gândire -- nu necesită vre-o resursă
mâncare -- sunt necesare două (ambele) beţişoare (pentru orez)
endciclu
Fiecare filozof poate utiliza doar cele două furculiţe, plasate de o parte şi alta a farfuriei sale, la terminare
întorcându-le în poziţia iniţială. Durata celor două faze este arbitrară, dar finită.
1) Să se arate, că alocarea furculiţelor poate conduce la blocarea sistemului (blocarea a două sau mai multe
procese).