UNIVERZITET CRNE GORE Prirodno matematički fakultet – Podgorica

Miloš Ljumović Koncept niti u savremenim Windows operativnim sistemima – Kreiranje i sinhronizacija niti
ZAVRŠNI RAD

Podgorica, 2011

UNIVERZITET CRNE GORE Prirodno matematički fakultet – Podgorica

Kreiranje i sinhronizacija niti

Koncept niti u savremenim Windows operativnim sistemima – Kreiranje i sinhronizacija niti
ZAVRŠNI RAD

Operativni sistemi Predrag Stanišić Miloš Ljumović Računarstvo i informacione tehnologije 2607980210013

Podgorica, februar, 2012
strana 2

Kreiranje i sinhronizacija niti

Sažetak

Na početku ovog rada konceptualno ćemo predstaviti niti, zatim dati pregled komandi za korišćenje niti i završiti programom koji demonstrira kreiranje, sinhronizaciju i korišćenje višestrukih niti. Objasnićemo kako i kada treba kreirati niti u programima, kako upravljati nitima dok se izvršavaju i kako ih sinhronizovati korišćenjem mutexa, semafora i kritične sekcije.

strana 3

Kreiranje i sinhronizacija niti

Sadržaj

Sažetak Uvod Istorijat

------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

3 5 7 8 14 21 32 43 52 53

1. Koncept niti

2. Raspored i sinhronizacija niti 3. Komande vezane za niti 4. Sinhronizacija niti Zaključak Literatura

5. Primjer sa višestrukim nitima - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ---------------------------------------------------------------------------------------------------------

strana 4

Kreiranje i sinhronizacija niti

Uvod

Centralni koncept u svim operativnim sistemima je process, odnosno apstrakcija programa koji se izvršava. Većina modernih računara može da obavlja više stvari (operacija) odjednom. Dok se izvršava korisnički program, računar može čitati sa čvrstog diska, pisati u buffer štampača, itd. U multitasking sistemu CPU vrši brze smjene programa, izvršavajući svaki svega nekoliko desetina milisekundi. Dok se, striktno govoreći, u jednoj jedinici vremena može izvršavati samo jedan proces na procesoru, operativni sistem brzim smjenama (reda veličine nekoliko milisekundi) stvara utisak da se izvršava više procesa odjednom ili kako se to drugačije naziva pseudoparalelizam, nasuprot hardverski podržanom paralelizmu u multiprocesorskim sistemima, u kojima dvije ili više CPU-a dijeli zajedničku memoriju.

U tradicionalnim operativnim sistemima, svaki proces posjeduje svoj privatni adresni prostor i jednu nit kontrole, koja se obično naziva primarna ili inicijalna nit. U suštini ovo je definicija procesa. Nerijetko je potrebno imati više niti u adresnom prostoru procesa koje se izvršavaju u kvazi-paralelnom kontekstu, ponašajući se kao da su odvojeni procesi, izuzev zajedničkog adresnog prostora.
strana 5

Kreiranje i sinhronizacija niti
Vrlo važan koncept u svim operativnim sistemima je nit, ili nit kontrole. Objekat nit posjeduje programski brojač koji vodi evidenciju (računa) koja instrukcija treba da se izvrši naredni put kada nit dobije svoje vrijeme na procesoru, registre koji čuvaju tekuće vrijednosti promjenljivih s kojima nit manipuliše, stek gdje se smještaju podaci vezani za pozive funkcija (procedura) koje još nisu vratile kontrolu pozivaču, itd. Iako se nit izvršava u kontekstu procesa, niti i procesi su jako različiti koncepti i treba ih posmatrati odvojeno. Procesi se koriste (posmatrano uopšte) da objedine zajedničke resurse; niti su entiteti (najmanja jedinica) koji se zakazuju za izvršavanje na procesoru. U ovom radu konceptualno ćemo predstaviti niti, zatim dati pregled komandi za korišćenje niti i završiti programom koji demonstrira kreiranje, sinhronizaciju i korišćenje višestrukih niti.

strana 6

Kreiranje i sinhronizacija niti

Istorijat

Ranije verzije Windows-a, odnosno operativni sistemi mlađi od Windows-a 2000, radile su na principu permissive multitasking, odnosno dozvoljene višeprocesne obrade, dok novije verzije, počev od Windows-a 2000 rade na principu preemptive multitasking sistema, odnosno prekidne višeprocesne obrade. U dozvoljenom multitasking sistemu operativni sistem se oslanja na takozvanu „fer raspodjelu resursa“, odnosno na to da aplikacije redovno predaju kontrolu sistemu, kako bi druge aplikacije imale mogućnost da dobiju svoj dio vremena. U prekidnom multitasking sistemu operativni sistem zbirno suspenduje izvršavanje niti da bi dao drugim nitima odgovarajući dio vremena za izvršavanje. Očigledno, prekidni multitasking sistem ima sposobnost da obezbijedi mnogo lakše izvršavanje više zadataka, a uz to i ne zavisi od dobre volje svakog programa ili vještine programera u predavanju kontrole nad CPU-om.

strana 7

Kreiranje i sinhronizacija niti

1. Koncept Niti

Niti uvode novinu u modelu procesa u vidu mogućnosti izvršavanja više različitih zadataka odjednom. Imati više niti koje se izvršavaju paralelno u jednom procesu je analogno paralelnom izvršavanju više procesa na jednom računaru. Dok procesi dijele fizičku memoriju, diskove, štampače i druge resurse, niti dijele adresni prostor procesa, otvorene datoteke i druge privatne resurse jednog procesa. Zato što niti imaju određena svojstva procesa nazivamo ih još i lightweight procesima. Izraz multithreading se takođe koristi da opiše situaciju u kojoj je dozvoljeno izvršavanje više niti u okviru jednog procesa.

Na slici 1.(a) vidimo 3 tradicionalna procesa. Svaki proces posjeduje svoj privatni adresni prostor i jednu nit izvršavanja. Nasuprot tome na slici 1.(b) vidimo jedan proces sa 3 niti. Iako u oba sličaja imamo po 3 niti, na slici 1.(a) svaka nit izvršava se u različitom adresnom prostoru, dok se na slici 1.(b) sve niti izvršavaju u istom adresnom prostoru.

strana 8

Kreiranje i sinhronizacija niti

Slika 1. (a) Tri procesa, sa jendom niti kontrole. (b) Jedan proces sa tri niti.

Kada se proces sa više niti izvršava na računaru sa jednim procesorskim jezgrom niti se stalno, brzo, smjenjuju. Na slici 2 vidimo kako izgleda izvršavanje više programa. Kao što je već rečeno, brzim smjenama između različitih procesa, operativni sistem stvara iluziju odvojenih sekvencijalnih procesa koji se izvršavaju paralelno. Multithreading radi na isti način. CPU brzo izvršava instrukcije prebacujući se sa jedne niti na drugu, stvarajući iluziju o paralelnom izvršavanju. Sa tri niti u kontekstu jednog procesa, čini se da se niti izvršavaju paralelno, svaka dobijajući jednu trećinu vremena na procesoru.

Slika 2. (a) Izvršavanje četiri programa. (b) Konceptualni model četiri nezavisna sekvencijalna procesa. (c) Samo jedan program je aktivan u jednoj jedinici vremena.
strana 9

Kreiranje i sinhronizacija niti
Različite niti u procesima nisu toliko nezavisne, koliko su različiti procesi. Sve niti imaju isti adresni prostor, što znači da dijele sve globalne promjenljive. Obzirom da sve niti jednog procesa mogu koristiti sav adresni prostor procesa, jedna nit može čitati, pisati ili čak potpuno izbrisati podatke vezane za drugu nit. Ne postoji zaštita na nivou niti koja bi ih spriječila da jedna drugoj „kvare“ podatke, prvo zato što je to nemoguće implementirati, a drugo zato što za tim nema potrebe. Nasuprot procesima, koji se „utrkuju“ za resurse i „neprijateljski“ su nastrojeni jedan prema drugom, niti pripadaju jednom procesu, dijele njegove resurse i „prijateljski“ su nastrojene. Da bi se spriječila međusobna interferenca niti uvešćemo mehanizme za sinhronizaciju niti, koji će nam omogućiti siguran rad sa višestrukim nitima. Interesanta primjena niti je kod grafičkog korisničkog interfejsa (GUI), kod takvog sistema aplikacije uglavnom posjeduju prozor (formu), koji je ujedno komunikacija (interface) sa korisnikom. Svaki prozor koji kreira program pripada niti koja ga je kreirala. Kada nit kreira prozor, sistem joj dodjeljuje red poruka, a nit mora da uđe u petlju poruka da bi čitala iz svog reda. Svaka nit koja želi da prima poruke, mora da kreira prozor za sebe, čak i ako prozor ostane sakriven. Samo niti koje kreiraju prozore dobijaju redove poruka.

Kada treba kreirati niti i procese

Trebalo bi uzeti u obzir kreiranje niti svaki put kada program obavlja asinhrone operacije. Programi sa više prozora, na primjer, imaju mnogo koristi od kreiranja niti za svaki od prozora. Drugi primjer bio bi korišćenje savremenog programa za obradu teksta, poput Microsoft-ovog Word-a, sa ugrađenom automatskom provjerom gramatike. Ovdje je
strana 10

jedna nit (ili više njih) odgovorna za reagovanje na aktivnosti tastature, druge niti za ažuriranje teksta na ekranu, ostale niti upravljaju redovnim ažuriranjem rezervnih verzija radne datoteke, itd. Nit koja je zauzeta provjeravanjem teksta koji je bio zapisan do sad u odnosu na odabrani rječnik ili rječnike, kada riječ nije prepoznata, šalje poruku da bi obavijestila nit displeja da označi nepoznatu riječ. Bilo koja nit može kreirati druge niti ili nove procese. Kada program treba da uradi nekoliko stvari odjednom mora da odluči da li da kreira procese ili niti koje bi dijelile posao. Dobra programerska praksa kaže da treba izabrati niti kad god je to moguće iz jednostavnog razloga što ih sistem kreira brzo i one veoma lako vrše međusobnu interakciju. Kreiranje procesa traje duže, pošto sistem mora da učita novu, izvršnu, datoteku sa diska. Nasuprot tome, kreiranje novog procesa ima prednosti pošto svaki proces dobija svoj privatni adresni prostor, a na taj način se sprječava i međusobna interferencija niti. Naravno, pri tom treba biti oprezan zbog mogućih problema sinhronizacije, čime ćemo se detaljnije baviti u četvrtom poglavlju.

Kreiranje i sinhronizacija niti

Objekti niti

Na nivou operativnog sistema nit je objekat kojeg je kreirao Object Manager. Kao i svi sistemski objekti i nit sadrži atribute (podatke) i metode (funkcije). Na slici 3 šematski je prikazan objekat nit i data je lista njegovih atributa i metoda.

strana 11

Kreiranje i sinhronizacija niti

Slika 3. Objekat nit i njegovi atributi. Većina metoda za niti ima odgovarajuće Win32 (WinAPI) funkcije. Kada se pozove SuspendThread, na primjer, Win32 podsistem odgovara pozivom metode Suspend. Drugim riječima, Win32 programski interfejs za aplikacije (API – Application Program Interface) izlaže metod Suspend za Win32 aplikacije.

Objekti i identifikacioni kodovi

Windows je uvijek štitio neke unutrašnje strukture, kao što su prozori i četke, od direktne manipulacije. Niti koje se izvršavaju na korisničkom nivou sistema ne mogu direktno da proučavaju ili modifikuju unutrašnjost sistemskog objekta. Samo pozivanjem Win32 metode možemo nešto uraditi sa objektom. Windows daje identifikacioni kod koji
strana 12

identifikuje objekat, a programer prosljeđuje identifikacioni kod funkcijama kojima je potreban. I niti imaju identifikacione kodove, kao i procesi, semafori, datoteke i svi objekti uopšte. Samo Object Manager može da mijenja unutrašnjost objekta. Funkcija koja kreira nit vraća identifikacioni kod za novi objekat. Sa identifikacionim kodom niti možemo: − povećati ili smanjiti prioritet izvršavanja niti; − pauzirati ili rezimirati nit; − završiti nit; − saznati koju je vrijednost vratila nit kada je završila izvršavanje.

Kreiranje i sinhronizacija niti

strana 13

Kreiranje i sinhronizacija niti

2. Raspored i sinhronizacija niti

Rad sa nitima zahtijeva više nego da ih samo pokrenemo i zaustavimo. Potrebno je da niti efikasno interaguju, što zahtijeva kontrolu nad vremenom. Kontrola vremena ima dva oblika: prioritet i sinhronizaciju. Prioritet kontroliše koliko često nit dobija vrijeme na procesoru, dok sinhronizacija reguliše niti kada se takmiče da dobiju zajedničke resurse na korišćenje i daje sekvencu po kojoj nekoliko niti mora da izvrši zadatke u određenom redosljedu.

Prioritet procesa, osnovni prioritet i dinamički prioritet

Kad jedna nit završi rad i kada se traži sljedeća nit za izvršavanje, prednost imaju niti sa visokim prioritetom. Neke aktivnosti, kao što je odgovor na iznenadni gubitak napajanja, uvijek se izvršavaju sa vrlo visokim prioritetom. Elementi za obnavljanje sistemskih prekida imaju viši prioritet od korisničkih procesa. Ukratko, svaki proces ima rejting listu prioriteta, a osnovni prioritet niti potiče iz procesa koji ih posjeduje.

strana 14

Kreiranje i sinhronizacija niti
Kao što je ranije prikazano na slici 3, atributi objekta nit uključuju osnovni prioritet i dinamički prioritet. Kada pozivamo komande za promjenu prioriteta niti, mijenjamo samo osnovni prioritet. Ipak, nije moguće promijeniti prioritet niti za više od dva koraka gore ili dolje u odnosu na prioritet njenog procesa. Drugim riječima, nit ne može da bude mnogo bitnija od svog roditelja. Iako proces ne može da podigne svoje niti na visok prioritet, sistem može. Sistem dozvoljava neku vrstu unapređenja polja – dinamički prioritet – nitima koje preuzimaju važne zadatke. Kada korisnik izvrši neki unos u prozor, sistem uvijek podiže na viši nivo sve niti u procesu koji posjeduje prozor. Kada nit koja čeka podatke sa čvrstog diska te podatke konačno i primi, sistem unapređuje i tu nit. Ova privremena unapređenja, kada se dodaju na osnovni prioritet, formiraju dinamički prioritet. Dispečer (Dispatcher), odnosno element za pravljenje rasporeda, bira niti za izvršavanje na osnovu njihovog dinamičkog prioriteta. Osnovni i dinamičkig prioritet procesa dat je na slici 4.

strana 15

Kreiranje i sinhronizacija niti

Slika 4. Kako se opseg prioriteta niti izvodi iz prioriteta procesa

strana 16

Kreiranje i sinhronizacija niti
Povećanja dinamičkog prioriteta počinju da opadaju momentalno, nakon završenog zadatka. Dinamički prioritet niti spušta se za jedan stepen unazad svaki put kada nit primi sljedeći dio vremena na CPU-om i konačno se stabilizuje na osnovnom proritetu niti.

Kako se pravi raspored

Da bi odabrao sljedeću nit, dispečer počinje od reda najvišeg prioriteta, zatim se spušta niže do ostatka liste. Međutim, red možda ne sadrži sve niti u sistemu. Neke niti mogu biti suspendovane ili blokirane. U bilo kom trenutku nit može biti u jednom od sljedećih šest stanja: − Ready (spremna), nalazi se u redu – čeka izvršavanje; − StandBy (u pripravnosti), sljedeća za izvršavanje; − Running (izvršava se), izvršava se u interakciji sa CPU-om; − Waiting (čeka), ne izvršava se – čeka signal za rezimiranje; − Transition (prelaz), treba upravo da se izvrši pošto sistem učita njen kontekst; − Terminated (završena), završeno izvršavanje, ali objekat nije izbrisan. Kada dispečer odabere spremnu nit iz reda, sistem učitava kontekst (context) u operativnu memoriju. Kontekst uključuje niz vrijednosti za mašinske registre, kernel stek, blok okruženja niti i korisnički stek u adresnom prostoru procesa niti (ako je dio konteksta bio poslat na disk, nit ulazi u prelazno stanje dok sistem sakuplja djelove). Promjena niti znači čuvanje svih djelova konteksta i učitavanje svih djelova sljedeće niti. Novoučitana nit se izvršava jedan dio vremena, koji je reda 2 ∗ 10−6 𝑠 (u obzir je uzet
strana 17

procesor Intel E6750). Sistem održava brojač koji mjeri tekući dio vremena. Za svaki otkucaj

sata sistem umanjuje vrijednost brojača za određenu vrijednost. Kada dostigne nulu, dispečer izvodi promjenu konteksta i postavlja novu nit za izvršavanje.

Kreiranje i sinhronizacija niti

Kako se odvija sinhronizacija

Da bi se niti uopšte izvršavale, mora se zakazati njihovo izvršavanje. Da bi se izvršavale bez međusobne interference, potrebno ih je sinhronizovati. Recimo da jedna nit kreira četkicu (Brush), a zatim kreira nekoliko niti koje dijele četkicu i crtaju s njom. Prva nit ne smije uništiti četkicu dok ostale niti ne završe crtanje. Ili, recimo, da jedna nit prihvata ulaz od strane korisnika i upisuje ga u datoteku, dok druga nit čita iz datoteke i obrađuje tekst. Nit koja čita ne smije da čita dok nit koja upisuje, piše u datoteku. Obje situacije zahtijevaju sredstva za koordinaciju akcija između nekoliko niti. Jedno rješenje bilo bi da se kreira globalna Boolean promjenljiva bDone koju jedna nit koristi da signalizira drugoj niti. Nit koja upisuje mogla bi da podesi bDone na TRUE, a nit koja čita mogla bi da izvršava petlju dok ne ustanovi da se indikator bDone promijenio. Ovakav mehanizam bi radio, ali nit koja izvršava petlju troši mnogo procesorskog vremena. Umjesto toga Windows operativni sistem podržava niz sinhronizacionih objekata: − Objekat mutex (mutex znači mutual exclusion – međusobno isključenje) radi kao uska kapija gdje može da prođe samo jedna nit u jednom trenutku; − Objekat semaphore (semafor) radi kao kapija sa više djelova kroz koju može proći ograničen broj niti; − Objekat event (događaj) emituje javni signal koji bilo koja nit koja sluša može da primi (čuje); − Objekat critical section (kritična sekcija) radi baš kao i mutex, ali samo u okviru jednog procesa.
strana 18

Svi gore navedeni objekti su sistemski objekti, koje je kreirao Object Manager. Iako svaki sinhronizacioni objekat koordinira različite interakcije, svi oni rade na sličan način. Nit koja želi da izvrši neku koordiniranu operaciju, čeka odgovor od jednog od ovih objekata i nastavlja tek kada primi isti. Dispečer uklanja iz reda objekte koji čekaju da ne bi trošili procesorsko vrijeme. Kada signal stigne, dispečer dozvoljava rezimiranje niti. Kako i kada signal stiže zavisi od objekta. Na primjer, jedna bitna karakteristika mutexa je ta da samo jedna nit može da ga posjeduje. Mutex ne radi ništa, osim što dozvoljava da ga posjeduje samo jedna nit u jednom trenutku. Ako nekoliko niti treba da radi sa jednom datotekom, možemo kreirati mutex kako bi zaštitili datoteku. Kada bilo koja nit započne operaciju nad datotekom, prvo zatraži mutex, i ako ni jedna druga ne drži mutex, nit nastavlja rad. Ako je s druge strane, neka druga nit upravo „ugrabila“ mutex za sebe, zahtjev ne uspijeva, nit se blokira i postaje suspendovana dok čeka da preuzme vlasništvo nad mutexom. Kada jedna nit završi upisivanje, ona oslobađa mutex i nit koja čeka „oživljava“ ponovo, prima mutex i izvodi svoje operacije nad datotekom. Mutex ne štiti ništa aktivno. On radi samo zato što se niti koje ga koriste slažu da ne upisuju u datoteku bez prethodnog posjedovanja istog i ne postoji ništa što bi spriječilo da sve niti pokušaju da upisuju odjednom. Mutex je samo signal, nešto kao i Boolean promjenljiva bDone u primjeru sa petljom. Mutex se može koristiti da bi se zaštitile globalne promjenljive, hardverski port, identifikacioni kod za kanal (pipe) ili klijentsku oblast prozora. Svaki put kada više niti dijeli neki sistemski resurs, treba uzeti u obzir sinhronizaciju istih, kako ne bi dolazilo do međusobnih konflikata. Mutexi, semafori i događaji mogu da koordiniraju niti u različitim procesima, ali objekat kritična sekcija je vidljiv samo nitima u jednom procesu. Kada jedan proces kreira podproces (child process), podproces često nasljeđuje identifikacione kodove za postojeće sinhronizacione objekte. Objekti kritične sekcije se ne nasljeđuju.
strana 19

Kreiranje i sinhronizacija niti

Sinhronizacioni objekat je, kao i drugi sistemski objekti, struktura podataka. Sinhonizacioni objekti imaju dva stanja: signalizirano i nesignalizirano. Niti vrše interakciju sa sinhronizacionim objektima tako što mijenjaju signal ili tako što čekaju signal. Nit koja čeka signal je blokirana i ne izvršava se. Kada se signal desi, nit koja čeka prima objekat, isključuje signal, izvodi neki sinhronizovani zadatak i ponovo uključuje signal kada ostavlja objekat. Niti mogu da čekaju i druge objekte osim mutexa, semafora, događaja i kritičnih sekcija. Nekad ima smisla čekati proces, nit, tajmer ili fajl. Ovi objekti služe i za druge namjene, ali kao i sinhronizacioni objekti posjeduju stanje signala. Procesi i niti signaliziraju kada se završe. Tajmer objekti signaliziraju kada prođe određeno vrijeme. Fajlovi signaliziraju kada se završi operacija upisa ili čitanja. Loša sinhronizacija dovodi do grešaka (bugs). Na primjer, greška prekida dešava se kada dvije niti čekaju jedna drugu. Nijedna od njih neće se završiti dok se ona druga ne završi prva. Stanje trke se dešava kada program ne uspije da sinhronizuje svoje niti. Recimo da jedna nit upisuje u fajl a druga nit čita novi sadržaj. Da li program radi, zavisi od toga koja nit će pobijediti u trci za I/O operacijama. Ako nit koja čita pokuša prva da pročita, program neće raditi.

Kreiranje i sinhronizacija niti

strana 20

Kreiranje i sinhronizacija niti

3. Komande vezane za niti

Da bi predstavili karakteristike programiranja sa više niti, objasnićemo djelove Win32 API-ja koji su vezani za niti. Na početku ćemo objasniti komande za kreiranje i modifikaciju niti, a zatim ćemo se koncentrisati na komande za sinhronizaciju niti.

Kreiranje i modifikacija niti

Ciklus života niti počinje kada pozovemo funkciju CreateThread. Druge funkcije dozvoljavaju da proučavamo nit, da je suspendujemo ili rezimiramo, promijenimo njen prioritet i da je završimo.

Kreiranje niti

Bilo koja nit može kreirati drugu nit pozivanjem funkcije CreateThread, što obezbjeđuje slične usluge za nit kakve main ili WinMain funkcije obezbjeđuju za cio
strana 21

program. Drugim riječima, argumenti funkcije CreateThread definišu osobine koje nit treba da ima da bi započela život – primarno; siguronosne privilegije i početnu funkciju. Život niti poklapa se sa životom njene glavne (main) funkcije. Kada funkcija vrati vrijednost, nit se završava. Nit se može pokrenuti u bilo kojoj funkciji koja prima jedan LPVOID (void*) parametar i vraća DWORD (unsigned long) vrijednost. CreateThread dozvoljava da proslijedimo LPVOID u početnu funkciju. Ako nekoliko niti izvršava jednu funkciju, možemo svakoj od njih proslijediti različit argument. Svaka od njih mogla bi da primi pokazivač na drugačije ime fajla, na primjer, ili drugačiji identifikacioni kod objekta koji čeka. Funkcija CreateThread zahtijeva šest parametara. Slijedi izgled prototipa:
HANDLE WINAPI CreateThread( LPSECURITY_ATTRIBUTES lpsa, DWORD cbStack, // privilegije pristupa // inicijalna veličina steka

Kreiranje i sinhronizacija niti

LPTHREAD_START_ROUTINE lpStartAddress, // pokazivač na funkciju LPVOID lpvThreadParam, DWORD fdwCreate, LPDWORD lpThreadID );

// vrijednost proslijeđena funkciji // aktivan ili suspendovan // sistem ovdje vraća ID

Prvi parametar ukazuje na strukturu SECURITY_ATTRIBUTES koja utvrđuje ko smije da dijeli objekat i da li drugi procesi smiju da ga modifikuju. Struktura sadrži deskriptor sigurnosti koji dodjeljuje privilegije pristupa za različite korisnike sistema i grupe korisnika. Većina programera jednostavno prihvata podrazumijevani deskriptor koji dolazi sa tekućim procesom. Takođe, siguronosna struktura sadrži indikator nasljeđivanja, koji, ako je postavljen na TRUE, dozvoljava bilo kojim podprocesima koji su kreirani da automatski naslijede identifikacioni kod za ovaj objekat.
strana 22

typedef struct _SECURITY_ATTRIBUTES { DWORD nLength;

Kreiranje i sinhronizacija niti
// veličina strukture _SECURITY_ATTRIBUTES // NULL za prihvatanje deskriptora procesa // TRUE ako djeca mogu da naslijede objekat

LPVOID lpSecurityDescriptor; BOOL bInheritHandle;

} SECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES;

Ne treba da kreiramo strukturu SECURITY_ATTRIBUTES, osim, ako želimo da nit bude naslijeđena. Ako proslijedimo NULL kao prvi parametar za CreateThread, nova nit prima podrazumijevani deskriptor i neće biti naslijeđena. Sljedeća tri parametra daju novoj niti materijal sa kojim može da radi. Po podrazumijevanoj vrijednosti, svaka nit prima stek koji je iste veličine kao stek primarne niti. Veličinu možete mijenjati uz pomoć drugog parametra, ali ako je kasnije potrebno više mjesta u steku, sistem ga automatski proširuje. Treći parametar ukazuje na funkciju gdje će se nit startovati, a vrijednost u četvrtom parametru postaje argument koji se prosljeđuje početnoj funkciji. Parametar fdwCreate može da ima jednu od dvije vrijednosti: 0 (nula) ili CREATE_SUSPENDED. Suspendovana nit ne počinje da se izvršava dok je ne „poguramo“ malo pomoću funkcije ResumeThread. Program koji kreira veliki broj niti mogao bi da ih suspenduje, akumulira njihove identifikacione kodove, i kada su spremne, da ih pokrene sve odjednom. Posljednji parametar ukazuje na praznu riječ DWORD gdje funkcija CreateThread postavlja broj koji jedinstveno identifikuje nit u sistemu. Nekoliko funkcija zahtijeva da identifikujemo nit po njihovim ID brojevima umjesto po njihovim identifikacionim kodovima. Funkcija CreateThread vraća identifikacioni kod za novu nit. Ako nit nije mogla biti kreirana, identifikacioni kod će biti NULL. Treba znati da će sistem kreirati nit čak i ako su lpStartAddress ili lpvThreadParam nevažeće, ili ako ukazuju na nevažeće adrese (adrese ili
strana 23

podaci kojima se ne može pristupiti). U tim slučajevima funkcija CreateThread vraća tačan identifikacioni kod, ali se nova nit završava momentalno i vraća kod greške. Možemo testirati validnost niti pomoću GetExitCodeThread, koja vraća STILL_ACTIVE ako se nit nije završila. Osim ako funkciji CreateThread ne damo eksplicitni deskriptor sigurnosti, novi identifikacioni kod dolazi sa punim pravima pristupa novom objektu. U slučaju niti, puni pristup znači da pomoću ovog identifikacionog koda možemo da suspendujemo, rezimiramo, završimo ili promijenimo prioritet niti. Identifikacioni kod ostaje važeći čak i pošto se nit završila. Da bi uništili objekat nit, treba da zatvorimo njen identifikacioni kod pozivom funkcije CloseHandle. Ako postoji više od jednog identifikacionog koda, nit neće biti uništena sve dok posljednji identifikacioni kod ne bude zatvoren. Ako zaboravimo da zatvorimo identifikacioni kod, sistem će to uraditi automatski kada se proces završi.

Kreiranje i sinhronizacija niti

Promjena prioriteta niti

Niti visokog prioriteta dobijaju više procesorskog vremena, završavaju svoj posao brže i više reaguju na korisnika. Međutim, ako odredimo da sve niti budu najvišeg prioriteta, gubi se smisao davanja prioriteta. Ako veliki broj niti ima isti prioritet – bilo da je njihov prioritet visok ili nizak – dispečer mora da im da isti dio procesorskog vremena. Jedna nit može više regovati samo ako druge niti manje reaguju. Isto pravilo se podjednako odnosi i na procese. Sljedeće funkcije služe za dobijanje ili modifikovanje osnovnog prioriteta niti:

BOOL WINAPI SetThreadPriority( HANDLE hThread, int nPriority // nit koju treba modifikovati // njen nivo prioriteta

strana 24

);

Kreiranje i sinhronizacija niti

int WINAPI GetThreadPriority( HANDLE hThread ); // nit koju treba modifikovati

SetThreadPriority vraća TRUE ili FALSE za uspjeh ili neuspjeh, dok GetThreadPriority vraća tekući prioritet niti. Da bi imenovali moguće vrijednosti prioriteta za obje funkcije, koristimo sljedeći niz konstanti:

THREAD_PRIORITY_LOWEST THREAD_PRIORITY_BELOW_NORMAL THREAD_PRIORITY_NORMAL THREAD_PRIORITY_ABOVE_NORMAL THREAD_PRIORITY_HIGHEST

-

dva nivoa ispod procesa jedan nivo ispod procesa isti nivo kao kod procesa jedan nivo iznad procesa dva nivoa iznad procesa nivo 15 (u procesima normalnih korisnika) nivo 1 (u procesima normalnih korisnika)

THREAD_PRIORITY_TIME_CRITICAL THREAD_PRIORITY_IDLE -

Prvih pet vrijednosti podešava nivo u odnosu na nivo osnovnog, njenog roditeljskog, procesa, što je ranije prikazano na slici 4. Posljednje dvije, za kritični i neaktivni prioritet, iskazuju apsolutne nivoe prioriteta u višim i nižim ekstremima klase prioriteta roditelja (ekstremi za kodove prioriteta u realnom vremenu su 16 i 31). Nivo neaktivnog prioriteta radi dobro za screensaver-e, jer oni ne bi trebalo da se izvršavaju osim u slučaju kada se ništa drugo ne dešava.

Suspendovanje i rezimiranje izvršavanja niti

Suspendovana nit prestaje da se izvršava i neće se nalaziti u rasporedu za dobijanje procesorskog vremena. Ostaje u ovom stanju dok je druga nit ne primora da se rezimira. Suspendovanje niti može biti korisno ako, na primjer, korisnik prekida zadatak. Možemo
strana 25

suspendovati nit dok čekamo da korisnik potvrdi otkazivanje. Ako korisnik izabere da nastavi, prekinuta nit se može rezimirati tamo gdje je stala. Program koji ćemo predstaviti kasnije suspenduje nekoliko niti za crtanje svaki put kada korisnik mijenja veličinu prozora. Kada se prozor ponovo iscrta (ofarba), niti nastavljaju crtanje. Nit poziva sljedeće funkcije da bi natjerala drugu nit da pauzira ili da se rezimira:

Kreiranje i sinhronizacija niti

DWORD WINAPI SuspendThread( HANDLE hThread ); // nit koju treba suspendovati

DWORD WINAPI ResumeThread( HANDLE hThread ); // nit koju treba rezimirati

Jedna nit se može suspendovati uzastopno nekoliko puta, bez bilo kakvih komandi za rezimiranje, ali svaka komanda SuspendThread mora nekad da se upari sa komandom ResumeThread. Sistem broji koliko ima komandi za suspenziju za svaku nit (atribut SunspensionCount na slici 4.) . Komanda SuspendThread inkrementira brojač, a ResumeThread ga dekrementira, dok obje funkcije vraćaju prethodnu vrijednost brojača u obliku DWORD vrijednosti. Samo kada se brojač vrati na nulu, nit nastavlja da se izvršava. Nit može da suspenduje samu sebe (ali ne može da rezimira samu sebe) i može samu sebe da uspava za podešeni period. Komanda Sleep odlaže izvršavanje, uklanjajući nit iz reda dok ne prođe određeno vrijeme. Interaktivne niti koje pišu ili crtaju informacije za korisnika, često kratko zadrijemaju da bi dale korisniku vrijeme da vidi izlaz. Uspavanost niti je bolja od prazne petlje zato što ne koristi procesorsko vrijeme.

VOID WINAPI Sleep( DWORD dwMilliseconds // trajanje pauze );

strana 26

Kreiranje i sinhronizacija niti
DWORD WINAPI SleepEx( DWORD dwMilliseconds, BOOL bAlertable ); // trajanje pauze // TRUE da se rezimira ako se I/O operacija završi

Proširena funkcija SleepEx obično radi u konjukciji sa I/O funkcijama u pozadini i može se koristiti za iniciranje operacije čitanja ili upisa bez čekanja da se operacija završi. Operacija se nastavlja u pozadini. Kada se završi, sistem obavještava korisnika tako što pozove povratnu (callback) proceduru iz programa. I/O operacija u pozadini (koja je još poznata i kao preklapajuća I/O operacija) posebno pomaže u interaktivnim programima koji moraju stalno da reaguju na korisnika pri radu sa relativno sporim uređajima, kao što su drajveri, mrežni diskovi i drugi. Boolean parametar bAlertable u funkciji SleepEx dozvoljava da sistem probudi nit prije vremena ako se preklapajuća I/O operacija završila prije nego što istekne interval spavanja. Ako je funkcija SleepEx prekinuta, ona vraća WAIT_IO_COMPLETION. Ako interval prođe bez prekida, SleepEx vraća nulu.

Dobijanje informacija o postojećim nitima

Nit može lako dobiti dva dokaza o svom identitetu: identifikacioni kod i identifikator. Sljedeće funkcije vraćaju informacije koje identifikuju tekuću nit:

DWORD WINAPI GetCurrentThreadId( void );

HANDLE WINAPI GetCurrentThread( void );

Povratna vrijednost iz funkcije GetCurrentThreadId poklapa se sa vrijednošću u lpThreadID poslije komande CreateThread. To je vrijednost koja jedinstveno identifikuje nit
strana 27

u sistemu. Iako nekoliko Win32 API komandi zahtijeva da znamo ID niti, to može biti korisno za praćenje niti širom sistema bez potrebe da držimo sve identifikacione kodove otvorene. Treba imati na umu da otvoreni identifikacioni kodovi sprječavaju da se nit uništi. Identifikacioni kod koji vraća funkcija GetCurrentThread ima istu namjenu kao i identifikacioni kod iz funkcije CreateThread. Iako radi na isti način kao i drugi identifikacioni kodovi, to je, u stvari, pseudoidentifikacioni kod – specijalna konstanta, koju sistem interpretira na određeni način - kao što, na primjer, jedna tačka (.) uvijek ukazuje na tekući direktorijum, a this u jeziku C++ uvijek ukazuje na tekući objekat. Pseudoidentifikacioni kod koji vraća GetCurrentThread uvijek ukazuje na tekuću nit. Da bi dobila pravi, prenosivi identifikacioni kod za sebe, funkcija DuplicateHandle može se koristiti na sljedeći način:
// ovdje ćemo smjestiti duplikat // izvorni proces // originalni identifikacioni kod // odredišni proces // novi duplikat identifikacionog koda // prava pristupa (koje je prevazišao // posljednji parametar) FALSE, DUPLICATE_SAME_ACCESS // djeca ne nasljeđuju kod // kopira prava pristupa iz originalnog // identifikacionog koda );

Kreiranje i sinhronizacija niti

HANDLE hThread; DuplicateHandle( GetCurrentProcess( ), GetCurrentThread( ), GetCurrentProcess( ), &hThread, 0,

Dok CloseHandle nema nikakav efekat na pseudoidentifikacioni kod, identifikacioni kod iz DuplicateHandle je realan i mora se eventualno zatvoriti. Korišćenje pseudoidentifikacionog koda omogućava da GetCurrentThread radi brže, jer pretpostavlja da bi nit trebala da ima pun pristup samoj sebi i vraća svoj rezultat, a da pri tom ne uzima u obzir siguronosne karakteristike.
strana 28

Kreiranje i sinhronizacija niti

Prekid izvršavanja niti

Baš kao što se i Windows program završava kada dođe do kraja WinMain funkcije, nit se normalno završava kada dođe do kraja funkcije gdje je počela. Kada nit dođe do kraja svoje početne funkcije, sistem automatski poziva funkciju ExitThread.

VOID WINAPI ExitThread( DWORD dwExitCode ); // izlazni kod za nit koja se završava

Iako sistem automatski poziva funkciju ExitThread, možemo je pozvati direktno ako neki uslov primorava nit da se izvršava neograničeno.

DWORD WINAPI ThreadFunction( LPVOID lpParameter ) { HANDLE hThread = CreateThread( <parametri> );

// Poslovi inicijalizacije se dešavaju ovdje. // Testirajte da vidite da li je bilo problema. if ( <stanje greške> ) { ExitThread( ERROR_CODE ); } // otkazuje se nit

// nema greške, rad se nastavlja return( SUCCESS_CODE ); // ova linija prouzrokuje da sistem pozove ExitThread }

ERROR_CODE i SUCCESS_CODE su onakvi kakvim ih definišemo. U ovom primjeru mogli smo dovoljno lako da otkažemo nit komandom return:
strana 29

if ( <error condition> ) { return( ERROR_CODE ); }

Kreiranje i sinhronizacija niti
// otkazuje se nit

Komanda return ima isti efekat kao ExitThread; u stvari ona čak kao rezultat ima poziv ExitThread. Kada se nit završi iskazom return, DWORD (unsigned long) povratna vrijednost postaje izlazni kod koji se automatski prosljeđuje funkciji ExitThread. Kada se nit završila, njen izlazni kod je dostupan preko sljedeće funkcije:

BOOL WINAPI GetExitCodeThread( HANDLE hThread, LPDWORD lpExitCode ); // nit za koju se traži izlazni kod // DWORD riječ u kojoj će biti smješten kod

Funkcija GetExitCodeThread vraća FALSE ako je greška sprječava da utvrdi povratnu vrijednost. Funkcija ExitThread, bilo da se poziva eksplicitno ili implicitno kao posljedica iskaza return, permanentno uklanja nit iz reda i uništava stek niti. Ipak, ona ne uništava objekat nit, što omogućava upite o izlaznom statusu niti čak i kada nit prestane da se izvršava. Kada je moguće, identifikacioni kodovi bi trebalo da se zatvore eksplicitno (pozivom funkcije CloseHandle) da bi se izbjeglo zauzimanje prostora u memoriji. Sistem završava nit kada su zadovoljena sljedeća dva uslova: − Kada je posljednji identifikacioni kod niti zatvoren; − Kada se nit više ne izvršava. Sistem neće uništiti nit koja se izvršava, čak iako su svi njeni identifikacioni kodovi zatvoreni. Umjesto toga, nit nije uništena sve dok ne prestane da se izvršava. Ipak, ako proces ostavi otvorene identifikacione kodove kada se završava, sistem ih automatski zatvara i uklanja objekte „siročiće“ koje više ne drži ni jedan proces.

strana 30

Funkcijom ExitThread nit se graciozno zaustavlja na mjestu koje je sama izabrala, dok funkcija TerminateThread dozvoljava da jedna nit stopira drugu naglo i proizvoljno:

Kreiranje i sinhronizacija niti

BOOL WINAPI TerminateThread( HANDLE hThread, // nit koju treba stopirati DWORD dwExitCode ); // izlazni kod za nit koja se stopira

Nit se ne može zaštititi od završavanja. Svako ko ima identifikacioni kod niti, takav da je THREAD_TERMINATE zastavica setovana, može primorati nit da se momentalno zaustavi, nezavisno od njenog trenutnog stanja. Korišenje podrazumijevanih atributa sigurnosti u funkciji CreateThread proizvodi identifikacioni kod sa punim privilegijama pristupa. TerminateThread ne uništava stek niti ali obezbjeđuje izlazni kod. I ExitThread i TerminateThread postavljaju objekat niti u njegovo signalizirano stanje, tako da bilo koje druge niti koje čekaju ovu da se završi mogu da nastave rad. Poslije bilo koje od ovih komandi, objekat niti stoji beživotno dok se svi njegovi identifikacioni kodovi ne zatvore.

strana 31

Kreiranje i sinhronizacija niti

4. Sinhronizacija niti

Da bi radili sa nitima, moramo biti u mogućnosti da koordiniramo njihove akcije. Ponekad koordinacija podrazumijeva da treba osigurati da se određene akcije dešavaju u određenom redosljedu. Pored funkcija za kreiranje niti i modifikaciju njihovog prioriteta, Win32 API sadrži funkcije koje mogu primorati niti da čekaju signale od objekata, kao što su fajlovi ili procesi. Podržani su, takođe, specijalni sinhronizacioni objekti, kao što su mutexi i semafori. Funkcije koje čekaju da objekat dostigne svoje signalizirano stanje, najbolje ilustruju kako se koriste sinhronizacioni objekti. Sa jednim setom generičkih komandi za čekanje možemo da čekamo procese, niti, mutexe, semafore, događaje i nekoliko drugih objekata da dostignu svoje signalizirano stanje. Sljedećom komandom čekamo da jedan objekat uključi svoj signal:

DWORD WINAPI WaitForSingleObject( HANDLE hHandle, // objekat koji se čeka DWORD dwMilliseconds // maksimalno vrijeme čekanja );

strana 32

Funkcija WaitForSingleObject dozvoljava niti da suspenduje samu sebe, dok određeni objekat ne da svoj signal. U okviru ove komande nit takođe navodi koliko je dugo spremna da čeka objekat. Da bi vrijeme čekanja bilo neograničeno dugo, potrebno je podesiti interval na INFINITE. Ako je objekat već dostupan, ili ako dostigne svoje signalizirano stanje u okviru naznačenog vremena, funkcija WaitForSingleObject vraća nulu i izvršavanje se nastavlja. Ako interval prođe, a objekat još nije signaliziran, funkcija vraća WAIT_TIMEOUT. Da bi nit čekala nekoliko objekata odjednom, koristimo WaitForMultipleObjects. Možemo podesiti da ova funkcija vraća rezultat čim bilo koji od objekata postane dostupan, ili da čeka dok svi objekti konačno ne dostignu svoje signalizirano stanje. Program koji upravlja događajima mogao bi da podesi niz objekata koji ga interesuju i da reaguje kada neki od njih bude signaliziran.

Kreiranje i sinhronizacija niti

DWORD WINAPI WaitForMultipleObjects( DWORD nCount, const HANDLE* lpHandles, BOOL bWaitAll, DWORD dwMilliseconds ); // broj objekata koje treba čekati // niz identifikacionih kodova objekata // maksimalni period čekanja // TRUE čeka sve, FALSE čeka bilo koji

Ponovo, povratna vrijednost WAIT_TIMEOUT ukazuje na to da je interval prošao i da ni jedan objekat nije signalizirao. Ako je bWaitAll setovan na FALSE uspješna povratna vrijednost koja nosi indikator bilo kojeg elementa, ukazuje na to koji je element u nizu lpHandles bio signaliziran (indeksi su od 0 (nula) do nCount - 1). Ako je bWaitAll setovan na TRUE, funkcija ne vraća kontrolu dok svi indikatori (sve niti) ne budu kompletirani. Dvije proširene verzije funkcija čekanja dodaju status uzbune koji dozvoljava niti da se rezimira ako se asinhrona komanda čitanja ili upisa završi tokom čekanja. Efekat ovih funkcija je takav da one kažu: „Probudite me ako objekat postane dostupan, ako prođe određeno vrijeme ili ako se I/O operacija u pozadini bliži kraju“.
strana 33

Kreiranje i sinhronizacija niti
DWORD WINAPI WaitForSingleObjectEx( HANDLE hHandle, DWORD dwMilliseconds, // maksimalno vrijeme čekanja BOOL bAlertable ); // TRUE da se završi čekanje ako se I/O završi // objekat koji se čeka

DWORD WINAPI WaitForMultipleObjectsEx( DWORD nCount, const HANDLE* lpHandles, BOOL bWaitAll, // broj objekata koji se čeka // niz identifikacionih kodova objekata // TRUE čeka sve, FALSE čeka bilo koji

DWORD dwMilliseconds, // maksimalno vrijeme čekanja BOOL bAlertable );

// TRUE da se završi čekanje ako se I/O završi

Uspješne komande čekanja obično na neki način modifikuju objekat koji se čeka. Na primjer, kada nit čeka mutex i kada ga dobije, funkcija čekanja vraća mutex u njegovo nesignalizirano stanje da bi druge niti znale da se on koristi. Komande čekanja takođe smanjuju vrijednost brojača u semaforu i resetuju neke vrste događaja. Komande čekanja ne modifikuju stanje navedenog objekta dok svi objekti ne budu simultano signalizirani. Na primjer, mutex može biti signaliziran, ali nit ne prima vlasništvo odmah zato što se od nje zahtijeva da čeka dok ostali objekti ne budu takođe signalizirani; zato funkcija čekanja ne može da modifikuje objekat. Pored toga, mutex može da postane vlasništvo druge niti dok čeka, što će još više odložiti završetak stanja čekanja.

Kreiranje mutexa i semafora

Funkcijama za kreiranje mutexa i semafora treba da specifikujemo koje privilegije pristupa želimo, neke prvobitne uslove za objekat i opciono ime za objekat.
strana 34

Kreiranje i sinhronizacija niti
HANDLE WINAPI CreateMutex( LPSECURITY_ATTRIBUTES lpMutexAttributes, BOOL bInitialOwner, LPCTSTR lpName ); // opcioni atributi sigurnosti

// TRUE ako kreator želi vlasništvo // ime objekta

HANDLE WINAPI CreateSemaphore( LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, // opcioni atributi sigurnosti LONG lInitialCount, LONG lMaximumCount, LPCTSTR lpName ); // inicijalni broj (obično 0) // maksimalni broj niti // ime semafora

Ako je deskriptor sigurnosti NULL, vraćeni identifikacioni kod će posjedovati sve privilegije pristupa i neće ga naslijediti procesi djeca. Imena su opciona, ali korisna za identifikaciju samo kada nekoliko različitih procesa želi identifikacione kodove za isti objekat. Postavljanjem indikatora bInitialOwner na TRUE, nit kreira i dobija mutex istovremeno. Novi mutex ostaje nesignaliziran dok ga nit ne oslobodi. Dok samo jedna nit u jednom trenutku može dobiti mutex, semafor ostaje signaliziran dok njegov akvizicioni broj ne dostigne lMaximumCount. Ako još neke niti pokušaju da čekaju semafor, biće suspendovane dok neka druga nit ne smanji akvizicioni broj.

Preuzimanje i oslobađanje mutexa i semafora

Jednom kada je stvoren semafor ili mutex, niti vrše interakciju sa njim tako što ga preuzimaju ili oslobađaju. Da bi preuzela bilo koji od navedenih objekata, nit poziva funkciju WaitForSingleObject (ili neku njenu varijantu). Kada nit završi bilo koji zadatak koji objekat sinhronizuje, ona oslobađa objekat pomoću jedne od sljedećih funkcija:
strana 35

Kreiranje i sinhronizacija niti
BOOL WINAPI ReleaseMutex( HANDLE hMutex ); // mutex koji se oslobađa

BOOL WINAPI ReleaseSemaphore( HANDLE hSemaphore, LONG lReleaseCount, LPLONG lpPreviousCount ); // semafor koji se oslobađa // veličina inkrementa pri oslobađanju (obično 1) // promjenljiva koja prihvata prethodni broj

Oslobađanje semafora ili mutexa inkrementira njegov brojač. Kad god vrijednost brojača pređe nulu, objekat prelazi u svoje signalizirano stanje, a sistem provjerava da li ga čekaju neke druge niti. Samo nit koja već posjeduje mutex, drugim riječima, nit koja je već čekala mutex, može ga osloboditi. Bilo koja nit, ipak, može pozvati ReleaseSemaphore da bi podesila akvizicioni brojač na bilo koju vrijednost, najviše do svoje maksimalne vrijednosti. Promjena vrijednosti brojača za proizvoljne vrijednosti omogućava nam da mijenjamo broj niti koje mogu posjedovati semafor dok se program izvršava. Funkcija CreateSemaphore omogućava da podesimo brojač za novi semafor na neku drugu vrijednost osim maksimalne. Mogli bi, na primjer, da ga kreiramo sa prvobitnom vrijednošću, nulom, kako bi blokirali sve niti dok se program inicijalizuje, a zatim da povećamo vrijednost brojača pomoću ReleaseSemaphore. Nit može čekati isti objekat više od jednom bez blokiranja, ali svako čekanje mora biti propraćeno kasnijim oslobađanjem. Ovo važi za mutexe, semafore i kritične sekcije.

Rad sa događajima

strana 36

Događaj je objekat koji program kreira kada mu je potreban mehanizam za uzbunjivanje niti kada se desi neka akcija. U svojoj najprostijoj formi – događaj ručnog resetovanja – objekat događaj uključuje i isključuje svoj signal kao odgovor na dvije komande SetEvent (signal uključen) i ResetEvent (signal isključen). Kada je signal uključen , sve niti koje čekaju događaj primiće ga. Kada je signal isključen, sve niti koje čekaju događaj postaju blokirane. Za razliku od mutexa i semafora, događaji ručnog resetovanja mijenjaju svoje stanje samo kada ih neka nit eksplicitno setuje ili resetuje. Događaj ručnog resetovanja možemo koristiti da dozvolimo da se određene niti izvršavaju samo kada program ne boji svoj prozor ili samo pošto korisnik unese određene informacije. Slijede osnovne komande za rad sa događajima:

Kreiranje i sinhronizacija niti

HANDLE WINAPI CreateEvent( LPSECURITY_ATTRIBUTES lpEventAttributes, BOOL bManualReset, BOOL bInitialState, // privilegije sigurnosti // TRUE ako događaj mora biti resetovan ručno // TRUE da se kreira događaj u // signaliziranom stanju LPCTSTR lpName ); // ime događaja

BOOL WINAPI SetEvent( HANDLE hEvent ); // događaj koji treba setovati

BOOL WINAPI ResetEvent( HANDLE hEvent ); // događaj koji treba resetovati

Korišćenjem parametra bInitialState, CreateEvent omogućava da se novi događaj (objekat) kreira već signaliziran. Funkcije SetEvent i ResetEvent vraćaju TRUE ili FALSE da ukažu na uspjeh ili neuspjeh. Korišćenjem parametra bManualReset, CreateEvent dozvoljava da kreiramo automatski događaj resetovanja umjesto ručnog događaja resetovanja. Automatski događaj resetovanja vraća se u svoje nesignalizirano stanje odmah poslije komande SetEvent i zato je komanda ResetEvent suvišna.
strana 37

Kreiranje i sinhronizacija niti
Automatsko resetovanje uvijek oslobađa samo jednu nit na svaki signal prije resetovanja. Automatski događaj resetovanja bi mogao biti koristan za program gdje jedna glavna nit priprema podatke za druge, radne, niti. Svaki put kada je novi niz podataka spreman, glavna nit setuje događaj i jedna radna nit se oslobađa. Druge radne niti nastavljaju da čekaju u redu da bi dobile nove zadatke. Pored setovanja i resetovanja, događaj možemo i da pulsiramo:

BOOL WINAPI PulseEvent( HANDLE hEvent ); // događaj koji treba pulsirati

Puls uključuje signal za veoma kratko vrijeme i ponovo ga isključuje. Pulsiranje ručnog događaja omogućava svim nitima koje čekaju da prođu, i onda resetuje događaj. Pulsiranje automatskog događaja dozvoljava jednoj niti koja čeka da prođe, i onda resetuje događaj. Ako ni jedna nit ne čeka, nijedna neće ni proći. Setovanje automatskog događaja, sa druge strane, prouzrokovaće da događaj ostavi svoj signal uključen dok ga neka nit čeka. Čim jedna nit prođe, događaj se sam resetuje.

Dioba i uništavanje mutexa, semafora i događaja

Procesi, čak i nevezani procesi, mogu dijeliti mutexe, semafore i događaje. Diobom objekata procesi mogu da koordiniraju svoje aktivnosti, kao što to mogu niti. Postoje tri mehanizma za diobu. Jedan je nasljeđivanje, kada jedan proces kreira drugi, i novi proces dobije kopije identifikacionih kodova roditelja. Samo ti identifikacioni kodovi, koji su markirani za nasljeđivanje, kada su kreirani, biće proslijeđeni.
strana 38

Drugi metodi uključuju pozivanje funkcija za kreiranje drugog identifikacionog koda za postojeći objekat. Koju funkciju pozivamo zavisi od toga koje informacije već imamo. Ako imamo identifikacione kodove i za izvorni i za odredišni proces, koristimo funkciju DuplicateHandle. Ako imamo ime objekta koristimo neku od Open funkcija. Dva programa mogu se složiti unaprijed kada su u pitanju imena objekata koje dijele, ili jedan može proslijediti drugom ime objekta preko zajedničke memorije ili kanala.

Kreiranje i sinhronizacija niti

BOOL WINAPI DuplicateHandle( HANDLE hSourceProcessHandle, HANDLE hSourceHandle, HANDLE hTargetProcessHandle, // proces koji posjeduje originalni objekat // identifikacioni kod za originalni objekat // proces koji želi kopiju // identifikacionog koda LPHANDLE lpTargetHandle, // promjenljiva za smještanje duplikata // identifikacionog koda DWORD dwDesiredAccess, BOOL bInheritHandle, // zahtijevane privilegije pristupa // da li duplikat identifikacionog koda // može biti naslijeđen DWORD dwOptions ); // opcione akcije (zastavice)

HANDLE WINAPI OpenMutex( DWORD dwDesiredAccess, BOOL bInheritHandle, // zahtijevane privilegije pristupa // TRUE ako djeca mogu da naslijede // ovaj identifikacioni kod LPCTSTR lpName ); // ime mutexa

HANDLE WINAPI OpenSemaphore( DWORD dwDesiredAccess, BOOL bInheritHandle, // zahtijevane privilegije pristupa // TRUE ako djeca mogu da naslijede // ovaj identifikacioni kod LPCTSTR lpName ); // ime semafora

HANDLE WINAPI OpenEvent( DWORD dwDesiredAccess, BOOL bInheritHandle, // zahtijevane privilegije pristupa // TRUE ako djeca mogu da naslijede // ovaj identifikacioni kod

strana 39

LPCTSTR lpName );

Kreiranje i sinhronizacija niti
// ime događaja

Mutexi, semafori i događaji ostaju u memoriji dok se svi procesi koji ih posjeduju ne završe, ili dok se svi identifikacioni kodovi objekata ne zatvore pomoću CloseHandle.

BOOL WINAPI CloseHandle( HANDLE hObject ); // identifikcioni kod objekta koji treba zatvoriti

Rad sa kritičnim sekcijama

Objekat kritična sekcija obavlja u potpunosti istu funkciju kao mutex, osim što se kritična sekcija ne može dijeliti. Ona je vidljiva samo u okviru jednog procesa. I kritične sekcije i mutexi dozvoljavaju da ih posjeduje samo jedna nit u jednom trenutku, ali kritične sekcije rade mnogo brže i imaju manje zaglavlje. Funkcije za rad sa kritičnim sekcijama ne koriste istu terminologiju, kao funkcije za rad sa mutexima, ali rade uglavnom iste stvari. Prvo se deklariše objekat tipa kritična sekcija:

CRITICAL_SECTION cs;

Objekat CRITICAL_SECTION definisan je na sljedeći način:

typedef struct _RTL_CRITICAL_SECTION { PRTL_CRITICAL_SECTION_DEBUG DebugInfo; LONG LockCount; LONG RecursionCount; HANDLE OwningThread;

strana 40

HANDLE LockSemaphore; ULONG_PTR SpinCount;

Kreiranje i sinhronizacija niti

} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;

typedef RTL_CRITICAL_SECTION CRITICAL_SECTION, *LPCRITICAL_SECTION;

Prije upotrebe objekta kritična sekcija, potrebno ga je inicijalizovati pomoću funkcije:

void WINAPI InitializeCriticalSection( LPCRITICAL_SECTION lpCriticalSection );

Tip promjenljive LPCRITICAL_SECTION imenuje pokazivač (ne identifikacioni kod) na objekat kritična sekcija.

void WINAPI DeleteCriticalSection( LPCRITICAL_SECTION lpCriticalSection );

Za razliku od ostalih objekata koji su čekali neku od wait funkcija, objekat kritična sekcija radi na drugačiji način. Pomoću funkcije:

void WINAPI EnterCriticalSection( LPCRITICAL_SECTION lpCriticalSection );

objekat kritična sekcija ulazi u svoje nesignalizirano stanje. Pošto završi posao, pozivom funkcije:

void WINAPI LeaveCriticalSection( LPCRITICAL_SECTION lpCriticalSection );

strana 41

objekat kritična sekcija prelazi u svoje signalizirano stanje. pozivom funkcije:

Kreiranje i sinhronizacija niti

Kad nit završi rad sa objektom kritična sekcija, potrebno je da ga uništi i to radi

void WINAPI DeleteCriticalSection( LPCRITICAL_SECTION lpCriticalSection );

strana 42

Kreiranje i sinhronizacija niti

5. Primjer sa višestrukim nitima

Programom prikazanim na slici 5 obuhvaćeno je nekoliko ideja koje su obrađivane u ovom radu. Program određuje broj procesora (logičkih jezgara) u vrijeme izvršavanja i kreira isto toliki broj sekundarnih niti, a svaka od njih nasumice crta obojene pravougaonike nasumične veličine u prozoru djetetu dok se program ne završi.

Slika 5. Program „Niti“
strana 43

Kreiranje i sinhronizacija niti
Vrh prozora sadrži polje za listu koja prikazuje informacije o svim nitima. Odabirom niti i biranjem komande menija možemo suspendovati, rezimirati i promijeniti prioritet odabrane niti. Odabirom komande Control možemo aktivirati Mutex, tako da samo jedna nit crta u jednom trenutku.

Procedure za inicijalizaciju

Procedure za inicijalizaciju registruju dvije klase prozora: jednu za glavni, preklapajući, prozor i jednu za prozore djecu gdje niti crtaju; a takođe kreiraju i tajmer. U intervalima od 250 milisekundi, polje sa listom ažurira informacije o svakoj niti. Funkcija CreateWindows kreira i pozicionira sve prozore, uključujući i polje sa listom koja prikazuje informacije o svakoj niti. Pored registrovanja klase prozora (primarne niti) aplikacije i praćenja uobičajne procedure za instalaciju, procedura InitInstance podešava prioritet niti i startuje sve sekundarne niti u suspendovanom stanju.

BOOL InitInstance( HINSTANCE hInstance, int nCmdShow ) { // ... // označimo prvobitno stanje niti kao SUSPENDED for ( int i = 0; i < THREAD_COUNT; i++ ) { iState[ i ] = SUSPENDED; } // ... // podesimo da primarna nit bude važnija da bi se olakšao ulaz/izlaz SetThreadPriority( GetCurrentThread( ), THREAD_PRIORITY_ABOVE_NORMAL ); // ... return CreateWindows( hwndMain ); }

strana 44

Kreiranje i sinhronizacija niti
Poziv SetThreadPriority povećava prioritet primarne niti. Ako su sve sekundarne niti bile zauzete, radeći istim proritetom kao glavna nit, meniji bi reagovali sporije. Funkcija CreateWindows kreira glavni prozor, polje za listu i seriju prozora djece za niti.

BOOL CreateWindows( HWND hwndParent ) { // ... // kreiramo list view hwndListView = CreateWindowEx( WS_EX_OVERLAPPEDWINDOW, WC_LISTVIEW, NULL, WS_BORDER | WS_CHILD | WS_VISIBLE | LVS_SINGLESEL | LVS_REPORT | LVS_SHOWSELALWAYS, 0, 0, 0, 0, hwndParent, ( HMENU ) ID_LISTVIEW, gInstance, NULL ); if( ! hwndListView ) { return( FALSE ); } // ... WNDCLASSEX wcex; wcex.cbSize wcex.style wcex.lpfnWndProc wcex.cbClsExtra wcex.cbWndExtra wcex.hInstance wcex.hIcon wcex.hCursor wcex.hbrBackground wcex.lpszMenuName wcex.lpszClassName wcex.hIconSm = = = = = = = = = = = = sizeof( WNDCLASSEX ); CS_HREDRAW | CS_VREDRAW; ThreadWndProc; 0; 0; gInstance; LoadIcon( gInstance, MAKEINTRESOURCE(IDI_NITI) ); LoadCursor( NULL, IDC_ARROW ); ( HBRUSH ) ( COLOR_WINDOW + 1 ); MAKEINTRESOURCE( IDC_NITI ); lpThreadClassName; LoadIcon( gInstance, MAKEINTRESOURCE(IDI_NITI) );

if ( ! RegisterClassEx( &wcex ) ) { return FALSE; } // ... // kreiramo 4 prozora for ( iCount = 0; iCount < THREAD_COUNT; iCount++ ) { hwndChild[ iCount ] = CreateWindow( lpThreadClassName, NULL, WS_BORDER | WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN, 0, 0, 0, 0, hwndParent, NULL, gInstance, NULL ); if ( ! hwndChild[ iCount ] ) { return( FALSE ); } }

strana 45

Kreiranje i sinhronizacija niti
// ... return( TRUE ); }

Procedure za rad sa prozorima i porukama

Main_OnTimer poziva proceduru UpdateListView da raščisti polje sa listom, generiše nove stringove za informacije i vrši prikaz istih. Funkcija Main_OnSize suspenduje sve sekundarne niti dok program mijenja poziciju prozora djece da bi ta pozicija odgovarala novoj veličini. U suprotnom, zauzete niti usporavale bi operaciju prikaza. Pored kreiranja niti OnCreate kreira mutex i kompletira proces inicijalizacije niti i proces podešavanja sinhronizacije mutexa.

BOOL OnCreate( HWND hWnd ) { UINT uRet; PROCESSOR_NUMBER previousProcessorNumber = { 0 }; PROCESSOR_NUMBER processorNumber = { 0 }; // kreiramo 4 niti , inicijalno suspendovane for ( iCount = 0; iCount < THREAD_COUNT; iCount++ ) { iRectCount[ iCount ] = 0; dwThreadData[ iCount ] = iCount; hThread[ iCount ] = CreateThread( NULL, 0, (LPTHREAD_START_ROUTINE) StartThread, ( LPVOID ) ( &( dwThreadData[ iCount ] ) ), CREATE_SUSPENDED, ( LPDWORD ) ( & ( dwThreadID[ iCount ] ) ) ); processorNumber.Number = iCount; SetThreadIdealProcessorEx( hThread[ iCount ], &processorNumber, &previousProcessorNumber ); if( ! hThread[ iCount ] ) { // ako nit nije kreirana greska - izadji return( FALSE ); } } // Kreiramo tajmer sa 5 sekundi periodom. // Tajmer koristimo za update list view-a. uRet = ( UINT ) SetTimer( hWnd, TIMER, 100, NULL );

strana 46

if ( ! uRet ) { return( FALSE ); }

Kreiranje i sinhronizacija niti
// nije bilo moguce kreirati tajmer

// kreiramo sinhronizacioni objekat mutex hDrawMutex = CreateMutex( NULL, FALSE, NULL ); if ( ! hDrawMutex ) { // nije bilo moguce kreirati mutex KillTimer( hWnd, TIMER ); // zaustavi tajmer return( FALSE ); } // startujemo niti sa prioritetom below normal for ( iCount = 0; iCount < THREAD_COUNT; iCount++ ) { SetThreadPriority( hThread[ iCount ],THREAD_PRIORITY_BELOW_NORMAL); iState[ iCount ] = ACTIVE; ResumeThread( hThread[ iCount ] ); } RECT rect; GetClientRect( hWnd, &rect ); Main_OnSize( hWnd, 0, rect.right, rect.bottom ); return( TRUE ); } void Main_OnTimer( HWND hWnd, UINT uTimerID ) { UpdateListView( ); // update list view return; } void Main_OnSize( HWND hWnd, UINT uState, int cxClient, int cyClient { // suspendujemo sve aktivne niti da bi se obavile // izmjene u poziciji i velicini prozora for ( iCount = 0; iCount < THREAD_COUNT; iCount++ ) { if ( iState[ iCount ] == ACTIVE ) { SuspendThread( hThread[ iCount ] ); iState[ iCount ] = SUSPENDED; } } // postavimo list view na odradjenoj poziciji MoveWindow( hwndListView, 0, 0, cxClient, LISTVIEW_HEIGHT, TRUE ); MoveWindow( hwndChild[ 0 ], 0, LISTVIEW_HEIGHT - 1, cxClient / THREAD_COUNT + 1, cyClient, TRUE ); for ( iCount = 1; iCount < THREAD_COUNT; iCount++ ) { MoveWindow( hwndChild[ iCount ], (iCount*cxClient) / THREAD_COUNT, LISTVIEW_HEIGHT-1, cxClient/THREAD_COUNT + 1, cyClient, TRUE ); } for ( iCount = 0; iCount < THREAD_COUNT; iCount++ ) { ) // sada se sve niti izvrsavaju!

strana 47

iRectCount[ iCount ] = 0; }

Kreiranje i sinhronizacija niti

// rezimiramo niti for ( iCount = 0; iCount < THREAD_COUNT; iCount++ ) { if ( iState[ iCount ] == SUSPENDED ) { ResumeThread( hThread[ iCount ] ); iState[ iCount ] = ACTIVE; } } return; }

Procedure za modifikaciju

Procedura DoThread odgovara na komande menija tako što modifikuje nit odabranu iz liste. DoThread može promijeniti prioritet niti, suspendovati je ili rezimirati. Niz iState snima trenutno stanje svake niti (aktivno ili suspendovano). Niz hThreads sadrži identifikacione kodove svake sekundarne niti.

void DoThread( int iCmd ) { int iThread; int iPriority; // sacuvaj indeks niti iz list view-a iThread = ListView_GetNextItem(hwndListView, -1, LVNI_SELECTED ); switch( iCmd ) { case IDM_SUSPEND: { if( iState[ iThread ] != SUSPENDED ) { SuspendThread( hThread[ iThread ] ); iState[ iThread ] = SUSPENDED; } break; } case IDM_RESUME: { if( iState[ iThread ] != ACTIVE ) { ResumeThread( hThread[ iThread ] ); iState[ iThread ] = ACTIVE;

strana 48

} break;

Kreiranje i sinhronizacija niti

} case IDM_INCREASE: { iPriority = GetThreadPriority( hThread[ iThread ] ); switch( iPriority ) { case THREAD_PRIORITY_LOWEST: { SetThreadPriority( hThread[ iThread ], THREAD_PRIORITY_BELOW_NORMAL ); break; } case THREAD_PRIORITY_BELOW_NORMAL: { SetThreadPriority( hThread[ iThread ], THREAD_PRIORITY_NORMAL ); break; } case THREAD_PRIORITY_NORMAL: { SetThreadPriority( hThread[ iThread ], THREAD_PRIORITY_ABOVE_NORMAL ); break; } case THREAD_PRIORITY_ABOVE_NORMAL: { SetThreadPriority( hThread[ iThread ], THREAD_PRIORITY_HIGHEST ); break; } default: { break; } } break; } case IDM_DECREASE: { iPriority = GetThreadPriority( hThread[iThread] ); switch( iPriority ) { case THREAD_PRIORITY_BELOW_NORMAL: { SetThreadPriority( hThread[ iThread ], THREAD_PRIORITY_LOWEST ); break; } case THREAD_PRIORITY_NORMAL: { SetThreadPriority( hThread[iThread], THREAD_PRIORITY_BELOW_NORMAL ); break; } case THREAD_PRIORITY_ABOVE_NORMAL: { SetThreadPriority( hThread[iThread], THREAD_PRIORITY_NORMAL ); break; }

strana 49

case THREAD_PRIORITY_HIGHEST: { SetThreadPriority( hThread[iThread], THREAD_PRIORITY_ABOVE_NORMAL ); break; } default: { break; } } break; } default: { break; } } return; }

Kreiranje i sinhronizacija niti

Procedure za niti

Kada funkcija OnCreate konstruiše sekundarne niti, prosljeđuje pokazivač na funkciju StartThread pri svakom pozivu funkcije CreateThread. StartThread postaje glavna procedura za sve sekundarne niti i tačka gdje one počinju i završavaju se. Ako je bUseMutex TRUE, niti će čekati da dobiju mutex prije nego što počnu da crtaju, a samo jedna nit će crtati u jednom trenutku.

DWORD WINAPI StartThread( LPVOID lpThreadData ) { DWORD dwWait; // povratna vrijednost za WaitSingleObject // sacuvamo ID niti LPDWORD pdwThreadID = ( LPDWORD ) lpThreadData; // niti crtaju sve dok je bTerminate razlicito od TRUE while ( ! bTerminate ) { if ( bUseMutex ) // da li koristimo mutex? { // nit ceka mutex, a zatim crta dwWait = WaitForSingleObject( hDrawMutex, INFINITE ); if( dwWait == 0 ) { DrawProc( *pdwThreadID ); // crtamo pravoguaonike Sleep( GetSleepTime( GetThreadPriority( hThread[ *pdwThreadID ] ) ) );

strana 50

ReleaseMutex( hDrawMutex ); // nit oslobadja mutex } } else // ne koristimo mutex – pocni crtanje { DrawProc( *pdwThreadID ); Sleep( GetSleepTime( GetThreadPriority( hThread[ *pdwThreadID ] ) ) ); } } // ova naredba implicitno poziva ExitThread. return 0L; }

Kreiranje i sinhronizacija niti

Nakon poziva funkcije DrawProc svakoj niti se određuje vrijeme spavanja u odnosu na prioritet iste. Ovo je bitno iz razloga što bi niti inače veoma brzo završavale svoj posao – crtanje (red veličine je manje od milisekunde), pa gledajući izvršavanje programa ne bi vidjeli kako se posao stvarno odvija. Zato funkcija GetSleepTime računa vrijeme spavanja u odnosu na prioritet niti.

int GetSleepTime( int iPriority ) { switch ( iPriority ) { case THREAD_PRIORITY_LOWEST: { return 2000; } case THREAD_PRIORITY_BELOW_NORMAL: { return 1000; } case THREAD_PRIORITY_NORMAL: { return 500; } case THREAD_PRIORITY_ABOVE_NORMAL: { return 250; } case THREAD_PRIORITY_HIGHEST: { return 0; } } return INFINITE; }

strana 51

Kreiranje i sinhronizacija niti

Zaključak

Sinhronizacija niti nije ni malo jednostavan zadatak, posebno kada su u pitanju veliki sistemi gdje čitave cjeline moraju koordinirano dijeliti resurse. A uz to i same cjeline unutar sebe moraju sinhronizovati zadatke (niti) tako da ne smetaju jedna drugoj. U ovom radu obrađeni su samo osnovni koncepti navedene tematike – koliko da se predstavi zadatak sinhronizacije i pokažu mehanizmi kojima se sinhronizacija sprovodi.

strana 52

Kreiranje i sinhronizacija niti

Literatura

1. Modern Operating Systems 3rd Edition, Andrew S. Tanenbaum 2. Operating System Concepts, Abraham Silberschatz 3. Mastering Windows 2000 – Programming in C++, Ben Ezzell 4. MSDN, Microsoft Developer Network

strana 53