Fire de Executie in Python
Fire de Executie in Python
Fire de Executie in Python
1/12
Recapitulare laborator 1
n cadrul laboratorului de ASC folosim versiunea 2.x a Python-ului (cel putin 2.6). Aceast versiune
este incompatibil cu Python 3.x i unele construcii sau biblioteci este posibil s nu fie suportate nici
de versiunile anterioare 2.6.
Particulariti de limbaj:
Un fiier de cod Python este considerat un modul. Pentru a folosi alte module utilizm import n
urmtoarele modaliti:
import_example.py
import random
random.randint(0,4)
asc:lab2:index
https://cs.curs.pub.ro/wiki/asc/asc:lab2:index
modulului
from random import randint
randint(0,4)
"lab")
"lab", 2)
c=2)
"lab", c=2)
#
#
#
#
#
hello
hello
hello
hello
hello
world 0
lab 0
lab 2
world 2
lab 2
Ce este un thread?
Sistemele de calcul moderne sunt capabile de a executa mai multe operaii n acelasi timp. Sistemul
de operare este cel care permite rularea mai multor aplicaii simultan, dar aceast idee se poate
extinde i la nivelul unei aplicaii. De exemplu, o aplicaie ce ruleaz un stream video online trebuie
simultan s citeasc coninutul video de pe reea, s l decomprime, s actualizeze display-ul local cu
aceste informaii etc. Spunem c aplicaiile ce ofer aceste capabiliti constituie un software
concurent.
Deci ce este concurena? Concurena este proprietatea unei logici de program de a putea executa
simultan un set de task-uri. Paralelismul reprezint o metod de implementare a acestei
paradigme de programare ce permite rularea unui set de task-uri ntr-un mod care utilizeaz core-uri
multiple, procesoare multiple sau chiar mai multe maini (ntr-o structur de tip cluster de exemplu).
Thread-urile reprezint o metod de implementare a concurenei, fiind fire de execuie create (
spawned) n cadrul unui program principal (process) ce execut concurent task-uri definite de
programator. Un fir de execuie este parte a unui proces, iar implementarea difer de la un sistem de
operare la altul. Mai multe thread-uri pot exista n cadrul aceluiai proces, ele partajnd anumite
resurse: memorie, descriptori I/O etc. n aceast privin thread-urile difer de procese prin faptul c
variabilele globale pot fi accesate de ctre toate thread-urile unui proces i pot servi ca mediu de
comunicaie ntre thread-uri. Fiecare thread are totui i un set propriu de variabile locale. Din acest
motiv thread-urile mai sunt numite i lightweight processes.
https://cs.curs.pub.ro/wiki/asc/
2015/04/05 02:50
3/12
n cadrul unui sistem uni-procesor, rularea concurent a mai multor fire de execuie se face prin
metoda partajrii timpului de execuie (time sharing / time division / time slicing), sistemul de operare
alternnd succesiv ntre execuia thread-urile active (percepia este cea a rulrii simultane ns n
realitate un singur thread ruleaz la un moment dat).
n cadrul unui sistem multi-procesor sau multi-core, thread-urile vor rula n general cu adevrat
simultan, cu fiecare procesor rulnd un thread specific.
Din punct de vedere al suportului pentru programarea multithreading limbajele se mpart n dou
categorii:
limbaje cu thread-uri utilizator (green threads) ce nu sunt vizibile sistemului de operare, ci doar la
nivelul unui singur proces (gsii vreun dezavantaj?)
limbaje cu thread-uri native (adesea denumite i kernel threads) ce sunt vizibile la nivelul sistemului
de operare, ceea ce permite execuia lor paralel pe mai multe core-uri
Primul argument pentru metodele unei clase este ntotdeauna obiectul surs, numit self,
echivalent-ul lui this.
Cnd ne referim la membrii clasei, trebuie s folosim self.membru, ntr-un mod asemanator cu
folosirea this din Java (doar c n Python este obligatoriu s folosim self nu doar pentru a face
distincie ntre cmpurile clasei i parametrii/variabilele cu aceleai nume din funcii).
Metoda special __init__() este apelat la instanierea clasei i poate fi considerat un
constructor. Definirea metodelor __init__() este opional.
Metoda special __del__() este apelat cnd nu mai sunt referine la acest obiect i poate fi
asemuit cu un destructor. Definirea metodelor __del__() este opional.
n cazul motenirii, n metoda __init__() trebuie nti apelat __init__()-ul claselor printe.
Implicit toate cmpurile si metodele claselor sunt publice. Pentru a declara un cmp/metod privat
asc:lab2:index
https://cs.curs.pub.ro/wiki/asc/asc:lab2:index
numele acesteia trebuie prefixat cu __ (mai multe detalii putei afla aici).
Instanierea se face prin apelarea obiectului clas, posibil cu argumente.
class_example.py
class Student:
""" O clasa care reprezinta un student. Comentariile docstring se
pun dupa declaratie :) """
def __init__(self, name, grade=5):
# constructor cu parametru
default; echivalent cu mai multi constructori overloaded
self.name = name
# campurile clasei pot fi
declarate oriunde!
self.change_grade(grade)
# apelul unei metode a clasei
def change_grade(self, grade):
intotdeauna 'self'
self.grade = grade
x = Student("Alice")
y = Student("Bob", 10)
x.change_grade(8)
Clasele Python pot avea membri statici. n cazul cmpurilor, ele sunt declarate n afara oricrei
metode a clasei. Pentru metode avem dou variante: una folosind decoratorul @staticmethod,
cealalt folosind funcia built-in staticmethod. Observai c metodele statice nu au parametrul self.
static_example.py
class Util:
x = 2
@staticmethod
def do_stuff():
print "stuff"
# camp static
# metoda statica
def do_otherstuff():
# alta varianta de a declara o metoda
statica
print "other stuff"
do_otherstuff = staticmethod(do_otherstuff)
print Util.x
Util.do_stuff()
Util.do_otherstuff()
https://cs.curs.pub.ro/wiki/asc/
2015/04/05 02:50
5/12
Thread-uri
Un fir de execuie concurent este reprezentat n Pyhton de clasa Thread. Cel mai simplu mod de a
specifica instruciunile care se doresc a fi rulate concurent, este de a apela constructorul lui Thread cu
numele unei funcii care conine aceste instruciuni, precum n exemplul urmtor. Pornirea thread-ului
se face apoi cu metoda start(), iar pentru a atepta terminarea execuiei thread-ului se folosete
metoda join().
exemplul1.py
from threading import Thread
def my_concurrent_code(nr, msg):
""" Functie care va fi rulata concurent """
print "Thread", nr, "says:", msg
# creeaza obiectele corespunzatoare thread-urilor
t1 = Thread(target = my_concurrent_code, args = (1, "hello from thread"))
t2 = Thread(target = my_concurrent_code, args = (2, "hello from other
thread"))
# porneste thread-urile
t1.start()
t2.start()
# executia thread-ului principal continua de asemenea
print "Main thread says: hello from main"
# asteapta terminarea thread-urilor
t1.join()
ASC Wiki - https://cs.curs.pub.ro/wiki/asc/
asc:lab2:index
https://cs.curs.pub.ro/wiki/asc/asc:lab2:index
t2.join()
t
=
t
=
contine int-ul 42
(42)
contine un tuplu cu un singur element
(42,)
Crearea unui obiect Thread nu pornete execuia thread-ului. Acestu lucru se ntmpl doar dup
apelul metodei start().
O metod alternativ de a specifica instruciunile care se doresc a fi rulate concurent este de a crea o
subclas a lui Thread care suprascrie metoda run(). Se poate de asemenea suprascrie i metoda
__init__() (constructorul) pentru a primi argumentele cu care vor fi iniializate cmpurile proprii
subclasei. Dac optai pentru aceast abordare nu este indicat s suprascriei alte metode ale clasei
Thread, dect constructorul i run().
exemplul2.py
from threading import Thread
class MyThread(Thread):
""" Clasa care incapsuleaza codul nostru concurent """
def __init__(self, nr, msg):
Thread.__init__(self)
self.nr = nr
self.msg = msg
def run(self):
print "Thread", self.nr, "says:", self.msg
# creeaza obiectele corespunzatoare thread-urilor
t1 = MyThread(1, "hello from thread")
t2 = MyThread(2, "hello from other thread")
# porneste thread-urile
t1.start()
t2.start()
https://cs.curs.pub.ro/wiki/asc/
2015/04/05 02:50
7/12
Interpretorul cel mai popular de Python (CPython) folosete un lock intern (GIL - Global Interpreter
Lock) pentru a simplifica implementarea unor operaii de nivel sczut (managementul memoriei,
apelul extensiilor scrise n C etc.). Acest lock permite execuia unui singur thread n interpretor la un
moment dat i limiteaz paralelismul i performana thread-urilor Python. Mai multe detalii despre GIL
putei gsi n aceast prezentare.
Elemente de sincronizare
Pentru ca un program concurent s funcioneze corect este nevoie ca firele sale de execuie s
coopereze n momentul n care vor s acceseze date partajate. Aceast cooperare se face prin
intermediul partajrii unor elemente de sincronizare care pun la dispoziie un API ce ofer anumite
garanii despre starea de execuie a thread-urilor care le folosesc.
Thread
Pe lng facilitile de creare a noi fire de execuie, obiectele de tip Thread reprezint i cele mai
simple elemente de sincronizare, prin intermediul metodelor start() i join().
Metoda start() garanteaz c toate rezultatele thread-ului care o apeleaz (s-l numim t1), pn n
punctul apelului, sunt disponibile i n thread-ul care va porni (s-l numim t2). A se observa c nu se
ofer nici un fel de garanie despre rezultatele lui t1 care urmeaz dup apel. t2 nu poate face nici o
presupunere n acest caz, fr a folosi alte elemente de sincronizare.
asc:lab2:index
https://cs.curs.pub.ro/wiki/asc/asc:lab2:index
Metoda join() garanteaz thread-ului care o apeleaz (s-l numim t1) c thread-ul asupra creia este
apelat (s-l numim t2) s-a terminat i nu mai acceseaz date partajate. n plus toate rezultatele lui t2
sunt disponibile i pot fi folosite de ctre t1. A se observa c, fa de metoda start(), metoda join()
blocheaz execuia thread-ului care o apeleaz (t1) pn cnd t2 i termin execuia. Spunem c
join() este o metod blocant.
Lock
Lock-ul este un element de sincronizare care ofer acces exclusiv la poriunile de cod protejate de
ctre lock (cu alte cuvinte definete o seciune critic). Python pune la dispoziie clasa Lock pentru a
lucra cu acest element de sincronizare. Un obiect de tip Lock se poate afla ntr-una din urmtoarele
dou stri: blocat sau neblocat, implicit, un obiect de tip Lock fiind creat n starea neblocat. Sunt
oferite dou operaii care controleaz starea unui lock: acquire() i release().
Metoda acquire() va trece lock-ul n starea blocat. Dac lock-ul se afla deja n starea blocat, thread-ul
care a apelat acquire() se va bloca pn cnd lock-ul este eliberat (pentru a putea fi blocat din nou).
Metoda release() este cea care trece lock-ul n starea deblocat. Cele dou metode garanteaz c un
singur thread poate deine lock-ul la un moment dat, oferind astfel posibilitatea ca un singur thread
s execute seciunea de cod critic. O alt garanie a lock-ului este c toate rezultatele thread-ului
care a efectuat release() sunt disponibile i pot fi folosite de urmtoarele thread-uri care execut
acquire().
Spre deosebire de un mutex (ex: pthread_mutex), n Python, metodele acquire() i release() pot fi
apelate de thread-uri diferite. Cu alte cuvinte un thread poate face acquire() i alt thread poate face
release(). Datorit acestei diferene subtile nu este recomandat s folosii un obiect Lock n acest mod.
Pentru a reduce confuziile i a obine acelai efect se poate folosi un obiect BoundedSemaphore
iniializat cu valoarea 1.
Lock-ul este utilizat n majoritatea cazurilor pentru a proteja accesul la structuri de date partajate,
care altfel ar putea fi modificate de un fir de execuie n timp ce alte fire de execuie ncearc
simultan s citeasc sau s modifice i ele aceeai structur de date. Pentru a rezolva aceast situaie,
poriunile de cod care acceseaz structura de date partajat sunt ncadrate ntre apeluri acquire() i
release() pe acelai obiect Lock partajat de toate thread-urile care vor sa acceseze structura.
Exemplul de mai jos prezint folosirea unui lock pentru a proteja accesul la o list partajat de mai
multe thread-uri.
exemplul3.py
from threading import Lock, Thread
def inc(lista, lock, index, n):
""" Incrementeaza elementul index din lista de n ori """
for i in xrange(n):
lock.acquire()
lista[index] += 1
lock.release()
https://cs.curs.pub.ro/wiki/asc/
2015/04/05 02:50
9/12
Semaphore
Semaforul este un element de sincronizare cu o interfa asemntoare Lock-ului (metodele acquire()
i release()) ns cu o comportare diferit. Python ofer suport pentru semafoare prin intermediul
clasei Semaphore.
Un Semaphore menine un contor intern care este decrementat de un apel acquire() i incrementat
de un apel release(). Metoda acquire() nu va permite decrementarea contorului sub valoarea 0, ea
blocnd execuia thread-ului n acest caz pn cnd contorul este incrementat de un release().
ASC Wiki - https://cs.curs.pub.ro/wiki/asc/
asc:lab2:index
https://cs.curs.pub.ro/wiki/asc/asc:lab2:index
Metodele acquire() i release() pot fi apelate fr probleme de thread-uri diferite, aceast utilizare
fiind des ntlnit n cazul semafoarelor.
Un exemplu clasic de folosire a semaforului este acela de a limita numrul de thread-uri care
acceseaz concurent o resurs precum n exemplul urmtor:
exemplul4.py
from random import randint, seed
from threading import Semaphore, Thread
from time import sleep
def access(nr, sem):
sem.acquire()
print "Thread-ul", nr, " acceseaza"
sleep(randint(1, 4))
print "Thread-ul", nr, " a terminat"
sem.release()
# initializam semaforul cu 3 pentru a avea maxim 3 thread-uri active la
un moment dat
semafor = Semaphore(value = 3)
# stocam obiectele Thread pentru a putea face join
thread_list = []
seed()
# pornim thread-urile
for i in xrange(10):
thread = Thread(target = access, args = (i, semafor))
thread.start()
thread_list.append(thread)
# asteptam terminarea thread-urilor
for i in xrange(len(thread_list)):
thread_list[i].join()
Exerciii
1. Hello Thread - rezolvai exerciiile din fiierul task1.py din scheletul de laborator. (4p)
2. Protejarea variabilelor folosind locks - rezolvai exerciiile din fiierul task2.py din scheletul de
laborator. (3p)
3. Implementai problema producator-consumator folosind semafoare. (3p)
Mai muli productori i mai multi consumatori comunic printr-un buffer partajat, limitat la un
numr fix de valori. Un productor pune cte o valoare n buffer iar un consumator poate s ia
cte o valoare din buffer.
https://cs.curs.pub.ro/wiki/asc/
2015/04/05 02:50
11/12
Avei nevoie de dou semafoare, unul pentru a indica dac se mai pot pune valori n buffer i
cellalt pentru a arta dac exist vreo valoare care poate fi luat din buffer de ctre
consumatori.
4. Implementai problema filosofilor. (2p)
Se consider mai muli filozofi ce stau n jurul unei mese rotunde. n mijlocul mesei este o farfurie
cu spaghete. Pentru a putea mnca, un filozof are nevoie de dou beisoare. Pe mas exist cte
un beior ntre fiecare doi filozofi vecini. Regula este c fiecare filozof poate folosi doar
beioarele din imediata sa apropriere. Trebuie evitat situaia n care nici un filozof nu poate
acapara ambele beioare. Comportamentul tuturor filozofilor trebuie s fie identic.
Codul protejat (ncadrat) de lockuri nu e bine s conin print-uri sau instruciuni ce nu lucreaz cu
variabila partajat (e.g. sleep).
Nu este recomandat ca ntr-o aplicaie concurent s avei print-uri, i nici lock-uri pe print-uri, acest
lucru afectnd comportarea threadurilor. n laborator se folosesc print-uri mai mult in scop de
debugging, i v recomandm s folosii format sau concatenare (+) pentru a obine afiri atomice.
Resurse
Referine
Documentaie module
modulul thread
modulul threading - Thread, Lock, Semaphore
asc:lab2:index
https://cs.curs.pub.ro/wiki/asc/asc:lab2:index
From:
https://cs.curs.pub.ro/wiki/asc/ - ASC Wiki
Permanent link:
https://cs.curs.pub.ro/wiki/asc/asc:lab2:index
Last update: 2015/03/14 17:24
https://cs.curs.pub.ro/wiki/asc/