Python: dodatne usluge operacijskog sustava: višenitnost
Why did the multithreaded chicken cross the road?
to To other side. get the
Pojam procesne niti
-
POSIX Threads, poznati i pod nazivom Pthreads, omogućavaju procesima paralelizam izvođenja zasnovan na procesnim nitima i dijeljenoj memoriji
-
POSIX standard za rad s procesnim nitima koji ima više različitih implementacija
- Linux koristi Native POSIX Thread Library (NPTL), dokumentacija je dostupna putem
man phtreads
,man pthread.h
iman pthread_*
- FreeBSD koristi libthr, dokumentacija je dostupna putem u
pthread(3)
(man 3 phtread
)
- Linux koristi Native POSIX Thread Library (NPTL), dokumentacija je dostupna putem
-
-
module threading
(službena dokumentacija) nudi pristup POSIX threads sučelju threading.enumerate()
vraća listu svih trenutno aktivnih niti (uvijek je unutra barem glavna nit u kojoj se program izvodi)-
threading.Thread(target=funkcija, args=(arg1, arg2))
stvara objekt tipaThread
koji će pokrenuti funkcijufunkcija
s argumentimaarg1
iarg2
thread.start()
započinje izvođenje procesne nitithread.run()
nije isto što ithread.start()
; pokreće funkciju koja je dana kaotarget
unutar trenutne procesne niti, ne stvara novu procesnu nitthread.join()
čeka da procesna nit da završi s izvođenjem ("pridružuje" joj se; donekle sličnoos.wait()
)thread.is_alive()
vraćaTrue
ako se procesna nit i dalje izvodi
import threading import time def f1(): time.sleep(1) print("Pokrenuta je funkcija f1") def f2(arg): time.sleep(1) print("Pokrenuta je funkcija f2 s argumentom", arg) def f3(arg1, arg2): time.sleep(1) print("Pokrenuta je funkcija f2 s argumentima", arg1, "i", arg2) t1 = threading.Thread(target=f1) t2 = threading.Thread(target=f2, args=(5,)) # (5,) je tipa tuple i duljine 1, a (5) == 5 t3 = threading.Thread(target=f2, args=("ovo je thread t3",)) t4 = threading.Thread(target=f3, args=(2.7, [1, 2])) print(threading.enumerate() print("Je li nit t1 je pokrenuta?", t1.start()) print("Je li nit t2 je pokrenuta?", t2.start()) print("Je li nit t3 je pokrenuta?", t3.start()) print("Je li nit t4 je pokrenuta?", t4.start()) t1.start() t2.start() t3.start() t4.start() print(threading.enumerate() print("Je li nit t1 je pokrenuta?", t1.start()) print("Je li nit t2 je pokrenuta?", t2.start()) print("Je li nit t3 je pokrenuta?", t3.start()) print("Je li nit t4 je pokrenuta?", t4.start()) t1.join() t2.join() t3.join() t4.join() print(threading.enumerate() print("Je li nit t1 je pokrenuta?", t1.start()) print("Je li nit t2 je pokrenuta?", t2.start()) print("Je li nit t3 je pokrenuta?", t3.start()) print("Je li nit t4 je pokrenuta?", t4.start())
Zadatak
- Definirajte funkciju
countdown_to_zero(n)
koja prima kao argument prirodni brojn
i radi odbrojavanje, odnosno uwhile
petlji smanjue broj za 1 sve dok ne dođe do nule. - Pokrenite tri niti
t1
,t2
,t3
redom za brojeve 651929250, 421858921, 2188312. Pripazite da i ovdje(arg1,)
nije isto što i(arg1)
. - U glavnom procesu "odspavajte" 5 sekundi, a zatim provjerite jesu li živi
t1
,t2
it3
, i one koji jesu pridružite na glavni proces.
Zaključavanje
-
stanje utrke (engl. race condition) javlja se kod zapisivanja vrijednosti dijeljene varijable od strane više procesnih niti istovremeno, primjerice dvije procesne niti imaju zadatak povećati vrijednost dijeljene varijable koja trenutno ima vrijednost 2 za 1:
- prva nit očita trenutnu vrijednost 2 i poveća je za 1, dobije rezultat 3
- druga nit očita trenutnu vrijednost 2 i poveća je za 1, dobije rezultat 3
- prva nit zapiše dobivenu vrijednost 3
- druga nit zapiše dobivenu vrijednost 3
- očekivan rezultat nakon 2 uvećanja je 4, a imamo 3
# u ovako jednostavnom slučaju ćemo vrlo teško izvesti iznad opisani poredak operacija # zadatak niže će ilustrirati kakve probleme stvara stanje utrke import threading x = 2 def uvecaj(): global x x = x + 1 t1 = threading.Thread(target=uvecaj) t2 = threading.Thread(target=uvecaj) t1.start() t2.start() t1.join() t2.join() print("Vrijednost je", x)
-
threading.Lock()
-- jednostavan objekt zaključavanja koji se može dohvatiti samo jednom; svi ostali pokušaji dohvaćanja će blokirati dok se ne dogodi otpuštanje zaključavanja od strane koja je napravila dohvaćanjelock.acquire([blocking])
-- dohvaćanje zaključavanjalock.release()
-- otpuštanje zaključavanjalock.locked()
-- vraćaTrue
ako je zaključavanje dohvaćeno
import threading x = 2 lock_x = threading.Lock() def uvecaj(): global x lock_x.acquire() x = x + 1 lock_x.release() t1 = threading.Thread(target=uvecaj) t2 = threading.Thread(target=uvecaj) t1.start() t2.start() t1.join() t2.join() print("Vrijednost je", x)
-
threading.RLock()
-- jednostavan objekt zaključavanja koji se može dohvatiti više puta, ali samo od strane niti koja ga je već dohvatila; svi ostali pokušaji dohvaćanja će blokirati dok se ne dogodi onoliko otpuštanja zaključavanja od strane koja je napravila dohvaćanje koliko je napravljeno dohvaćanjarlock.acquire([blocking])
-- dohvaćanje zaključavanjarlock.release()
-- otpuštanje zaključavanjalock.locked()
-- vraćaTrue
ako je zaključavanje dohvaćeno
import threading x = 2 rlock_x = threading.RLock() def uvecaj(): global x rlock_x.acquire() # može i više puta što ima smisla kod složenijih programa x = x + 1 # svaki acquire() mora imati pripadni release() rlock_x.release() t1 = threading.Thread(target=uvecaj) t2 = threading.Thread(target=uvecaj) t1.start() t2.start() t1.join() t2.join() print("Vrijednost je", x)
Zadatak
Napišite program koji računa zbroj prvih 500000 prirodnih brojeva u dvije procesne niti; definirajte globalnu varijablu zbroj, i učinite jedna procesna nit u globalnu varijablu zbraja brojeve od 1 do 250000, a druga od 250001 do 500000.
Izvedite program bez zaključavanja, s običnim zaključavanjem i s višestrukim zaključavanjem. Ima li ovdje potrebe za višestrukim zaključavanjem? Objasnite zašto.
Zadatak
Analizirajte vrijeme potrebno za izvođenje programa za sljedeće varijante.
- Promijenite program iz prethodnog zadatka da računa produkt brojeva u danom rasponu umjesto zbroja. Usporedite vrijeme izvođenja tog programa i programa iz prethodnog zadatka.
- Pored toga, usporedite performanse kod varijante koja dohvaća lock u svakoj iteraciji for petlje s vremenom izvođenja varijante koja to dohvaća lock prije for petlje. Koja je brža? Objasnite zašto. (Raspon smanjite po potrebi.)
- Naposlijetku, isprobajte korištenje višestrukog zaključavanja umjesto običnog. Ima li razlike u vremenu izvođenja? Objasnite zašto.
threading.Condition([lock])
je proširenje pojma zaključavanja kojim se ovdje nećemo detaljnije baviti
Semafori
-
threading.Semaphore([value])
proširuje zaključavanje i ima brojač početne vrijednostivalue
koji omogućuje višestruko dohvaćanje (najviševalue
puta) i otpuštanje; izmislio ih je Edsger W. Dijkstras.acquire([blocking])
dohvaća zaključavanjes.release()
otpušta zaključavanje
import threading x = 2 semafor_x = threading.Semaphore(1) # semafor s vrijednošću 1 ima istovjetno ponašanje kao zaključavanje def uvecaj(): global x semafor_x.acquire() x = x + 1 semafor_x.release() t1 = threading.Thread(target=uvecaj) t2 = threading.Thread(target=uvecaj) t1.start() t2.start() t1.join() t2.join() print("Vrijednost je", x)
-
threading.BoundedSemaphore([value])
je semafor koji ograničava broj otpuštanja koje je moguće pozvati da ne prijeđu početno postavljenu vrijednostvalue
bs.acquire([blocking])
dohvaća zaključavanjebs.release()
otpušta zaključavanje
import threading x = 2 semafor_x = threading.BoundedSemaphore(1) # semafor s vrijednošću 1 ima istovjetno ponašanje kao zaključavanje def uvecaj(): global x semafor_x.acquire() x = x + 1 semafor_x.release() # dodatni poziv semafor_x.release() bacio bi iznimku tipa ValueError t1 = threading.Thread(target=uvecaj) t2 = threading.Thread(target=uvecaj) t1.start() t2.start() t1.join() t2.join() print("Vrijednost je", x)
Zadatak
Knjižnica nudi tri knjige:
- Marko Marulić: Judita, 5 komada
- Fjodor Mihajlovič Dostojevski: Zločin i kazna, 3 komada
- Eugen Kumičić: Urota zrinsko-frankopanska, 4 komada
Reprezentirajte te tri knjige u programu kao tri niza znakova proizvoljnog sadržaja, i pridružite im semafore s odgovarajućom vrijednosti. (Razmislite hoćete li koristiti ograničene semafore.)
Napišite funkciju lektira()
koja prima jedan argument tipa znakovni niz, a to je ime učenika, koje se zatim ispisuje na ekran (npr. "Ja sam Domagoj i posudit ću tri knjige"
). Učenik zatim "posuđuje te tri knjige", odnosno dohvaća njihove semafore i radi na njima acquire()
, pa "čita te knjige", odnosno prvo ispisuje ime knjige na ekran (npr. "Ja sam Sonja i čitam Fjodor Mihajlovič Dostojevski: Zločin i kazna"
). Naposlijetku "vraća te tri knjige", odnosno ispisuje da vraća knjige (npr. "Ja sam Ivan i vraćam tri knjige"
), a zatim radi release()
.
Učinite da sedam učenika posuđuje knjige, odnosno pokrenite sedam procesnih niti za učenike imena redom Domagoj, Ivan, Luka, Snežana, Romana, Sonja, Marta.
Događaji
-
threading.Event()
je jednostavan mehanizam sinkronizacije između procesnih niti kod kojeg jedna nit signalizira da se događaj dogodio, a druga čeka na signalizacijuevent.is_set()
vraćaTrue
ako se događaj dogodioevent.set()
postavlja da se događaj dogodioevent.clear()
vraća na početno stanje (događaj se nije dogodio)event.wait([timeout])
čeka na signalizaciju da se događaj dogodio
import threading event = threading.Event() def postavi(): global event print("Događaj će biti postavljen") event.set() def cekaj(): global event event.wait() print("Dočekano je postavljanje događaja") t1 = threading.Thread(target=postavi) t2 = threading.Thread(target=cekaj) t1.start() t2.start() t1.join() t2.join()
Zadatak
Pečete Ledolette s nadjevom od marelica koji zahtijevaju dvije minute za vađenje iz Ledo škrinje i otpakiravanje i 20 minuta u pećnici. Napravite dvije procesne niti, od kojih jedna pokreće funkciju odmrzavanje(n)
, gdje n
broj minuta koje se Ledolette pripremaju za stavljanje u pećnicu, i drugu koja pokreće funkciju pecnica(n)
, gdje je n
broj minuta koje se kroasani peku u pećnici. Iskoristite time.sleep()
s praktično upotrebljivim vrijednostima u sekundama da simulirate čekanje. Pokrenite istovremeno obje niti, ali učinite da druga nit čeka na događaj odmrzavanja kroasana koji prva nit postavlja.
Dodatni zadatak
Napišite program koji vrši zbroj kubova brojeva u rasponu od 1 do 300000 u 3 niti i raspodijelite tako da 1. nit računa raspon od 1 do 100000, 2. nit od 100001 do 200000, 3. nit od 200001 do 300000. Iskoristite višenitnosti i varijablu u koju ćete spremiti zbroj učinite dijeljenom; iskoristite zaključavanje kod promjene varijable u svakoj od niti; napravite događaj koji postavlja 3. nit u trenutku kad završi s izvođenjem, i učinite da na njega čekaju preostale dvije niti.
Brojači
-
threading.Timer(interval, funkcija)
pokreće funkciju u posebnoj procesnoj niti nakon što prođe vremenski interval trajanja danog u sekundamatimer.start()
započinje čekanje vrijednosti intervalatimer.cancel()
otkazuje čekanje
import threading def doceka(): print("Tko čeka, dočeka") t = threading.Timer(10, doceka) t.start() # ispisuje "Tko čeka, dočeka" nakon 10 sekundi
Zadatak
Kuhate čaj i treba vam 1 minuta da zavrije voda, a istovremeno u pećnici koja ima pokvaren timer pečete kolačiće madelaine koji će biti taman kako treba za 2 minute. Napravite program sa dva timera, svaki sa pripadnom funkcijom, od kojih će jedna ispisati na ekran "Voda spremna za čaj!"
, a druga "Kolačići madelaine taman kako treba!"
u odgovarajućim trenucima.
Barijere
-
threading.Barrier([parties])
omogućuje zadanom broju niti koji iznosi vrijednosti parametraparties
da čekaju jedna na drugu prije nastavka izvođenja; izvođenje se nastavlja kad sve pozovu funkcijuwait()
barrier.wait()
pokreće čekanje na ostale niti da dođu do barijerebarrier.reset()
vraća barijeru u prvobitno stanje, bez niti koje čekajubarrier.abort()
postavlja barijeru u polomljeno stanje, u kojem će svi pozivi funkcijiwait()
rezultirati iznimkom
import threading b = threading.Barrier(2) def f1(): global b print("Prije barijere 1") b.wait() print("Nakon barijere 1") def f2(): global b print("Prije barijere 1") b.wait() print("Nakon barijere 1") t1 = threading.Thread(target=f1) t2 = threading.Thread(target=f2) t1.start() t2.start() t1.join() t2.join()
Zadatak
Dodajte dijeljenu barijeru u obje funkcije prethodnog zadatka tako da se po završetku izrade čaja, odnosno kolačića, čeka na onu drugu stranu, a zatim na ekran ispiše:
I čim sam prepoznao okus u lipov čaj namočena komada madelaine, koji mi je svake nedjelje davala tetka Leonie (tada još doduše nisam znao razlog, zbog koga me ta uspomena tako usrećivala, nego sam to otkriće morao odgoditi za mnogo poslije), odmah se pojavi i stara, siva kuća na ulicu, u kojoj je bila njena soba, pa se kao pozorišni dekor pridruži malom paviljonu, koji je gledao na vrt, a koji su na njenoj stražnjoj strani nadogradili za moje roditelje (i baš to je bio onaj krnji komad, koji sam do sad jedini vidio); a s kućom se pojavi i grad. Trg, kamo su me slali prije ručka, ulice, kojima sam trčao od jutra do večeri, po svakom vremenu, i šetnje, na koje smo odlazili, kad je bilo lijepo vrijeme. I kao što se dešava u onoj igri, kojom se Japanci zabavljaju uranjajući u porculansku zdjelu punu vode komadiće do tad bezlična papira, koji se tek što je umočen, isteže, savija, bojadiše, diferencira, pretvara u cvijeće, kuće i određene osobe, koje je moguće prepoznati, tako je i sad sve cvijeće iz našeg vrta, iz Swannova perivoja, tako su svi lopoči s Vivonne, oni dobri seoski ljudi, njihovi mali domovi, crkva i cio Combray sa svojom okolinom, tako je sve što ima oblik i čvrstoću, i grad i vrtovi, izašlo iz moje čaše čaja.
Dodatni zadatak
Napišite program koji vrši zbroj kvadrata brojeva u rasponu od 1 do 500000 u 3 niti (raspodijelite po želji). Iskoristite višenitnosti i varijablu u koju ćete spremiti zbroj učinite dijeljenom; iskoristite zaključavanje kod promjene varijable u svakoj od niti; napravite dijeljenu barijeru između tri niti tako da niti čekaju jedna na drugu i istovremeno završavaju izvođenje.
Author: Vedran Miletić