This shows you the differences between two versions of the page.
sde:laboratoare:08_ro_python [2020/04/07 19:35] ioana_maria.culic |
— (current) | ||
---|---|---|---|
Line 1: | Line 1: | ||
- | ====== Laborator 8 - Thread-uri Linux ====== | ||
- | ===== Materiale ajutătoare ===== | ||
- | *[[http://elf.cs.pub.ro/so/res/laboratoare/lab08-slides.pdf | lab08-slides.pdf]] | ||
- | *[[http://elf.cs.pub.ro/so/res/laboratoare/lab08-refcard.pdf | lab08-refcard.pdf]] | ||
- | |||
- | ==== Nice to read ==== | ||
- | |||
- | * TLPI - Chapter 29, Threads: Introduction | ||
- | * TLPI - Chapter 30, Threads: Thread Synchronization | ||
- | * TLPI - Chapter 31, Threads: Thread Safety and Per-Thread Storage | ||
- | ===== Prezentare teoretică ===== | ||
- | |||
- | În laboratoarele anterioare a fost prezentat conceptul de **proces**, acesta fiind unitatea elementară de alocare a resurselor utilizatorilor. În cadrul acestui laborator este prezentat conceptul de **fir de execuție** (sau **thread**), acesta fiind unitatea elementară de planificare într-un sistem. Ca și procesele, firele de execuție reprezintă un mecanism prin care un calculator poate sǎ ruleze mai multe task-uri simultan. | ||
- | |||
- | Un fir de execuție există în cadrul unui proces, și reprezintă o unitate de execuție mai fină decât acesta. În momentul în care un proces este creat, în cadrul lui există un singur fir de execuție, care execută programul secvențial. Acest fir poate la rândul lui sǎ creeze alte fire de execuție; aceste fire vor rula porțiuni ale binarului asociat cu procesul curent, posibil aceleași cu firul inițial (care le-a creat). | ||
- | ==== Diferențe dintre fire de execuție și procese ==== | ||
- | |||
- | *procesele nu partajează resurse între ele (decât dacă programatorul folosește un mecanism special pentru asta - shared memory spre exemplu), pe când firele de execuție partajează în mod implicit majoritatea resurselor unui proces. Modificarea unei astfel de resurse dintr-un fir este vizibilă instantaneu și din celelalte fire: | ||
- | *segmentele de memorie precum ''.heap'', ''.data'' și ''.bss'' (deci și variabilele stocate în ele) | ||
- | *descriptorii de fișiere (așadar, închiderea unui fișier este vizibilă imediat pentru toate firele de execuție), indiferent de tipul fișierului: | ||
- | *sockeți | ||
- | *fișiere normale | ||
- | *pipe-uri | ||
- | *fișiere ce reprezintă dispozitive hardware (de ex. ''/dev/sda1''). | ||
- | *fiecare fir are un context de execuție propriu, format din: | ||
- | *stivă | ||
- | *set de registre (deci și un contor de program - registrul ''(E)IP'') | ||
- | |||
- | Procesele sunt folosite de SO pentru a grupa și aloca resurse, iar firele de execuție pentru a planifica execuția de cod care accesează (în mod partajat) aceste resurse. | ||
- | ==== Avantajele firelor de execuție ==== | ||
- | |||
- | Deoarece toate firele de execuție ale unui proces folosesc spațiul de adrese al procesului de care aparțin, folosirea lor are o serie de avantaje: | ||
- | *crearea/distrugerea unui fir de execuție durează mai puțin decât crearea/distrugerea unui proces | ||
- | *durata context switch-ului între firele de execuție aceluiași proces este foarte mică, întrucât nu e necesar să se "comute" și spațiul de adrese (pentru mai multe informații, căutați „TLB flush”) | ||
- | *comunicarea între firele de execuție are un overhead mai mic (realizată prin modificarea unor zone de memorie din spațiul comun de adrese) | ||
- | |||
- | Firele de execuție se pot dovedi utile în multe situații, de exemplu, pentru a îmbunătăți timpul de răspuns al aplicațiilor cu interfețe grafice (GUI), unde prelucrările CPU-intensive se fac de obicei într-un fir de execuție diferit de cel care afișează interfața. | ||
- | |||
- | De asemenea, ele simplifică structura unui program și conduc la utilizarea unui număr mai mic de resurse (pentru că nu mai este nevoie de diversele forme de IPC pentru a comunica). | ||
- | ==== Tipuri de fire de execuție ==== | ||
- | |||
- | Din punctul de vedere al implementării, există 3 categorii de fire de execuție: | ||
- | *Kernel Level Threads (KLT) | ||
- | *User Level Threads (ULT) | ||
- | *Fire de execuție hibride | ||
- | |||
- | <spoiler Detalii despre categoriile de fire de execuţie> | ||
- | |||
- | ** Kernel Level Threads ** | ||
- | |||
- | Managementul și planificarea firelor de execuție sunt realizate în kernel; programele creează/distrug fire de execuție prin apeluri de sistem. Kernel-ul menține informații de context, atât pentru procese, cât și pentru firele de execuție din cadrul proceselor, iar planificarea execuției se face la nivel de fir. | ||
- | |||
- | __Avantaje__ : | ||
- | *dacă avem mai multe procesoare putem lansa în execuție simultană mai multe fire de execuție ale aceluiași proces; | ||
- | *blocarea unui fir nu înseamnă blocarea întregului proces; | ||
- | *putem scrie cod în kernel care să se bazeze pe fire de execuție. | ||
- | |||
- | __Dezavantaje__ : | ||
- | *comutarea contextului este efectuată de kernel (cu o viteză de comutare mai mică): | ||
- | *se trece dintr-un fir de execuție în kernel | ||
- | *kernelul întoarce controlul unui alt fir de execuție. | ||
- | |||
- | ** User Level Threads ** | ||
- | |||
- | Kernel-ul nu este conștient de existența firelor de execuție, iar managementul acestora este realizat de procesul în care ele există (implementarea managementului firelor de execuție este realizată de obicei în biblioteci). Schimbarea contextului nu necesită intervenția kernel-ului, iar algoritmul de planificare depinde de aplicație. | ||
- | |||
- | __Avantaje__ : | ||
- | *schimbarea de context nu implică kernelul ⇒ comutare rapidă | ||
- | *planificarea poate fi aleasă de aplicație; aplicația poate folosi acea planificare care favorizează creșterea performanțelor | ||
- | *firele de execuție pot rula pe orice SO, inclusiv pe SO-uri care nu suportă fire de execuție la nivel kernel (au nevoie doar de biblioteca care implementează firele de execuție la nivel utilizator). | ||
- | |||
- | __Dezavantaje__ : | ||
- | *kernel-ul nu știe de fire de execuție ⇒ dacă un fir de execuție face un apel blocant toate firele de execuție planificate de aplicație vor fi blocate. Acest lucru poate fi un impediment întrucât majoritatea apelurilor de sistem sunt blocante. O soluție este utilizarea unor variante non-blocante pentru apelurile de sistem. | ||
- | *nu se pot utiliza la maximum resursele hardware: kernelul planifică firele de execuție de care știe, câte unul pe fiecare procesor. Kernelul nu este conștient de existența firelor de execuție user-level ⇒ el va vedea un singur fir de execuție ⇒ va planifica procesul respectiv pe maximum un procesor, chiar dacă aplicația ar avea mai multe fire de execuție planificabile în același timp. | ||
- | |||
- | ** Fire de execuție hibride ** | ||
- | |||
- | Aceste fire încearcă să combine avantajele firelor de execuție user-level cu cele ale firelor de execuție kernel-level. O modalitate de a face acest lucru este de a utiliza fire kernel-level pe care să fie multiplexate fire user-level. KLT sunt unitățile elementare care pot fi distribuite pe procesoare. De regulă, crearea firelor de execuție se face în user space și tot aici se face aproape toată planificarea și sincronizarea. Kernel-ul știe doar de KLT-urile pe care sunt multiplexate ULT, și doar pe acestea le planifică. Programatorul poate schimba eventual numărul de KLT alocate unui proces. | ||
- | |||
- | </spoiler> | ||
- | ===== Modulul threading ===== | ||
- | |||
- | În ceea ce privește firele de execuție, POSIX nu specifică dacă acestea trebuie implementate în user-space sau kernel-space. Linux le implementează în kernel-space, dar nu diferențiază firele de execuție de procese decât prin faptul că firele de execuție partajează spațiul de adresă (atât firele de execuție, cât și procesele, sunt un caz particular de "task"). Pentru folosirea firelor de execuție în Python trebuie să includem modulul [[https://docs.python.org/3/library/threading.html|threading]]. | ||
- | ==== Crearea firelor de execuție ==== | ||
- | |||
- | Modulul ''threading'' expune clasa ''Thread''. Astfel, un fir de execuție este creat prin instantierea clasei: | ||
- | |||
- | <code python> | ||
- | import threading | ||
- | threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None) | ||
- | </code> | ||
- | * group - se foloseste la extinderea clasei Thread; se recomanda pastrarea valoarei ''None''; | ||
- | * target - functia care va fi apelata la rularea threadului; | ||
- | * name - numele threadului; daca acesta nu este specificat, se va genera un nume de forma''Thread-N'', unde e un numar; | ||
- | * args - lista de parametrii care va fi pasata functiei ''target''; parametrii vor fi pasati in ordinea in care functia ii primeste; | ||
- | * kwargs - dictionar care va stoca parametrii pasati functiei ''target'', sub forma numele parametru-valoare; | ||
- | * daemon - specifică dacă threadul este de tip daemon; dacă valoarea este None, proprietatea va fi moștenită de la threadul curent. | ||
- | |||
- | Noul fir creat poate fi lansat prin apelarea functiei [[https://docs.python.org/3/library/threading.html#threading.Thread.start|start()]]. Acesta va executa codul specificat de funcția ''target'' căreia i se vor pasa argumentele din ''args'' sau ''kwargs''. | ||
- | |||
- | Pentru a determina firului de execuție curent se poate folosi funcția [[https://docs.python.org/3/library/threading.html#threading.current_thread|current_thread]]: | ||
- | |||
- | <code python> | ||
- | import threading | ||
- | threading.current_thread() | ||
- | </code> | ||
- | |||
- | ==== Așteptarea firelor de execuție ==== | ||
- | |||
- | Firele de execuție se așteaptă folosind funcția [[https://docs.python.org/3/library/threading.html#threading.Thread.join|join]]: | ||
- | |||
- | <code python> | ||
- | t = threading.Thread() | ||
- | t.join(timeout=None) | ||
- | </code> | ||
- | |||
- | Odata ce un thread a facut join pe un altul, acesta se va bloca pana threadul pe care s-a facut join isi va incheia executia. | ||
- | Daca threadul pe care s-a facut join va arunca o exceptie de tipul [[https://docs.python.org/3/library/threading.html#threading.excepthook|excepthook]] va fi aruncata in threadul care a facut join. | ||
- | |||
- | <note info> | ||
- | Este recomandat ca programul principal să apeleze întotdeauna funcția ''join'' pentru threadurile pe care le generează. | ||
- | </note> | ||
- | |||
- | ==== Fire de execuție de tip daemon ==== | ||
- | Un thread de tip daemon are drept scop procesarea unor operații fară a impacta firul de execuție principal. | ||
- | |||
- | Proprietatea principală a acestor threaduri este că programul principal își va încheia execuția dacă rămân doar threaduri de acest tip. | ||
- | |||
- | <note warning> | ||
- | Deși este o operație legală, se recomandă să nu folosiți funcția ''join'' pe un thread de tip daemon. | ||
- | <note> | ||
- | ==== Terminarea firelor de execuție ==== | ||
- | |||
- | Un fir de execuție își încheie execuția în mod automat, la sfârșitul codului firului de execuție. | ||
- | |||
- | ==== Interacțiunea cu firele de execuție ==== | ||
- | |||
- | Pentru a schimba informații între programul principal si alte threaduri, vom folosi modulul [[https://docs.python.org/3/library/queue.html|queue]]. Folosind acest modul, putem implementa o coadă în care vom plasa mesajele din partea firelor de execuție. | ||
- | |||
- | Modululul suportă crearea a 6 tipuri diferite de cozi: | ||
- | * Queue - coadă de tip FIFO, primul element inserat în coadă va fi primul extras; | ||
- | * LifoQueue - ultimul element inserat în coadă va fi primul extras; | ||
- | * PriorityQueue - Fiecare element are o prioritate, pe baza căreia se alege care element va fi cel extras; | ||
- | * SimpleQueue - coadă de tip FIFO cu funcționalități limitate; | ||
- | * Empty - se aruncă o excepție dacă se încearcă extragerea unui element când coada e goală; | ||
- | * Full - se aruncă o excepție dacă se încearcă adăugarea unui element când coada e plină. | ||
- | |||
- | <note info> | ||
- | În continuare vom discuta doar despre primele 3 tipuri de cozi, la restul aplicându-se alte operații. Pentru mai multe detalii despre restul claselor din modulul ''queue'', vă recomandăm citirea documentației. | ||
- | </note> | ||
- | |||
- | Pentru a crea o coadă, trebuie să instanțiem una din clasele expuse de modulul ''queue'': | ||
- | <code python> | ||
- | import queue | ||
- | |||
- | fifo_queue = queue.Queue() | ||
- | lifo_queue = queue.LifoQueue() | ||
- | priority_queue = queue.PriorityQueue() | ||
- | </code> | ||
- | |||
- | Funcțiile [[https://docs.python.org/3/library/queue.html#queue.Queue.put|put]] și [[https://docs.python.org/3/library/queue.html#queue.Queue.put_nowait|put_nowait]] adaugă elemente în coadă. | ||
- | <code python> | ||
- | q.put (item, block=True, timeout=None) | ||
- | q.put_nowait (item) | ||
- | </code> | ||
- | |||
- | Funcțiile [[https://docs.python.org/3/library/queue.html#queue.Queue.get|get]] și [[https://docs.python.org/3/library/queue.html#queue.Queue.get_nowait|get_nowait]] extrag elemente în coadă. | ||
- | <code python> | ||
- | q.get (block=True, timeout=None) | ||
- | q.get_nowait () | ||
- | </code> | ||
- | |||
- | Dacă coada este goală la apelarea uneia din cele două funcții, o excepție de tipul [[https://docs.python.org/3/library/queue.html#queue.Empty|Empty]] este generată. | ||
- | |||
- | ===== Exemplu ===== | ||
- | Mai jos vom crea un program care generează un thread ce primește valori de la programul principal și răspunde la aceastea. | ||
- | |||
- | TODO - primeste hello sau goodbye si raspunde cu SDE sau class. | ||
- | ===== Așteptarea cozilor ===== | ||
- | |||
- | Similar cu așteptarea firelor de execuție, putem să apelăm funcția [[https://docs.python.org/3/library/queue.html#queue.Queue.join|join]] pe o coadă, pentru a opri execuția pogramului până toate elementele din coadă au fost procesate. | ||
- | |||
- | Funcția ''join'' trebuie apelată în paralel cu funcția [[https://docs.python.org/3/library/queue.html#queue.Queue.task_done| task_done]]. Fiecare thread va apela această funcție pentru a semnala terminarea procesării. | ||
- | |||
- | ==== Thread Specific Data (TSD) ==== | ||
- | |||
- | Uneori este util ca o variabilă să fie specifică unui fir de execuție (invizibilă pentru celelalte fire). Linux permite memorarea de perechi (cheie, valoare) într-o zonă special desemnată din stiva fiecărui fir de execuție al procesului curent. Cheia are același rol pe care îl are numele unei variabile: desemnează locația de memorie la care se află valoarea. | ||
- | |||
- | Fiecare fir de execuție va avea propria copie a unei "variabile" corespunzătoare unei chei ''k'', pe care o poate modifica, fără ca acest lucru să fie observat de celelalte fire, sau să necesite sincronizare. De aceea, TSD este folosită uneori pentru a optimiza operațiile care necesită multă sincronizare între fire de execuție: fiecare fir calculează informația specifică, și există un singur pas de sincronizare la sfârșit, necesar pentru reunirea rezultatelor tuturor firelor de execuție. | ||
- | |||
- | Cheile sunt de tipul ''pthread_key_t'', iar valorile asociate cu ele, de tipul generic ''void *'' (pointeri către locația de pe stivă unde este memorată variabila respectivă). Descriem în continuare operațiile disponibile cu variabilele din TSD: | ||
- | |||
- | === Crearea și ștergerea unei variabile === | ||
- | |||
- | O variabilă se creează folosind [[http://linux.die.net/man/3/pthread_key_create|pthread_key_create]]: | ||
- | |||
- | <code c> | ||
- | int pthread_key_create(pthread_key_t *key, void (*destr_function) (void *)); | ||
- | </code> | ||
- | |||
- | Al doilea parametru reprezintă o funcție de cleanup. Acesta poate avea una din valorile: | ||
- | *''NULL'' și este ignorat | ||
- | *pointer către o funcție de cleanup care se execută la terminarea firului de execuție | ||
- | |||
- | Pentru ștergerea unei variabile se apelează [[http://linux.die.net/man/3/pthread_key_delete|pthread_key_delete]]: | ||
- | |||
- | <code c> | ||
- | int pthread_key_delete(pthread_key_t key); | ||
- | </code> | ||
- | |||
- | Funcția nu apelează funcția de cleanup asociată variabilei. | ||
- | |||
- | === Modificarea și citirea unei variabile === | ||
- | |||
- | După crearea cheii, fiecare fir de execuție poate modifica propria copie a variabilei asociate folosind funcția [[http://linux.die.net/man/3/pthread_setspecific|pthread_setspecific]]: | ||
- | |||
- | <code c> | ||
- | int pthread_setspecific(pthread_key_t key, const void *pointer); | ||
- | </code> | ||
- | |||
- | Pentru a determina valoarea unei variabile de tip TSD se folosește funcția [[http://linux.die.net/man/3/pthread_getspecific|pthread_getspecific]]: | ||
- | |||
- | <code c> | ||
- | void* pthread_getspecific(pthread_key_t key); | ||
- | </code> | ||
- | ==== Funcții pentru cleanup ==== | ||
- | |||
- | Funcțiile de cleanup asociate TSD-urilor pot fi foarte utile pentru a asigura faptul că resursele sunt eliberate atunci când un fir se termină singur sau este terminat de către un alt fir. Uneori poate fi util să se poată specifica astfel de funcții fără a crea neapărat un TSD. Pentru acest scop există funcțiile de cleanup. | ||
- | |||
- | <spoiler Detalii despre funcţii de cleanup> | ||
- | O astfel de funcție de cleanup este o funcție care este apelată când un fir de execuție se termină. Ea primește un singur parametru de tipul ''void *'' care este specificat la înregistrarea funcției. | ||
- | |||
- | O funcție de cleanup este folosită pentru a elibera o resursă numai în cazul în care un fir de execuție apelează [[http://linux.die.net/man/3/pthread_exit|pthread_exit]] sau este terminat de un alt fir folosind [[http://linux.die.net/man/3/pthread_cancel|pthread_cancel]]. În circumstanțe normale, atunci când un fir nu se termină în mod forțat, resursa trebuie eliberată explicit, iar funcția de cleanup nu trebuie să fie apelată. | ||
- | |||
- | Pentru a înregistra o astfel de funcție de cleanup se folosește : | ||
- | |||
- | <code c> | ||
- | void pthread_cleanup_push(void (*routine) (void *), void *arg); | ||
- | </code> | ||
- | |||
- | Aceasta funcție primește ca parametri un pointer la funcția care este înregistrată și valoarea argumentului care va fi transmis acesteia. Funcția ''routine'' va fi apelată cu argumentul ''arg'' atunci când firul este terminat forțat. Daca sunt înregistrate mai multe funcții de cleanup, ele vor fi apelate în ordine LIFO (cea mai recent instalată va fi prima apelată). | ||
- | |||
- | Pentru fiecare apel [[http://linux.die.net/man/3/pthread_cleanup_push|pthread_cleanup_push]] trebuie să existe și apelul corespunzător [[http://linux.die.net/man/3/pthread_cleanup_pop|pthread_cleanup_pop]] care deînregistrează o funcție de cleanup: | ||
- | |||
- | <code c> | ||
- | void pthread_cleanup_pop(int execute); | ||
- | </code> | ||
- | |||
- | Această funcție va deînregistra cea mai recent instalată funcție de cleanup, și dacă parametrul ''execute'' este nenul o va și executa. | ||
- | |||
- | **Atentie!** Un apel [[http://linux.die.net/man/3/pthread_cleanup_push|pthread_cleanup_push]] trebuie să aibă un apel corespunzător [[http://linux.die.net/man/3/pthread_cleanup_pop|pthread_cleanup_pop]] în __aceeași funcție__ și la __același nivel de imbricare__. | ||
- | |||
- | Un mic exemplu de folosire a funcțiilor de cleanup : | ||
- | |||
- | <code c th_cleanup.c> | ||
- | |||
- | void *alocare_buffer(int size) | ||
- | { | ||
- | return malloc(size); | ||
- | } | ||
- | |||
- | void dealocare_buffer(void *buffer) | ||
- | { | ||
- | free(buffer); | ||
- | } | ||
- | |||
- | /* functia apelata de un fir de execuție */ | ||
- | |||
- | void functie() | ||
- | { | ||
- | void *buffer = alocare_buffer(512); | ||
- | |||
- | /* înregistrarea funcției de cleanup */ | ||
- | pthread_cleanup_push(dealocare_buffer, buffer); | ||
- | |||
- | /* aici au loc prelucrari, și se poate apela pthread_exit | ||
- | sau firul poate fi terminat de un alt fir */ | ||
- | |||
- | /* deînregistrarea functiei de cleanup și execuția ei | ||
- | (parametrul dat este nenul) */ | ||
- | |||
- | pthread_cleanup_pop(1); | ||
- | } | ||
- | </code> | ||
- | |||
- | </spoiler> | ||
- | ==== Atributele unui fir de execuție ==== | ||
- | |||
- | Atributele reprezintă o modalitate de specificare a unui comportament diferit de comportamentul implicit. Atunci când un fir de execuție este creat cu ''pthread_create'' se pot specifica atributele pentru respectivul fir de execuție. Atributele implicite sunt suficiente pentru marea majoritate a aplicațiilor. Cu ajutorul unui atribut se pot schimba: | ||
- | *starea: unificabil sau detașabil | ||
- | *politica de alocare a procesorului pentru firul de execuție respectiv (round robin, FIFO, sau system default) | ||
- | *prioritatea (cele cu prioritate mai mare vor fi planificate, în medie, mai des) | ||
- | *dimensiunea și adresa de start a stivei | ||
- | |||
- | Mai multe detalii puteți găsi în [[so:laboratoare-2013:resurse:threaduri_extra#lucrul cu atributele unui thread|secțiunea suplimentară dedicată]]. | ||
- | ==== Cedarea procesorului ==== | ||
- | |||
- | Un fir de execuție cedează dreptul de execuție unui alt fir, în urma unuia din următoarele evenimente: | ||
- | |||
- | *efectuează un apel blocant (cerere de I/O, sincronizare cu un alt fir de execuție) și kernel-ul decide că este //rentabil// să facă un context switch | ||
- | *i-a expirat cuanta de timp alocată de către kernel | ||
- | *cedează voluntar dreptul, folosind funcția [[http://linux.die.net/man/2/sched_yield|sched_yield]]:<code c> | ||
- | int sched_yield(void); | ||
- | </code> | ||
- | |||
- | Dacă există alte procese interesate de procesor, unul dintre procese va acapara procesorul, iar dacă nu există niciun alt proces în așteptare pentru procesor, firul curent își continuă execuția. | ||
- | ==== Alte operații ==== | ||
- | |||
- | /*<spoiler >*/ | ||
- | Dacă dorim să fim siguri că un cod de inițializare se execută o singură dată putem folosi funcția: | ||
- | |||
- | <code c> | ||
- | pthread_once_t once_control = PTHREAD_ONCE_INIT; | ||
- | int pthread_once(pthread_once_t *once_control, void (*init_routine) (void)); | ||
- | </code> | ||
- | |||
- | Scopul funcției ''pthread_once'' este de a asigura că o bucată de cod (de obicei folosită pentru inițializări) se execută o singură dată. Argumentul ''once_control'' este un pointer la o variabilă inițializată cu ''PTHREAD_ONCE_INIT''. Prima oară când această funcție este apelată ea va apela funcția ''init_routine'' și va schimba valoarea variabilei ''once_control'' pentru a ține minte că inițializarea a avut loc. Următoarele apeluri ale acestei funcții cu același ''once_control'' nu vor face nimic. | ||
- | |||
- | Funcția ''pthread_once'' întoarce 0 în caz de succes sau cod de eroare în caz de eșec. | ||
- | |||
- | Pentru a determina dacă doi identificatori se referă la același fir de execuție se poate folosi: | ||
- | |||
- | <code c> | ||
- | int pthread_equal(pthread_t thread1, pthread_t thread2); | ||
- | </code> | ||
- | |||
- | Pentru aflarea/modificarea priorităților sunt disponibile următoarele apeluri: | ||
- | |||
- | <code c> | ||
- | int pthread_setschedparam(pthread_t target_thread, int policy, const struct sched_param *param); | ||
- | int pthread_getschedparam(pthread_t target_thread, int *policy, struct sched_param *param); | ||
- | </code> | ||
- | |||
- | /*</spoiler>*/ | ||
- | ==== Compilare ==== | ||
- | |||
- | La compilare trebuie specificată și biblioteca ''libpthread'' (deci se va folosi argumentul ''-lpthread''). | ||
- | <note warning> Nu legați un program single-threaded cu această bibliotecă. Anumite apeluri din bibliotecile standard pot avea implementări mai ineficiente sau mai greu de depanat când se utilizează această bibliotecă. </note> | ||
- | |||
- | ==== Exemplu ==== | ||
- | |||
- | În continuare, este prezentat un exemplu simplu în care sunt create 2 fire de execuție, fiecare afișând un caracter de un anumit număr de ori pe ecran. | ||
- | |||
- | <code c exemplu.c> | ||
- | #include <pthread.h> | ||
- | #include <stdio.h> | ||
- | |||
- | /* parameter structure for every thread */ | ||
- | struct parameter { | ||
- | char character; /* printed character */ | ||
- | int number; /* how many times */ | ||
- | }; | ||
- | |||
- | /* the function performed by every thread */ | ||
- | void* print_character(void *params) | ||
- | { | ||
- | struct parameter *p = (struct parameter *) params; | ||
- | int i; | ||
- | |||
- | for (i = 0; i < p->number; i++) | ||
- | printf("%c", p->character); | ||
- | printf("\n"); | ||
- | |||
- | return NULL; | ||
- | } | ||
- | |||
- | int main() | ||
- | { | ||
- | pthread_t fir1, fir2; | ||
- | struct parameter fir1_args, fir2_args; | ||
- | |||
- | /* create one thread that will print 'x' 11 times */ | ||
- | fir1_args.character = 'x'; | ||
- | fir1_args.number = 11; | ||
- | if (pthread_create(&fir1, NULL, &print_character, &fir1_args)) { | ||
- | perror("pthread_create"); | ||
- | exit(1); | ||
- | } | ||
- | |||
- | /* create one thread that will print 'y' 13 times */ | ||
- | fir2_args.character = 'y'; | ||
- | fir2_args.number = 13; | ||
- | if (pthread_create(&fir2, NULL, &print_character, &fir2_args)) { | ||
- | perror("pthread_create"); | ||
- | exit(1); | ||
- | } | ||
- | |||
- | /* wait for completion */ | ||
- | if (pthread_join(fir1, NULL)) | ||
- | perror("pthread_join"); | ||
- | if (pthread_join(fir2, NULL)) | ||
- | perror("pthread_join"); | ||
- | |||
- | return 0; | ||
- | } | ||
- | </code> | ||
- | |||
- | Comanda utilizată pentru a compila acest exemplu va fi: | ||
- | |||
- | <code bash> | ||
- | gcc -o exemplu exemplu.c -lpthread | ||
- | </code> | ||
- | |||
- | |||
- | ====== Exerciţii de laborator ====== | ||
- | |||
- | ===== Exercițiul 0 - Joc interactiv (2p) ===== | ||
- | |||
- | * Detalii desfășurare [[http://ocw.cs.pub.ro/courses/so/meta/notare#joc_interactiv|joc]]. | ||
- | |||
- | ===== Linux (9p) ===== | ||
- | |||
- | Pentru rezolvarea laboratorului, va rugam sa clonati [[https://www.github.com/upb-fils/sde|repository-ul]]. daca il aveti deja, va rugam sa rulati ''git pull''. | ||
- | |||
- | <note tip> Pentru a vă ajuta la implementarea exercițiilor din laborator, în directorul ''utils'' din arhivă există un fișier ''utils.h'' cu funcții utile. </note> | ||
- | |||
- | <note>Pentru a instala paginile de manual pentru 'pthreads' | ||
- | <code bash>sudo apt-get install manpages-posix manpages-posix-dev</code> | ||
- | </note> | ||
- | |||
- | ==== Exercițiul 1 - Thread Stack (2p) ==== | ||
- | |||
- | Intrați în directorul ''1-th_stack'' și inspectați sursa, apoi compilați și rulați programul. Urmăriți cu ''pmap'' sau folosind ''procfs'' cum se modifică spațiul de adresă al programului: | ||
- | <code bash> | ||
- | watch -d pmap $(pidof th_stack) | ||
- | watch -d cat /proc/$(pidof th_stack)/maps | ||
- | </code> | ||
- | |||
- | Zonele de memorie cu dimensiunea de 8MB (8192KB) care se creează după fiecare apel ''pthread_create'' reprezintă noile //stive// alocate de către biblioteca ''libpthread'' pentru fiecare thread în parte. Observați că, în plus, se mai mapează de fiecare dată o pagină (4KB) cu protecția ''%%---p%%'' (PROT_NONE, private - vizibil în ''procfs'') care are rolul de %%"%%pagină de gardă%%"%%. | ||
- | |||
- | Motivul pentru care nu se termină programul este prezența unui ''while(1)'' în funcția thread-urilor. Folosiți ''Ctrl+C'' pentru a termina programul. | ||
- | ==== Exercițiul 2 - Fire de execuție vs Procese (2p) ==== | ||
- | |||
- | Intrați în directorul ''2-th_vs_proc'' și inspectați sursele. Ambele programe simulează un server care creează fire de execuție/procese. Compilați și rulați pe rând ambele programe. | ||
- | |||
- | În timp ce rulează, afișați, într-o altă consolă, câte fire de execuție/procese sunt create în ambele situații folosind comanda ''ps -L -C <nume_program>''. | ||
- | |||
- | <code bash> | ||
- | ps -L -C threads | ||
- | ps -L -C processes | ||
- | </code> | ||
- | |||
- | Verificați ce se întâmplă dacă la un moment dat un fir de execuție moare (sau un proces, în funcție de ce executabil testați). Testați utilizând funcția ''do_bad_task'' la fiecare al 4-lea fir de execuție/process. | ||
- | ==== Exercițiul 3 - Thread safety (2p) ==== | ||
- | |||
- | <note important> | ||
- | Datorită faptului că mașina virtuală ''spook'' are un singur core virtual, exercițiul următor trebuie realizat pe mașina fizică pentru a permite mai multor thread-uri să ruleze în același moment de timp. | ||
- | </note> | ||
- | |||
- | Intrați în directorul ''3-safety'' și inspectați sursa ''malloc.c''. Funcțiile ''thread_function'' și ''main'' **NU** sunt thread-safe relativ la variabilele ''global_storage'' și ''function_global_storage'' (revedeți semnificația lui ''[[http://en.wikipedia.org/wiki/Thread_safety | thread safety]]''). Există o [[https://en.wikipedia.org/wiki/Race_condition | condiție de cursă]] între cele două thread-uri create la incrementarea variabilei ''function_global_storage'', declarată în funcția ''thread_function'', și o altă condiție de cursă între toate thread-urile procesului la incrementarea variabilei globale ''global_storage''. | ||
- | <note tip> | ||
- | Un utilitar foarte folositor este ''helgrind'', care poate detecta automat aceste condiții de cursă. Îl putem folosi în cazul nostru așa: | ||
- | <code bash> | ||
- | valgrind --tool=helgrind ./mutex | ||
- | </code> | ||
- | </note> | ||
- | * ne vom concentra pentru rezolvarea TODO1 pe executabilui mutex | ||
- | |||
- | ''TODO 1'': Pentru a rezolva aceste doua conditii de cursa apelati functia increase_numbers intr-un mod thread_safe cu ajutor API-ului pus la dispozitie de critical.h | ||
- | |||
- | În fișierul ''malloc.c'' se creează ''NUM_THREADS'' thread-uri care alocă memorie în 1000 runde. Sunt șanse mari ca thread-urile să execute apeluri ''malloc'' concurente. | ||
- | După ce a-ti rezolvat ''TODO1'', compilati și rulati de mai multe ori. Observăm că programul rulează cu succes. Pentru a face verificări suplimentare, rulăm din nou ''helgrind'': | ||
- | |||
- | <code bash> | ||
- | valgrind --tool=helgrind ./mutex | ||
- | </code> | ||
- | |||
- | Observăm că nici ''helgrind'' nu raportează vreo eroare, lucru care conduce la faptul că funcția ''malloc'' ar fi thread-safe. (chiar daca acesta nu este protejat de API-ul pus la dispozitie) | ||
- | Pentru a putea fi siguri trebuie să consultăm paginile de manual și codul sursă. | ||
- | |||
- | <note important> | ||
- | Este important de știut că anumite funcții sunt thread-safe iar altele nu. Găsiți o listă cu funcțiile care nu sunt thread-safe în pagina de manual [[http://man7.org/linux/man-pages/man7/pthreads.7.html|pthreads(7)]], în secțiunea ''Thread-safe functions''. | ||
- | |||
- | Funcția ''malloc'' din implementarea GLIBC **este thread-safe**, lucru indicat în pagina de manual [[http://man7.org/linux/man-pages/man3/malloc.3.html#NOTES| malloc(3)]] (al treilea paragraf din secțiunea ''NOTES'') și vizibil în codul sursă prin prezența câmpului ''mutex'' în [[https://sourceware.org/git/?p=glibc.git;a=blob;f=malloc/malloc.c;h=f361bad636167cf1680cb75b5098232c9232d771;hb=HEAD#l1672|structura malloc_state]]. | ||
- | </note> | ||
- | |||
- | ''TODO 2'': Implementati un spinlock folosindu-va de operatii atomice. | ||
- | Operatiile atomice existente in standardul GCC le gasiti la [[https://gcc.gnu.org/onlinedocs/gcc/_005f_005fatomic-Builtins.html | __atomic functions]] | ||
- | |||
- | In fisierul critical.c trebuie sa completati in dreptul comentarilor asociate ''TODO 2'', avand la dispozitie hint-uri. | ||
- | |||
- | Testati si rulati de mai multe ori, pentru a verifica consistenta variabilei globale: global_storage, executabilul ''./spin''. | ||
- | |||
- | ==== Exercițiul 4 - Blocked (2p) ==== | ||
- | |||
- | |||
- | Inspectați fișierul ''blocked.c'' din directorul ''4-blocked'', compilați și executați binarul (repetați până detectați blocarea programului). Programul creează două fire de execuție care caută un număr magic, fiecare în intervalul propriu (nu este neapărat necesar ca numărul să fie găsit). Fiecare fir de execuție, pentru fiecare valoare din intervalul propriu, verifică dacă este valoarea căutată: | ||
- | * dacă da, marchează un câmp ''found'' pentru a înștiința și celălalt fir de execuție că a găsit numărul căutat. | ||
- | * dacă nu, inspectează câmpul ''found'' al structurii celuilalt fir de execuție, pentru a vedea dacă acesta a găsit deja numărul căutat. | ||
- | |||
- | Determinați cauza blocării, reparați programul și explicați soluția. Puteți utiliza ''helgrind'', unul din tool-urile ''valgrind'', pentru a detecta problema: <code bash> | ||
- | $ valgrind --tool=helgrind ./blocked | ||
- | </code> | ||
- | |||
- | <note tip> | ||
- | Așa cum ne arată și ''helgrind'', problema constă în faptul că cele două thread-uri iau cele două mutex-uri în ordinea inversă, situație foarte probabilă în a cauza un [[https://en.wikipedia.org/wiki/Deadlock|deadlock]]. | ||
- | </note> | ||
- | ==== Exercițiul 5 - Implementare comportament pthread_once (1p)==== | ||
- | |||
- | Aveți o funcție de inițializare pe care vreți să o apelați o singură dată. Pornind de la sursa ''once.c'' din directorul ''5-once'', asigurați-vă că funcția ''init_func'' este apelată o singură dată. **Nu** aveți voie să modificați funcția ''init_func'' sau să folosiţi ''pthread_once''. | ||
- | |||
- | Citiți despre funcționalitatea [[http://linux.die.net/man/3/pthread_once | pthread_once]] și revedeți secțiunea despre [[#mutex | mutex]]. | ||
- | |||
- | ==== Exercitiul 6 - Mutex vs Spinlock (1p) ==== | ||
- | |||
- | Dorim să testăm care varianta este mai eficientă pentru a proteja incrementarea unei variabile. | ||
- | |||
- | Intrați în directorul ''6-spin'', inspectați și compilați sursa ''spin.c''. În urma compilării vor rezulta două executabile, unul care folosește un mutex pentru sincronizare, iar altul un spinlock. | ||
- | |||
- | Comparați timpii de execuție: | ||
- | <code bash> | ||
- | time ./mutex | ||
- | time ./spin | ||
- | </code> | ||
- | |||
- | <note tip> | ||
- | Atunci când un fir de execuție găsește mutex-ul ocupat se va bloca. Atunci când un fir de execuție găsește spinlock-ul ocupat va face busy-waiting. | ||
- | </note> | ||
- | |||
- | /* | ||
- | ==== Extra ==== | ||
- | - Descărcați [[http://elf.cs.pub.ro/so/res/laboratoare/lab08-extra.zip|arhiva]] cu scripturi Python | ||
- | * Rulați tls.py; ce observați? De ce cu TLS programul afișează rezultatul corect, iar cu o variabilă globală nu? | ||
- | * Rulați profile_threads.py și extindeți scriptul pentru a intercepta trace-ului firelor de execuție. Vezi funcția [[http://docs.python.org/library/threading.html|settrace]] | ||
- | * Analizați cu [[http://linux.die.net/man/1/ltrace|ltrace]] apelurile pthread_* le care le face Python când creați un fir de execuție; ce observați? | ||
- | |||
- | */ | ||
- | ===== Resurse utile ===== | ||
- | |||
- | [[http://www.yolinux.com/TUTORIALS/LinuxTutorialPosixThreads.html | LinuxTutorialPosixThreads ]] | ||
- | |||
- | [[https://computing.llnl.gov/tutorials/pthreads/ | POSIX Threads Programming]] |