Differences

This shows you the differences between two versions of the page.

Link to this comparison view

so:laboratoare:laborator-09 [2019/04/22 08:45]
bogdan.purcareata [Exercițiul 6 - Barrier]
so:laboratoare:laborator-09 [2022/05/06 13:38] (current)
costin.carabas [Exercițiul 6 - Barrier]
Line 1: Line 1:
-====== Laborator 09 - Thread-uri ​Windows ======+====== Laborator 09 - Threaduri ​Windows ======
  
  
-===== Materiale ajutătoare ===== 
- 
-  *[[http://​elf.cs.pub.ro/​so/​res/​laboratoare/​lab09-slides.pdf | lab09-slides.pdf]] 
-  *[[http://​elf.cs.pub.ro/​so/​res/​laboratoare/​lab09-refcard.pdf | lab09-refcard.pdf]] 
  
 ==== Nice to read ==== ==== Nice to read ====
  
   * WSP4 - Chapter 7, Threads and Scheduling   * WSP4 - Chapter 7, Threads and Scheduling
-===== Crearea firelor de execuție ======+==== Crearea firelor de execuție ======
  
 Pentru a lansa un nou fir de execuție, există funcțiile [[http://​msdn.microsoft.com/​en-us/​library/​ms682453%28VS.85%29.aspx|CreateThread]] și [[http://​msdn.microsoft.com/​en-us/​library/​ms682437%28v=VS.85%29.aspx|CreateRemoteThread]] (a doua fiind folosită pentru a crea un fir de execuție în cadrul altui proces decât cel curent). [[https://​www.codeproject.com/​Articles/​86215/​Remote-Threads-Basics-Part-1|Aici]] găsiți mai multe informații despre remote threads. Pentru a lansa un nou fir de execuție, există funcțiile [[http://​msdn.microsoft.com/​en-us/​library/​ms682453%28VS.85%29.aspx|CreateThread]] și [[http://​msdn.microsoft.com/​en-us/​library/​ms682437%28v=VS.85%29.aspx|CreateRemoteThread]] (a doua fiind folosită pentru a crea un fir de execuție în cadrul altui proces decât cel curent). [[https://​www.codeproject.com/​Articles/​86215/​Remote-Threads-Basics-Part-1|Aici]] găsiți mai multe informații despre remote threads.
Line 344: Line 340:
 Win32 API pune la dispoziție un mecanism de acces sincronizat la variabile partajate între fire de execuție prin intermediul funcțiilor **interlocked** ([[http://​msdn.microsoft.com/​en-us/​library/​ms684122%28VS.85%29.aspx|Interlocked Variable Access]]), precum și operații atomice de inserare și ștergere în liste simplu înlănțuite ([[http://​msdn.microsoft.com/​en-us/​library/​ms684121%28VS.85%29.aspx|Interlocked Singly Linked Lists]]). Win32 API pune la dispoziție un mecanism de acces sincronizat la variabile partajate între fire de execuție prin intermediul funcțiilor **interlocked** ([[http://​msdn.microsoft.com/​en-us/​library/​ms684122%28VS.85%29.aspx|Interlocked Variable Access]]), precum și operații atomice de inserare și ștergere în liste simplu înlănțuite ([[http://​msdn.microsoft.com/​en-us/​library/​ms684121%28VS.85%29.aspx|Interlocked Singly Linked Lists]]).
 ==== Mutex Win32 ==== ==== Mutex Win32 ====
 +=== Crearea și deschiderea ===
  
-Pe scurt:+Sunt operații prin care se  obține un ''​HANDLE''​ al unui obiect de tip mutex. Este necesar doar un singur apel, fie el de creare sau de deschidere (se presupune ca alt proces a creat deja mutex-ul). 
 + 
 +Pentru a crea un mutex se folosește funcția [[http://​msdn.microsoft.com/​en-us/​library/​ms682411(VS.85).aspx|CreateMutex]] cu sintaxa : 
 + 
 +<columns 100% 50% ->
  
 <code c> <code c>
-/* creează un mutex */ +HANDLE ​ CreateMutex( 
-HANDLE CreateMutex( +     ​LPSECURITY_ATTRIBUTES ​lpAttributes
- LPSECURITY_ATTRIBUTES ​lpMutexAttributes,  +     ​BOOL bInitialOwner,​ 
- BOOL bInitialOwner,​  +     ​LPCTSTR lpName
- LPCTSTR lpName+
 ); );
-  +</​code>​ 
-/* deschide un mutex (identificat prin nume) */ + 
-HANDLE OpenMutex( +<​newcolumn>​ 
- DWORD dwDesiredAccess,  + 
- BOOL bInheritHandle +<code c> 
- LPCTSTR lpName +hMutex = CreateMutex( 
-); +     ​NULL,  ​/* default security attributes ​*/ 
-  +     FALSE/* initially not owned */ 
-/* eliberează un mutex ocupat ​*/ +     NULL /* unnamed ​mutex */
-BOOL ReleaseMutex( +
- HANDLE hMutex+
 ); );
 </​code>​ </​code>​
  
-Mai multe informaţii puteţgăsi în secţiunea dedicată  [[so:laboratoare-2013:laborator-05#​Mutex-uri|comunicației inter-proces]].+</​columns>​ 
 + 
 +Pentru a deschide un mutex deja existent este definită funcția [[http://​msdn.microsoft.com/​en-us/​library/​ms684315(VS.85).aspx|OpenMutex]] cu sintaxa : 
 + 
 +<columns 100% 50% -> 
 + 
 +<code c> 
 + ​HANDLE ​ OpenMutex( 
 +     ​DWORD ​ dwDesiredAccess,​ 
 +     ​BOOL ​ bInheritHandle,​ 
 +     ​LPCTSTR ​ lpName 
 + ); 
 +</​code>​ 
 + 
 +<​newcolumn>​ 
 + 
 +<code c> 
 +hMutex = OpenMutex( 
 +        MUTEX_ALL_ACCESS, ​ /* request full access */ 
 +        FALSE, ​            /* handle not inheritable */ 
 +        "​MyMutex" ​         /* object name */ 
 +);         
 +</​code>​ 
 +</​columns>​ 
 + 
 +=== Obținerea === 
 + 
 +Obținerea unui mutex se realizează folosind una din funcțiile de așteptare tratate anterior. 
 + 
 +Încercarea de acaparare a unui mutex presupune următorii pași
 +  * se verifică dacă mutex-ul este disponibil 
 +  * dacă da, îl pot acapara și devine indisponibil,​ și funcția întoarce succes 
 +  * dacă nu, aștept să devină disponibil, după care îl acaparez, și funcția întoarce succes 
 +  * la time-out funcția întoarce eroare (atenție: e posibil să nu existe time-out) 
 + 
 +Încercarea de obținere se poate face cu sau fară timp de expirare (time-out) în funcție de parametrii dați funcțiilor de așteptare. Cea mai des folosită funcție de așteptare este [[http://​msdn.microsoft.com/​en-us/​library/​ms687032%28VS.85%29.aspx|WaitForSingleObject]]. 
 + 
 +=== Cedarea === 
 + 
 +Folosind funcția [[http://​msdn.microsoft.com/​en-us/​library/​ms685066(VS.85).aspx|ReleaseMutex]] se cedează posesia mutex-ului, el devenind iar disponibil. Funcția are următoarea sintaxă : 
 + 
 +<code c> 
 + ​BOOL ​ ReleaseMutex( 
 +     ​HANDLE ​ hMutex 
 + ); 
 +</​code>​ 
 + 
 +Funcția va eșua dacă procesul nu deține mutex-ul. 
 + 
 +**Atenție!** pentru a putea folosi această funcție ''​HANDLE''​-ul trebuie să aibă cel puțin dreptul de acces ''​MUTEX_MODIFY_STATE''​. 
 +=== Distrugerea === 
 + 
 +Operația de **distrugere** a unui mutex este aceeași ca pentru orice ''​HANDLE''​. Se folosește funcția [[http://​msdn.microsoft.com/​en-us/​library/​ms724211%28VS.85%29.aspx|CloseHandle]]. După ce toate ''​HANDLE''​-urile unui mutex au fost închise, mutexul este distrus și resursele ocupate de acesta eliberate. 
 + 
 +**Atenție!** La terminarea execuției unui program toate ''​HANDLE''​-urile folosite de acesta sunt automat închise. Deci, spre deosebire de semafoarele IPC din Linux, este imposibil ca un mutex (sau semafor) în Windows să mai existe în sistem după ce programele care l-au folosit/​creat s-au terminat.
  
 ==== Semafor Win32 ==== ==== Semafor Win32 ====
 +Un semafor este un obiect de sincronizare care are intern un contor ce ia doar valori pozitive. Atât timp cât semaforul (contorul) are valori strict pozitive el este considerat disponibil (//​signaled//​). Când valoarea semaforului a ajuns la zero el devine indisponibil (//​nonsignaled//​) și următoarea încercare de decrementare va duce la o blocare a threadului/​procesului de pe care s-a făcut apelul până când semaforul devine disponibil.
  
-Avem următoarele funcţii:+Operația de decrementare se realizează doar cu o singură unitate (la fel ca în API-ul POSIX), în timp ce incrementarea se poate realiza cu orice valoare în limita maximă. 
 + 
 +=== Crearea și deschiderea === 
 + 
 +Funcția de creare a semafoarelor este [[http://​msdn.microsoft.com/​en-us/​library/​ms682438(VS.85).aspx|CreateSemaphore]] și are sintaxa ​:
  
 <code c> <code c>
-/* creează un semafor */ + HANDLE ​ CreateSemaphore( 
-HANDLE CreateSemaphore( +     ​LPSECURITY_ATTRIBUTES ​ ​lpSemaphoreAttributes
- LPSECURITY_ATTRIBUTES ​semattr,  +     ​LONG  ​lInitialCount
- LONG initial_count+     ​LONG  ​lMaximumCount
- LONG maximum_count,  +     ​LPCTSTR ​ ​lpNAME 
- LPCTSTR ​name + );
-); +
-  +
-/* deschide un semafor existent */ +
-HANDLE OpenSemaphore( +
- DWORD dwDesiredAccess,​  +
- BOOL bInheritHandle,​  +
- LPCTSTR name +
-); +
-  +
-/* incrementeare contor semafor cu '​lReleaseCount'​ */ +
-BOOL ReleaseSemaphore( +
- HANDLE hSemaphore,  +
- LONG lReleaseCount,​  +
- LPLONG lpPreviousCount +
-);+
 </​code>​ </​code>​
 +
 +Această funcție se poate folosi și pentru deschiderea unui semafor deja existent. Alternativ, pentru a folosi un semafor deja existent, este necesar obținerea ''​HANDLE''​-ului semaforului,​ operație ce se realizează folosind funcția [[http://​msdn.microsoft.com/​en-us/​library/​ms684326.aspx|OpenSemaphore]] cu următoarea sintaxă :
 +
 +<code c>
 + ​HANDLE ​ OpenSemaphore(
 +     ​DWORD ​ dwDesiredAccess,​
 +     ​BOOL ​ bInheritHandle,​
 +     ​LPCTSTR ​ lpNAME
 + );
 +</​code>​
 +
 +=== Decrementarea (așteptarea) ===
 +
 +Operația de decrementare a semaforului cu sau fără așteptare se realizează folosind una din funcțiile de așteptare. Cea mai des folosită este funcția [[http://​msdn.microsoft.com/​en-us/​library/​ms687032(VS.85).aspx|WaitForSingleObject]].
 +
 +===  Incrementarea ===
 +
 +Incrementarea semaforului se realizează folosind funcția [[http://​msdn.microsoft.com/​en-us/​library/​ms685071(VS.85).aspx|ReleaseSemaphore]] cu sintaxa :
 +
 +<code c>
 + ​BOOL ​ ReleaseSemaphore(
 +     ​HANDLE ​ hSemaphore,
 +     ​LONG ​ lReleaseCount,​
 +     ​LPLONG ​ lpPreviousCount
 + );
 +</​code>​
 +
 +=== Distrugerea ===
 +
 +Operația de distrugere a unui semafor este similară cu cea de distrugere a unui mutex. Se folosește funcția [[http://​msdn.microsoft.com/​en-us/​library/​ms724211%28VS.85%29.aspx|CloseHandle]]. După ce toate ''​HANDLE''​-urile unui semafor au fost închise, semaforul este distrus și resursele ocupate de acesta eliberate.
 +
 ==== Secțiune critică ==== ==== Secțiune critică ====
  
Line 616: Line 689:
 ==== Adăugarea de taskuri la thread pool ==== ==== Adăugarea de taskuri la thread pool ====
  
-=== Așteptarea unei operații de intrare/​ieșire asincrone ​=== +<​spoiler>​ 
 +**Așteptarea unei operații de intrare/​ieșire asincrone**\\
 Pentru a adăuga la //thread pool// un task care se va executa la finalul unei operații de intrare/​ieșire asincrone pe un anumit //file handle//, se va apela funcția: Pentru a adăuga la //thread pool// un task care se va executa la finalul unei operații de intrare/​ieșire asincrone pe un anumit //file handle//, se va apela funcția:
  
Line 640: Line 713:
 </​code>​ </​code>​
  
-=== Adăugarea unui task pentru execuție imediată ​=== +**Adăugarea unui task pentru execuție imediată**\\
 Pentru a adăuga la //thread pool// un task care să fie executat imediat se va apela funcția: Pentru a adăuga la //thread pool// un task care să fie executat imediat se va apela funcția:
  
Line 655: Line 727:
 ); );
 </​code>​ </​code>​
 +</​spoiler>​
 ==== Timer Queues ==== ==== Timer Queues ====
  
Line 731: Line 804:
 </​code>​ </​code>​
 ==== Registered Wait Functions ==== ==== Registered Wait Functions ====
 +<​spoiler>​
 Funcțiile de așteptare înregistrate sunt funcții de așteptare executate de un fir de execuție din //thread pool//. În momentul în care obiectul de sincronizare după care se așteaptă trece în starea //​signaled//,​ se va executa rutina //​callback//​ asociată funcției de așteptare înregistrate,​ de un fir de execuție din //thread pool//. În mod implicit, funcțiile de așteptare înregistrate se **rearmează automat** și rutinele //​callback//​ sunt executate de fiecare dată când obiectul de sincronizare după care se așteaptă trece în starea **//​signaled//​**,​ sau intervalul de timeout **expiră**. Acest lucru se repetă până când înregistrarea funcției de așteptare este anulată. Se poate seta, însă, ca funcția de așteptare înregistrată să se execute **o singură dată**. Funcțiile de așteptare înregistrate sunt funcții de așteptare executate de un fir de execuție din //thread pool//. În momentul în care obiectul de sincronizare după care se așteaptă trece în starea //​signaled//,​ se va executa rutina //​callback//​ asociată funcției de așteptare înregistrate,​ de un fir de execuție din //thread pool//. În mod implicit, funcțiile de așteptare înregistrate se **rearmează automat** și rutinele //​callback//​ sunt executate de fiecare dată când obiectul de sincronizare după care se așteaptă trece în starea **//​signaled//​**,​ sau intervalul de timeout **expiră**. Acest lucru se repetă până când înregistrarea funcției de așteptare este anulată. Se poate seta, însă, ca funcția de așteptare înregistrată să se execute **o singură dată**.
  
-=== Înregistrarea unei funcții de așteptare ​=== +**Înregistrarea unei funcții de așteptare**\\
 Pentru înregistrarea în //thread pool// a unei funcții de așteptare se va apela funcția: Pentru înregistrarea în //thread pool// a unei funcții de așteptare se va apela funcția:
  
Line 762: Line 834:
 Prin intermediul parametrului ''​dwFlags''​ se pot transmite caracteristici ale firului de execuție care va executa rutina ''​Callback'',​ precum și dacă funcția de așteptare trebuie să se execute doar o singură dată. Funcția va întoarce, prin parametrul ''​phNewWaitObject'',​ un //handle// ce va fi folosit pentru deînregistrarea funcției de așteptare. Prin intermediul parametrului ''​dwFlags''​ se pot transmite caracteristici ale firului de execuție care va executa rutina ''​Callback'',​ precum și dacă funcția de așteptare trebuie să se execute doar o singură dată. Funcția va întoarce, prin parametrul ''​phNewWaitObject'',​ un //handle// ce va fi folosit pentru deînregistrarea funcției de așteptare.
  
-=== Deînregistrarea unei funcții de așteptare ​=== +**Deînregistrarea unei funcții de așteptare**\\
 Pentru a anula înregistrarea unei funcții de așteptare se va apela una dintre funcțiile: Pentru a anula înregistrarea unei funcții de așteptare se va apela una dintre funcțiile:
  
Line 774: Line 845:
  
 Funcția ''​UnregisterWaitEx''​ va semnaliza //​event//​-ul ''​CompletionEvent''​ în cazul în care se termină cu succes și rutina de //​callback//​ s-a terminat cu succes. Dacă valoarea lui ''​CompletionEvent''​ nu este ''​NULL'',​ atunci funcția va aștepta finalizarea operației de așteptare și terminarea rutinei asociate. ​ Funcția ''​UnregisterWaitEx''​ va semnaliza //​event//​-ul ''​CompletionEvent''​ în cazul în care se termină cu succes și rutina de //​callback//​ s-a terminat cu succes. Dacă valoarea lui ''​CompletionEvent''​ nu este ''​NULL'',​ atunci funcția va aștepta finalizarea operației de așteptare și terminarea rutinei asociate. ​
 +</​spoiler>​
 +====== Sumar ======
 +^  Operație ​ ^  POSIX  ^  Windows ​ ^
 +|  Crearea unui fir de execuție ​ |  ''​pthread_create'' ​ |  [[#​crearea_firelor_de_executie|CreateThread]] ​ |
 +|  Așteptarea unui fir de execuție ​ |  ''​pthread_join'' ​ |  [[#​asteptarea_firelor_de_executie|WaitForSingleObject]] ​ |
 +|  Crearea unui mutex  |  ''​pthread_mutex_init'' ​ |  [[#​crearea_si_deschiderea|CreateMutex]] ​ |
 +|  Obținerea unui mutex  |  ''​pthread_mutex_lock'' ​ |  [[#​obtinerea|WaitForSingleObject]] ​ |
 +|  Cedarea unui mutex  |  ''​pthread_mutex_unlock'' ​ |  [[#​cedarea|ReleaseMutex]] ​ |
 +|  Distrugerea unui mutex  |  ''​pthread_mutex_destroy'' ​ |  [[#​distrugerea|CloseHandle]] ​ |
 +|  Crearea unui semafor ​ |  ''​sem_init''/''​sem_open'' ​ |  [[#​crearea_si_deschiderea1|CreateSemaphore]] ​ |
 +|  Decrementarea unui semafor ​ |  ''​sem_wait'' ​ |  [[#​decrementarea_asteptarea|WaitForSingleObject]] ​ |
 +|  Incrementarea unui semafor ​ |  ''​sem_post'' ​ |  [[#​incrementarea|ReleaseSemaphore]] ​ |
 +|  Distrugerea unui semafor ​ |  ''​sem_destroy''/''​sem_close'' ​ |  [[#​distrugerea1|CloseHandle]] ​ |
 +|  Thread Local Storage ​ |  [[https://​gcc.gnu.org/​onlinedocs/​gcc/​Thread-Local.html|__thread]],​ [[https://​linux.die.net/​man/​3/​pthread_key_create|pthread_key_create]],​ [[https://​linux.die.net/​man/​3/​pthread_setspecific|pthread_setspecific]],​ [[https://​linux.die.net/​man/​3/​pthread_getspecific|pthread_getspecific]],​ [[https://​linux.die.net/​man/​3/​pthread_key_delete|pthread_key_delete]] ​ |  [[#​thread_local_storage|TlsAlloc,​ TlsSetValue,​ TlsGetValue, ​ TlsFree]] ​ |
  
-====== Exerciții de laborator ====== 
  
-===== Exercițiul 0 - Joc interactiv ​=====+====== Exerciții ======
  
-  * Detalii desfășurare [[http://ocw.cs.pub.ro/courses/so/meta/notare#​joc_interactiv|joc]].+<note important>​ 
 +În cadrul laboratoarelor vom folosi repository-ul de git al materiei SO - https://github.com/​systems-cs-pub-ro/so. Va trebui sa clonați repository-ul pe masinile virtuale folosind comanda: ''​git clone https://​github.com/​systems-cs-pub-ro/so''​. Dacă doriți să descărcați repositoryul în altă locație, folosiți comanda ''​git clone https://github.com/​systems-cs-pub-ro/​so ${target}''​.
  
-===== Windows =====+Pentru a actualiza repository-ul,​ folosiți comanda ''​git pull origin master''​ din interiorul directorului în care se află repository-ul. Recomandarea este să îl actualizați cât mai frecvent, înainte să începeți lucrul, pentru a vă asigura că aveți versiunea cea mai recentă. În cazul în care gitul detectează conflicte la nivelul vreunui fişier, folosiți următoarele comenzi pentru a vă păstra modificările:​ 
 +<​code>​ 
 +git stash 
 +git pull origin master 
 +git stash pop 
 +</​code>​
  
-În rezolvarea laboratorului folosiți arhiva ​de sarcini [[http://elf.cs.pub.ro/so/​res/​laboratoare/​lab09-tasks.zip | lab09-tasks.zip]]+Pentru mai multe informații despre folosirea utilitarului git, urmați ghidul ​de la https://gitimmersion.com. 
 +</note> 
 + 
 +===== Windows =====
  
 <note tip> Pentru a deschide proiectul Visual Studio conținând exercițiile,​ deschideți fișierul lab09.sln. </​note>​ <note tip> Pentru a deschide proiectul Visual Studio conținând exercițiile,​ deschideți fișierul lab09.sln. </​note>​
Line 811: Line 904:
 În cadrul acestui exercițiu dorim să testăm diverse tipuri de incrementări atomice ale unei variabile, comparându-le timpul de execuție. Deschideți sursa ''​interlocked.c''​ din proiectul ''​3-interlocked''​. Programul crează ''​NO_THREADS''​ fire de execuție, care incrementează circular o variabilă (când se ajunge la o limită se resetează la 0). În cadrul acestui exercițiu dorim să testăm diverse tipuri de incrementări atomice ale unei variabile, comparându-le timpul de execuție. Deschideți sursa ''​interlocked.c''​ din proiectul ''​3-interlocked''​. Programul crează ''​NO_THREADS''​ fire de execuție, care incrementează circular o variabilă (când se ajunge la o limită se resetează la 0).
  
-Asigurați accesul exclusiv la variabila incrementată folosind [[#operatii atomice cu variabile partajate interlocked variable access | Interlocked Variables]] deoarece mecanismul e mai rapid decât o incrementare normală protejată cu ''​Mutex''​ sau ''​CRITICAL_SECTION''​ (folosiți funcția ''​InterlockedCompareExchange''​). Incrementarea circulară se va face în funcția ''​thread_function''​ (urmăriți comentariile cu // TODO 1 //). Veți avea nevoie de două operații interlocked (''​InterlockedIncrement''​ și ''​InterlockedCompareExchange''​).+Asigurați accesul exclusiv la variabila incrementată folosind [[#operatii atomice cu variabile partajate interlocked variable access | Interlocked Variables]] deoarece mecanismul e mai rapid decât o incrementare normală protejată cu ''​Mutex''​ sau ''​CRITICAL_SECTION''​ (folosiți funcția ''​InterlockedCompareExchange''​). Incrementarea circulară se va face în funcția ''​thread_function''​ (urmăriți comentariile cu //TODO 1//). Veți avea nevoie de două operații interlocked (''​InterlockedIncrement''​ și ''​InterlockedCompareExchange''​).
  
 Identificați o problemă cu folosirea ''​Interlocked Operations''​ pentru a incrementa circular o variabilă. Identificați o problemă cu folosirea ''​Interlocked Operations''​ pentru a incrementa circular o variabilă.
Line 839: Line 932:
 typedef struct { typedef struct {
  HANDLE hGuard;​  ​   /* mutex to protect internal variable access */  HANDLE hGuard;​  ​   /* mutex to protect internal variable access */
- HANDLE hEvent;​  ​   /* auto-resetable event */+ HANDLE hEvent;​  ​   /* manual ​resetable event */
  DWORD dwCount;​  ​   /* number of threads to have reached the barrier */  DWORD dwCount;​  ​   /* number of threads to have reached the barrier */
  DWORD dwThreshold; ​ /* barrier limit */  DWORD dwThreshold; ​ /* barrier limit */
Line 881: Line 974:
     * Implementați vizualizarea unui timer; spre exemplu, un fir de execuție care alternează două operații: draw și sleep. (desenează,​ se oprește, desenează, iar se oprește și tot așa; intervalul unei operații poate fi același)     * Implementați vizualizarea unui timer; spre exemplu, un fir de execuție care alternează două operații: draw și sleep. (desenează,​ se oprește, desenează, iar se oprește și tot așa; intervalul unei operații poate fi același)
 */ */
-===== Soluții ===== 
- 
- ​[[http://​elf.cs.pub.ro/​so/​res/​laboratoare/​lab09-sol.zip | Soluţii laborator 9]] 
  
-~~NOCACHE~~ 
so/laboratoare/laborator-09.txt · Last modified: 2022/05/06 13:38 by costin.carabas
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0