Î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).
.heap
, .data
și .bss
(deci și variabilele stocate în ele)/dev/sda1
).(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.
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:
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).
Din punctul de vedere al implementării, există 3 categorii de fire de execuție:
Î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 threading.
Modulul threading
expune clasa Thread
. Astfel, un fir de execuție este creat prin instantierea clasei:
import threading threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)
None
;Thread-N
, unde e un numar;target
; parametrii vor fi pasati in ordinea in care functia ii primeste;target
, sub forma numele parametru-valoare;
Noul fir creat poate fi lansat prin apelarea functiei 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 current_thread:
import threading threading.current_thread()
Firele de execuție se așteaptă folosind funcția join:
t = threading.Thread() t.join(timeout=None)
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 excepthook va fi aruncata in threadul care a facut join.
join
pentru threadurile pe care le generează.
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.
join
pe un thread de tip daemon.
Un fir de execuție își încheie execuția în mod automat, la sfârșitul codului firului de execuție.
Pentru a schimba informații între programul principal si alte threaduri, vom folosi modulul 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
, vă recomandăm citirea documentației.
Pentru a crea o coadă, trebuie să instanțiem una din clasele expuse de modulul queue
:
import queue fifo_queue = queue.Queue() lifo_queue = queue.LifoQueue() priority_queue = queue.PriorityQueue()
Funcțiile put și put_nowait adaugă elemente în coadă.
q.put (item, block=True, timeout=None) q.put_nowait (item)
Funcțiile get și get_nowait extrag elemente în coadă.
q.get (block=True, timeout=None) q.get_nowait ()
Dacă coada este goală la apelarea uneia din cele două funcții, o excepție de tipul Empty este generată.
Mai jos vom crea un program care generează un thread ce primește valori de la programul principal și răspunde la aceastea.
import queue import threading def worker (): while True: item = q1.get() if item == 'Hello': print (item) q2.put ('SDE') elif item == 'Good': print (item) q2.put ('bye') else: break q1 = queue.Queue() q2 = queue.Queue() t = threading.Thread (target=worker) t.start () q1.put ('Hello') print (q2.get ()) q1.put ('Good') print (q2.get ()) q1.put ('exit') t.join()
a. Creați un program care pornește 5 fire de execuție. Fiecare fir de execuție afișeaƶă numerele de la 0 la 5.
b. Afișați identificatorul fiecărui fir de execuție (hint: ident).
Creați un program care primește 3 parametrii din linia de comandă și pornește 3 fire de execuție. Fiecare fir de execuție va primi ca parametru de la programul principal unul din cele 3 numere si va afișa pe ecran par
sau impar
, în funcție de paritatea numărului.
În fișierul din directorul 3-numbers, porniți un thread care afișează numerele de la 0 la 5 la interval de o secundă.
join
.
Observați când procesul își termină execuția. Modificați programul principal pentru a își înceta execuția imediat după ce afișează cele două linii
main 1 main 2
În fișierul din directorul 4-alive, porniți câte un thread pentru fiecare funcție worker definită si așteptați doar după threadurile inca in executie (hint: is_alive).
Creați două threaduri, unul care să genereze numerele prime de la 0 la 50 și unul care să genereze numerele patrate perfecte de la 0 la 50. Programul principal va afișa numerele generate de cele două threaduri, dupa care isi va incheia executia.
Creați un program care primește comenzi bash de la tastatură și le execută intr-un thread.
Creati un program care foloseste 3 threaduri pentru a găsi maximul dintr-un șir de lungime 100000. Fiecare thread citeste valori din sir printr-o coada și pune intr-o altă coadă maximul. Folositi comanda queue.join pentru sincronizare.