Stanja niti. Kritični odjeljci Završetak sinkronizacije u Windows OS-u

Ovaj objekt sinkronizacije može se koristiti samo lokalno unutar procesa koji ga je stvorio. Ostali objekti mogu se koristiti za sinkronizaciju niti različitih procesa. Naziv objekta "kritični odjeljak" povezan je s nekim apstraktnim odabirom dijela programskog koda (odjeljka) koji izvodi neke operacije čiji se redoslijed ne može povrijediti. To jest, pokušaj dvije različite niti da istovremeno izvrše kod ovog odjeljka rezultirat će pogreškom.

Na primjer, može biti prikladno zaštititi funkcije pisaca s takvim odjeljkom, budući da bi istovremeni pristup nekoliko pisaca trebao biti isključen.

Za kritičnu sekciju uvode se dvije operacije:

unesite odjeljak Dok je bilo koja nit u kritičnom odjeljku, sve druge niti će automatski prestati čekati kada pokušaju ući u nju. Nit koja je već ušla u ovaj odjeljak može ući u njega više puta bez čekanja da se oslobodi.

napusti odjeljak Kada nit napusti odjeljak, brojač broja ulazaka te niti u odjeljak se dekrementira, tako da će odjeljak biti oslobođen za druge niti samo ako nit izađe iz odjeljka onoliko puta koliko je u njega ušla. Kada se kritični odjeljak oslobodi, samo će se jedna nit probuditi, čekajući dopuštenje za ulazak u ovaj odjeljak.

Općenito govoreći, u drugim API-jima koji nisu Win32 (kao što je OS/2), kritični odjeljak se ne tretira kao sinkronizacijski objekt, već kao dio programskog koda koji može izvršiti samo jedna nit aplikacije. Odnosno, ulazak u kritični odjeljak smatra se privremenim gašenjem mehanizma za prebacivanje niti do izlaska iz ovog odjeljka. Win32 API kritične odjeljke tretira kao objekte, što dovodi do zabune -- oni su po svojim svojstvima vrlo bliski neimenovanim ekskluzivnim objektima ( mutex, Pogledaj ispod).

Kada koristite kritične dijelove, morate paziti da ne dodijelite prevelike fragmente koda u dijelu, jer to može dovesti do značajnih kašnjenja u izvršavanju drugih niti.

Na primjer, u odnosu na već razmatrane gomile, nema smisla štititi sve funkcije gomile kritičnom sekcijom, budući da se funkcije čitača mogu izvršavati paralelno. Štoviše, korištenje kritičnog odjeljka čak i za sinkronizaciju pisaca zapravo se čini nezgodnim - jer da bi se pisac sinkronizirao s čitateljima, potonji će ipak morati ući u ovaj odjeljak, što praktički dovodi do zaštite svih funkcija jednim odjeljak.

Postoji nekoliko slučajeva učinkovite upotrebe kritičnih odjeljaka:

čitatelji se ne sukobljavaju s piscima (samo pisce treba zaštititi);

sve niti imaju približno jednaka prava pristupa (recimo, ne možete izdvojiti čiste pisce i čitatelje);

prilikom konstruiranja složenih sinkronizacijskih objekata, koji se sastoje od nekoliko standardnih, radi zaštite sekvencijalnih operacija na složenom objektu.

U prethodnim dijelovima članka govorio sam o generalni principi i specifične metode za izgradnju višenitnih aplikacija. Različite niti gotovo uvijek povremeno moraju međusobno komunicirati, a potreba za sinkronizacijom neizbježno se javlja. Danas ćemo pogledati najvažniji, najmoćniji i najsvestraniji alat za sinkronizaciju sustava Windows: objekte sinkronizacije kernela.

WaitForMultipleObjects i druge funkcije čekanja

Kao što se sjećate, da biste sinkronizirali niti, obično trebate privremeno obustaviti izvođenje jedne od niti. Međutim, mora se prevesti sredstvima operacijski sustav u stanje čekanja gdje ne oduzima CPU vrijeme. Već znamo dvije funkcije koje to mogu učiniti: SuspendThread i ResumeThread. Ali kao što sam rekao u prethodnom dijelu članka, zbog nekih značajki ove funkcije nisu prikladne za sinkronizaciju.

Danas ćemo pogledati drugu funkciju koja također stavlja nit u stanje čekanja, ali za razliku od SuspendThread/ResumeThread, ona je posebno dizajnirana za organiziranje sinkronizacije. To je WaitForMultipleObjects. Budući da je ova značajka toliko važna, malo ću odstupiti od svog pravila da ne ulazim u detalje API-ja i govoriti o njemu detaljnije, čak ću dati njegov prototip:

DWORD WaitForMultipleObjects (

DWORD nBroj , // broj objekata u nizu lpHandles

CONST RUČKA * lpHandles , // pokazivač na niz deskriptora objekta jezgre

BOOL bČekajSve , // zastavica koja označava treba li čekati sve objekte ili je dovoljan samo jedan

DWORD dwMilisekunde // pauza

Glavni parametar ove funkcije je pokazivač na niz ručica objekata jezgre. U nastavku ćemo govoriti o tome koji su to objekti. Za sada nam je važno znati da bilo koji od ovih objekata može biti u jednom od dva stanja: neutralnom ili "signalnom" (signalizirano stanje). Ako je zastavica bWaitAll FALSE, funkcija će se vratiti čim barem jedan od objekata da signal. A ako je zastavica TRUE, to će se dogoditi samo kada svi objekti počnu signalizirati odjednom (kao što ćemo vidjeti, ovo je najvažnije svojstvo ove funkcije). U prvom slučaju, prema vraćenoj vrijednosti, možete saznati koji je od objekata dao signal. Trebate od njega oduzeti konstantu WAIT_OBJECT_0 i dobit ćete indeks u nizu lpHandles. Ako vremensko ograničenje premaši vremensko ograničenje navedeno u posljednjem parametru, funkcija će prestati čekati i vratiti vrijednost WAIT_TIMEOUT. Kao timeout možete navesti konstantu INFINITE , a zatim će funkcija čekati "dok ne stane", ili možete obrnuto 0, a tada nit uopće neće biti obustavljena. U potonjem slučaju, funkcija će se odmah vratiti, ali njen rezultat će vam reći stanje objekata. Posljednja tehnika se vrlo često koristi. Kao što vidite, ova funkcija ima bogate mogućnosti. Postoji nekoliko drugih funkcija WaitForXXX, ali sve su one varijacije na glavnu temu. Konkretno, WaitForSingleObject samo je njegova pojednostavljena verzija. Svaki od ostalih ima svoju dodatnu funkcionalnost, ali se općenito rjeđe koristi. Na primjer, oni omogućuju odgovor ne samo na signale iz objekata jezgre, već i na dolazak novih prozorskih poruka u red čekanja niti. Njihov opis, kao i detaljne informacije o WaitForMultipleObjects, pronaći ćete, kao i obično, u MSDN-u.

Sada o tome što su ti misteriozni "objekti jezgre". Za početak, to uključuje same niti i procese. Oni ulaze u stanje signalizacije odmah po završetku. Ovo je vrlo važna značajka jer je često potrebno pratiti kada je nit ili proces prekinut. Neka, na primjer, naša poslužiteljska aplikacija sa skupom radnih niti treba biti dovršena. U isto vrijeme, kontrolna nit mora na neki način obavijestiti radne niti da je vrijeme za završetak rada (na primjer, postavljanjem globalne zastavice), a zatim pričekati dok sve niti ne završe, radeći sve što je potrebno za ispravan završetak akcije: oslobađanje resursa, obavještavanje klijenata o gašenju, zatvaranju mrežnih veza itd.

Činjenica da dretve uključuju signal na kraju rada, izuzetno je jednostavno riješiti problem sinkronizacije s završetkom dretve:

// Radi jednostavnosti, uzmimo samo jednu radnu nit. Pokrenimo ga:

HANDLE hWorkerThread = :: Stvori nit (...);

// Prije kraja rada, moramo nekako reći radnoj niti da je vrijeme za upload.

// Pričekajte da nit završi:

DWORD dwWaitResult = :: WaitForSingleObject ( hWorkerThread , BESKONAČNO );

ako( dwWaitResult != WAIT_OBJECT_0 ) { /* obrada grešaka */ }

// "Ručka" streama može se zatvoriti:

POTVRDI (:: CloseHandle ( hWorkerThread );

/* Ako CloseHandle ne uspije i vrati FALSE, ne izbacujem iznimku. Prvo, čak i da se to dogodilo zbog sistemske greške, to ne bi imalo izravnih posljedica za naš program, jer budući da zatvorimo ručicu, onda se u budućnosti ne očekuje rad s njim. U stvarnosti, kvar CloseHandlea može značiti samo grešku u vašem programu. Stoga ćemo ovdje umetnuti makronaredbu VERIFY kako je ne bismo propustili u fazi otklanjanja pogrešaka aplikacije. */

Kôd koji čeka da proces završi izgledat će slično.

Da ne postoji takva ugrađena mogućnost, radna nit bi morala nekako proslijediti informacije o svom završetku samoj glavnoj niti. Čak i da je ovo zadnje učinila, glavna nit ne može biti sigurna da radniku nije preostalo barem nekoliko asemblerskih instrukcija za izvršenje. NA pojedinačne situacije(na primjer, ako je kod niti u DLL-u koji se mora isprazniti kada završi) to može biti fatalno.

Želim vas podsjetiti da čak i nakon što je nit (ili proces) prekinuta, njezine oznake ostaju na snazi ​​sve dok ih funkcija CloseHandle izričito ne zatvori. (Usput, ne zaboravite to učiniti!) Ovo se radi samo kako biste u bilo kojem trenutku mogli provjeriti status niti.

Dakle, funkcija WaitForMultipleObjects (i njezini analozi) omogućuje vam sinkronizaciju izvršavanja niti sa stanjem objekata sinkronizacije, posebno drugih niti i procesa.

Posebni objekti jezgre

Prijeđimo na razmatranje objekata jezgre, koji su dizajnirani posebno za sinkronizaciju. To su događaji, semafori i muteksi. Pogledajmo ukratko svaki od njih:

događaj

Možda najjednostavniji i najtemeljniji objekt za sinkronizaciju. Ovo je samo zastavica koja se može postaviti funkcijama SetEvent / ResetEvent: signaliziranje ili neutralno. Događaj je najprikladniji način da signalizirate niti koja čeka da se dogodio neki događaj (zato se i zove) i možete nastaviti s radom. Pomoću događaja možemo lako riješiti problem sinkronizacije prilikom inicijalizacije radne niti:

// Zadržimo ručicu događaja u globalnoj varijabli radi jednostavnosti:

HANDLE g_hEventInitComplete = NULL ; // nikada ne ostavljajte varijablu neinicijaliziranu!

{ // kod u glavnoj niti

// kreiraj događaj

g_hEventInitComplete = :: CreateEvent ( NULL,

NETOČNO , // o ovom parametru ćemo govoriti kasnije

NETOČNO , // početno stanje - neutralno

ako(! g_hEventInitComplete ) { /* Ne zaboravite na obradu grešaka */ }

// kreirati radnu nit

DWORD idWorkerThread = 0 ;

HANDLE hWorkerThread = :: Stvori nit ( NULL , 0 , & WorkerThreadProc , NULL , 0 , & idWorkerThread );

ako(! hWorkerThread ) { /* obrada grešaka */ }

// čekati signal iz radne niti

DWORD dwWaitResult = :: WaitForSingleObject ( g_hEventInitComplete , BESKONAČNO );

ako( dwWaitResult != WAIT_OBJECT_0 ) { /* pogreška */ }

// sada možete biti sigurni da je radna nit završila inicijalizaciju.

POTVRDI (:: CloseHandle ( g_hEventInitComplete )); // ne zaboravite zatvoriti nepotrebne objekte

g_hEventInitComplete = NULL ;

// funkcija tijeka rada

DWORD WINAPI WorkerThreadProc ( LPVOID_parametar )

InitializeWorker (); // inicijalizacija

// signalizira da je inicijalizacija završena

BOOL je u redu = :: Postavi događaj ( g_hEventInitComplete );

ako(! je u redu ) { /* pogreška */ }

Treba napomenuti da postoje dvije izrazito različite varijante događaja. Jedan od njih možemo odabrati pomoću drugog parametra funkcije CreateEvent. Ako je TRUE, kreira se događaj čije se stanje kontrolira samo ručno, odnosno funkcijama SetEvent/ResetEvent. Ako je FALSE, generirat će se događaj automatskog resetiranja. To znači da čim se neka nit koja čeka na određeni događaj oslobodi signalom iz tog događaja, automatski će se vratiti u neutralno stanje. Njihova razlika je najizraženija u situaciji kada više niti čeka jedan događaj odjednom. Ručno kontrolirani događaj je poput startnog pištolja. Čim se postavi u signalizirano stanje, sve će se niti odjednom osloboditi. Događaj automatskog resetiranja, s druge strane, je poput okretišta podzemne željeznice: pustit će samo jedan protok i vratiti se u neutralno stanje.

Mutex

U usporedbi s događajem, ovo je specijaliziraniji objekt. Obično se koristi za rješavanje uobičajenog problema sinkronizacije kao što je pristup resursu koji dijeli više niti. Na mnoge je načine sličan događaju automatskog resetiranja. Glavna razlika je u tome što ima posebno vezanje za određenu nit. Ako je mutex u signaliziranom stanju, to znači da je slobodan i da ne pripada niti jednoj niti. Čim određena nit čeka ovaj mutex, potonji se vraća u neutralno stanje (ovdje je kao kod događaja automatskog resetiranja), a nit postaje njezin vlasnik sve dok eksplicitno ne oslobodi mutex funkcijom ReleaseMutex, ili prestaje. Stoga, kako bismo bili sigurni da samo jedna nit radi s dijeljenim podacima u isto vrijeme, sva mjesta na kojima se odvija takav rad trebaju biti okružena parom: WaitFor - ReleaseMutex :

HANDLE g_hMutex ;

// Neka ručka muteksa bude pohranjena u globalnoj varijabli. Naravno, mora se kreirati unaprijed, prije početka radnih niti. Pretpostavimo da je to već učinjeno.

intčekam = :: WaitForSingleObject ( g_hMutex , BESKONAČNO );

sklopka(čekam ) {

slučaj WAIT_OBJECT_0 : // Sve je u redu

pauza;

slučaj WAIT_BANDONED : /* Neka nit je završila, zaboravivši pozvati ReleaseMutex. Najvjerojatnije to znači grešku u vašem programu! Stoga ćemo, za svaki slučaj, ovdje ubaciti ASSERT, ali u konačnoj verziji (izdanju) smatrat ćemo ovaj kod uspješnim. */

TVRDITI ( lažno );

pauza;

zadano:

// Rukovanje pogreškama bi trebalo biti ovdje.

// Dio koda zaštićen mutexom.

ProcessCommonData ();

POTVRDI (:: ReleaseMutex ( g_hMutex ));

Zašto je mutex bolji od događaja automatskog poništavanja? U gornjem primjeru se također može koristiti, samo bi ReleaseMutex morao biti zamijenjen sa SetEvent. Međutim, može se pojaviti sljedeća poteškoća. Najčešće morate raditi s dijeljenim podacima na nekoliko mjesta. Što se događa ako ProcessCommonData u našem primjeru pozove funkciju koja radi s istim podacima i koja već ima vlastiti par WaitFor - ReleaseMutex (u praksi je to vrlo često)? Ako bismo koristili događaj, program bi očito visio, jer je unutar zaštićenog bloka događaj u neutralnom stanju. Mutex je složeniji. Uvijek ostaje u stanju signalizacije za glavnu nit, iako je u neutralnom stanju za sve ostale dretve. Stoga, ako je nit stekla mutex, ponovno pozivanje funkcije WaitFor neće blokirati. Štoviše, brojač je također ugrađen u mutex, tako da se ReleaseMutex mora pozvati isti broj puta koliko je bilo poziva WaitFor. Stoga možemo sigurno zaštititi svaki dio koda koji radi s dijeljenim podacima s parom WaitFor - ReleaseMute x bez brige da se ovaj kod može rekurzivno pozvati. To mutex čini vrlo lakim alatom za korištenje.

Semafor

Još specifičniji objekt sinkronizacije. Moram priznati da u mojoj praksi još nije bilo slučaja u kojem bi to bilo korisno. Semafor je dizajniran da ograniči najveći broj niti koje mogu raditi na resursu u isto vrijeme. U biti, semafor je događaj s brojačem. Sve dok je taj brojač veći od nule, semafor je u stanju signalizacije. Međutim, svaki poziv WaitFor smanjuje ovaj brojač za jedan dok ne postane nula i semafor prijeđe u neutralno stanje. Kao i mutex, semafor ima funkciju ReleaseSemaphor koja povećava brojač. Međutim, za razliku od muteksa, semafor nije vezan za nit, a ponovno pozivanje WaitFor/ReleaseSemaphor smanjit će/povećati brojač.

Kako se može koristiti semafor? Na primjer, može se koristiti za umjetno ograničavanje višenitnosti. Kao što sam već spomenuo, previše istovremeno aktivnih niti može značajno degradirati performanse cijelog sustava zbog čestih promjena konteksta. A ako bismo morali stvoriti previše radničkih niti, možemo ograničiti broj istovremeno aktivnih niti na broj koji odgovara broju procesora.

Što se još može reći o objektima za sinkronizaciju jezgre? Vrlo je zgodno dati im imena. Sve funkcije koje stvaraju objekte sinkronizacije imaju odgovarajući parametar: CreateEvent , CreateMutex , CreateSemaphore . Ako, na primjer, dvaput pozovete CreateEvent, oba puta navodeći isto neprazno ime, tada će drugi put funkcija, umjesto stvaranja novog objekta, vratiti ručicu postojećeg. To će se dogoditi čak i ako je drugi poziv upućen iz drugog procesa. Potonje je vrlo zgodno u slučajevima kada želite sinkronizirati niti koje pripadaju različitim procesima.

Kada više ne trebate objekt za sinkronizaciju, nemojte zaboraviti pozvati funkciju CloseHandle koju sam spomenuo ranije kada sam govorio o nitima. Zapravo, neće nužno odmah izbrisati objekt. Poanta je da objekt može imati nekoliko ručica, a onda će biti izbrisan tek kada se zadnja zatvori.

Želim te podsjetiti na to Najbolji način kako bi se osiguralo da će CloseHandle ili slična funkcija "čišćenja" sigurno biti pozvana, čak i u slučaju nenormalne situacije, znači staviti je u destruktor. Usput, ovo je jednom dobro i detaljno opisano u članku Kirill Pleshivtsev “Smart Destructor”. U gornjim primjerima ovu tehniku ​​nisam koristio isključivo u obrazovne svrhe, tako da je rad API funkcija bio vizualniji. U stvarnom kodu uvijek biste trebali koristiti klase omotače s pametnim destruktorima za čišćenje.

Usput, s funkcijom ReleaseMutex i sličnim, stalno se javlja isti problem kao i s CloseHandle . Mora se pozvati na kraju rada s dijeljenim podacima, bez obzira na to koliko je uspješno taj posao dovršen (uostalom, može se baciti iznimka). Posljedice "zaborava" ovdje su ozbiljnije. Ako se ne pozove CloseHandle samo će propuštati resurse (što je također loše!), tada će neobjavljeni mutex spriječiti druge niti da rade s dijeljenim resursom do prekida neuspjele niti, što najvjerojatnije neće dopustiti aplikaciji da normalno funkcionira. Da bismo to izbjegli, ponovno će nam pomoći posebno obučena klasa s pametnim destruktorom.

Završavajući pregled sinkronizacijskih objekata, želio bih spomenuti objekt koji nije u Win32 API-ju. Mnogi moji kolege se pitaju zašto Win32 nema specijalizirani objekt "jedan piše, mnogi čitaju". Neka vrsta "naprednog mutexa", koji osigurava da samo jedna nit može istovremeno pristupiti dijeljenim podacima za pisanje, a nekoliko niti može samo čitati odjednom. Sličan objekt može se pronaći u UNIX-u. Neke biblioteke, na primjer iz Borlanda, nude njegovu emulaciju na temelju standardnih objekata za sinkronizaciju. Međutim, stvarna korist od takvih emulacija je vrlo dvojbena. Takav objekt može se učinkovito implementirati samo na razini jezgre operativnog sustava. Ali u jezgri Windowsa ne postoji takav objekt.

Zašto se programeri Windows NT kernela nisu pobrinuli za ovo? Zašto smo gori od UNIX-a? Po mom mišljenju, odgovor je da jednostavno još nije postojala stvarna potreba za takvim objektom za Windows. Na normalnom jednoprocesorskom stroju, gdje niti još uvijek ne mogu fizički raditi istovremeno, to će biti praktički ekvivalentno mutexu. Na višeprocesorskom stroju može imati koristi dopuštajući da niti čitača rade paralelno. U isto vrijeme, ovaj će dobitak postati opipljiv tek kada je vjerojatnost "sudara" čitajućih niti visoka. Nedvojbeno će, na primjer, na stroju s 1024 procesora takav kernel objekt biti vitalan. Slični strojevi postoje, ali oni su specijalizirani sustavi koji pokreću specijalizirane operativne sustave. Često su takvi operativni sustavi izgrađeni na temelju UNIX-a, vjerojatno je odatle objekt poput "jedan piše, mnogi čitaju" ušao u češće korištene verzije ovog sustava. Ali na x86 strojevima na koje smo navikli u pravilu je instaliran samo jedan, a samo povremeno dva procesora. I samo najnapredniji modeli procesora kao što je Intel Xeon podržavaju 4 ili čak više konfiguracija procesora, ali takvi sustavi i dalje ostaju egzotični. Ali čak i na tako "naprednom" sustavu, "napredni mutex" može dati primjetan dobitak performansi samo u vrlo specifičnim situacijama.

Stoga implementacija "naprednog" mutexa jednostavno nije vrijedna truda. Na stroju s "niskim procesorom" može čak biti manje učinkovit zbog složenosti logike objekta u usporedbi sa standardnim muteksom. Imajte na umu da implementacija takvog objekta nije tako jednostavna kao što se može činiti na prvi pogled. S neuspješnom implementacijom, ako postoji previše niti za čitanje, nit za pisanje jednostavno "neće doći" do podataka. Iz tih razloga također ne preporučujem da pokušavate oponašati takav objekt. U stvarnim aplikacijama na stvarnim strojevima, obični mutex ili kritični odjeljak (o kojem će biti riječi u sljedećem dijelu članka) savršeno će se nositi sa zadatkom sinkronizacije pristupa dijeljenim podacima. Iako će se, pretpostavljam, s razvojem Windows OS-a prije ili kasnije pojaviti objekt jezgre "jedan piše, mnogo čita".

Bilješka. Zapravo, objekt "jedan piše - mnogi čitaju" u Windows NT još uvijek postoji. Jednostavno nisam znao za to kada sam pisao ovaj članak. Taj se objekt naziva "resursi jezgre" i nije dostupan programima korisničkog načina rada, što je vjerojatno razlog zašto nije dobro poznat. Sličnosti o tome mogu se pronaći u DDK. Hvala Konstantinu Manurinu što mi je ukazao na ovo.

Zastoj

Vratimo se sada funkciji WaitForMultipleObjects, točnije njenom trećem parametru, bWaitAll. Obećao sam vam reći zašto je sposobnost čekanja na nekoliko objekata odjednom tako važna.

Razumljivo je zašto je potrebna funkcija da čeka jedan od nekoliko objekata. U nedostatku posebne funkcije, to bi se moglo učiniti, osim sekvencijalnim provjeravanjem stanja objekata u praznoj petlji, što je, naravno, neprihvatljivo. Ali potreba za posebnom funkcijom koja vam omogućuje čekanje trenutka kada nekoliko objekata odjednom prijeđe u stanje signala nije tako očita. Doista, zamislite sljedeću tipičnu situaciju: u određenom trenutku naša nit treba pristup dvama skupovima zajedničkih podataka odjednom, od kojih je svaki odgovoran za svoj vlastiti mutex, nazovimo ih A i B. Čini se da nit može prvo pričekajte dok se mutex A ne pusti, uhvatite ga, zatim pričekajte da se mutex B oslobodi... Čini se da možemo učiniti s nekoliko poziva WaitForSingleObject-a. Doista, ovo će funkcionirati, ali samo dok sve druge dretve steknu mutekse istim redoslijedom: prvo A, zatim B. Što se događa ako određena nit pokuša učiniti suprotno: prvo stekne B, zatim A? Prije ili kasnije, doći će do situacije kada je jedna nit uhvatila mutex A, druga B, prva čeka B da se oslobodi, druga A. Jasno je da to nikada neće dočekati i program će stati.

Ova vrsta zastoja vrlo je čest bug. Kao i sve pogreške vezane uz sinkronizaciju, pojavljuje se samo s vremena na vrijeme i može programeru pokvariti mnogo živaca. Istodobno, gotovo svaka shema koja uključuje nekoliko sinkronizacijskih objekata prepuna je zastoja. Stoga ovom problemu treba posvetiti posebnu pozornost u fazi projektiranja takvog sklopa.

U navedenom jednostavnom primjeru, blokiranje je prilično lako izbjeći. Potrebno je zahtijevati da sve niti dobiju mutekse određenim redoslijedom: prvo A, zatim B. Međutim, u složenom programu, gdje postoji mnogo objekata međusobno povezanih na različite načine, to obično nije tako lako postići. Ne dva, već mnogo objekata i niti mogu biti uključeni u bravu. Stoga, najviše pouzdan način Kako bi se izbjegao zastoj u situaciji u kojoj nit treba nekoliko objekata za sinkronizaciju odjednom, treba ih uhvatiti sve jednim pozivom funkcije WaitForMultipleObjects s ​​parametrom bWaitAll=TRUE. Istini za volju, u ovom slučaju samo prebacujemo problem zastoja u jezgru operativnog sustava, ali glavna stvar je da to više neće biti naša briga. Međutim, u složenom programu s mnogo objekata, kada nije uvijek moguće odmah reći koji će od njih biti potreban za izvođenje određene operacije, često nije lako dovesti sve WaitFor pozive na jedno mjesto i također ih kombinirati.

Dakle, postoje dva načina za izbjegavanje zastoja. Morate osigurati da sinkronizacijske objekte uvijek hvataju niti točno istim redoslijedom ili da ih hvata jedan poziv WaitForMultipleObjects. Posljednja metoda je jednostavnija i poželjnija. Međutim, u praksi, s ispunjenjem oba zahtjeva, stalno se javljaju poteškoće, potrebno je kombinirati oba ova pristupa. Projektiranje složenih vremenskih krugova često je vrlo netrivijalan zadatak.

Primjer sinkronizacije

U većini tipičnih situacija, poput onih koje sam gore opisao, nije teško organizirati sinkronizaciju, dovoljan je događaj ili mutex. Ali povremeno postoje složeniji slučajevi u kojima rješenje problema nije tako očito. Želio bih to ilustrirati konkretnim primjerom iz svoje prakse. Kao što ćete vidjeti, rješenje se pokazalo iznenađujuće jednostavnim, ali prije nego što sam ga pronašao, morao sam isprobati nekoliko neuspješnih opcija.

Dakle zadatak. Gotovo svi moderni upravitelji preuzimanja, ili jednostavno "stolice za ljuljanje", imaju mogućnost ograničavanja prometa tako da "stolica za ljuljanje" koja radi u pozadini ne ometa korisnika u surfanju internetom. Razvijao sam sličan program i dobio sam zadatak implementirati upravo takvu “značajku”. Moja stolica za ljuljanje radila je prema klasičnoj shemi višenitnosti, kada svaki zadatak, u ovom slučaju preuzimanje određene datoteke, rješava zasebna nit. Ograničenje prometa trebalo je biti kumulativno za sve tokove. To jest, bilo je potrebno osigurati da tijekom određenog vremenskog intervala svi tokovi ne čitaju iz svojih utičnica više od određenog broja bajtova. Jednostavno dijeljenje ovog ograničenja na jednake dijelove između tokova očito će biti neučinkovito, jer preuzimanje datoteka može biti vrlo neravnomjerno, jedan će se preuzimati brzo, drugi sporo. Stoga nam treba zajednički brojač za sve niti, koliko je bajtova pročitano i koliko ih se još može pročitati. Tu sinkronizacija dobro dolazi. Dodatnu složenost zadatku davao je zahtjev da se bilo koja od radničkih niti može zaustaviti u bilo kojem trenutku.

Formulirajmo problem detaljnije. Odlučio sam priložiti sustav sinkronizacije u posebnu klasu. Ovo je njegovo sučelje:

razreda CKvota {

javnost: // metode

poništiti postaviti ( nepotpisani int _nKvota );

nepotpisani int Zahtjev ( nepotpisani int _nBytesToRead , HANDLE_hStopEvent );

poništiti Otpuštanje ( nepotpisani int _nBytesRevert , HANDLE_hStopEvent );

Povremeno, recimo jednom u sekundi, kontrolna nit poziva metodu Set, postavljajući kvotu preuzimanja. Prije nego što radna nit pročita podatke primljene s mreže, poziva metodu Request, koja provjerava da trenutna kvota nije nula, i ako je tako, vraća broj bajtova koji se mogu pročitati manji od trenutne kvote. Kvota se odgovarajuće smanjuje za taj broj. Ako je kvota nula kada je zahtjev pozvan, pozivajuća nit mora čekati dok kvota ne postane dostupna. Ponekad se dogodi da stvarno bude primljeno manje bajtova od traženog, u kojem slučaju nit vraća dio kvote koja joj je dodijeljena metodom Release. I, kao što sam rekao, korisnik može u bilo kojem trenutku dati naredbu za zaustavljanje preuzimanja. U tom slučaju čekanje se mora prekinuti, bez obzira na postojanje kvote. Za to se koristi poseban događaj: _hStopEvent. Budući da se zadaci mogu pokretati i zaustavljati neovisno, svaka radna nit ima svoj događaj zaustavljanja. Njegov se handle prosljeđuje metodama Request i Release.

U jednoj od neuspješnih opcija pokušao sam upotrijebiti kombinaciju mutexa koji sinkronizira pristup klasi CQuota i događaja koji signalizira postojanje kvote. Međutim, događaj zaustavljanja ne uklapa se u ovu shemu. Ako nit želi steći kvotu, tada njezino stanje čekanja mora biti kontrolirano složenim Booleovim izrazom: ((događaj mutex I kvota) ILI događaj zaustavljanja). Ali WaitForMultipleObjects to ne dopušta, možete kombinirati nekoliko kernel objekata bilo s AND ili OR operacijom, ali ne miješati. Pokušaj podijeliti čekanje s dva uzastopna poziva WaitForMultipleObjects neizbježno rezultira zastojem. Općenito, ovaj se put pokazao slijepom ulicom.

Neću više puštati maglu i reći vam rješenje. Kao što sam rekao, mutex je vrlo sličan događaju automatskog resetiranja. I ovdje imamo samo onaj rijedak slučaj kada je prikladnije koristiti ga, ali ne jedan, već dva odjednom:

razreda CKvota {

privatni: // podaci

nepotpisani int m_nKvota ;

CEvent m_eventHasQuota ;

CEvent m_eventNoQuota ;

Samo jedan od ovih događaja može se postaviti u isto vrijeme. Svaka nit koja manipulira kvotom mora postaviti prvi događaj ako je preostala kvota različita od nule, a drugi ako je kvota iscrpljena. Nit koja želi dobiti kvotu mora čekati prvi događaj. Nit za povećanje kvote samo treba čekati bilo koji od ovih događaja, jer ako su oba u stanju resetiranja, to znači da druga nit trenutno radi s kvotom. Dakle, dva događaja obavljaju dvije funkcije odjednom: sinkronizaciju pristupa podacima i čekanje. Konačno, budući da nit čeka jedan od dva događaja, događaj koji signalizira zaustavljanje se lako uključuje.

Dat ću primjer implementacije Request metode. Ostatak se provodi na sličan način. Malo sam pojednostavio kod korišten u stvarnom projektu:

nepotpisani int CKvota :: Zahtjev ( nepotpisani int _nZahtjev , HANDLE_hStopEvent )

ako(! _nZahtjev ) povratak 0 ;

nepotpisani int nOsigurati = 0 ;

RUKOVATI događajima [ 2 ];

hDogađaji [ 0 ] = _hStopEvent ; // Događaj zaustavljanja ima veći prioritet. Stavili smo ga na prvo mjesto.

hDogađaji [ 1 ] = m_eventHasQuota ;

int iWaitResult = :: WaitForMultipleObjects ( 2 , hDogađaji , NETOČNO , BESKONAČNO );

sklopka( iWaitResult ) {

slučaj WAIT_FAILED :

// POGREŠKA

baciti novi CWin32Iznimka ;

slučaj WAIT_OBJECT_0 :

// Zaustavi događaj. Riješio sam to s prilagođenom iznimkom, ali ništa me ne sprječava da to implementiram na neki drugi način.

baciti novi CStopException ;

slučaj WAIT_OBJECT_0 + 1 :

// Događaj "dostupna kvota"

TVRDITI ( m_nKvota ); // Ako je signal dao ovaj događaj, ali zapravo nema kvote, onda smo negdje pogriješili. Moram potražiti grešku!

ako( _nZahtjev >= m_nKvota ) {

nOsigurati = m_nKvota ;

m_nKvota = 0 ;

m_eventNoQuota . postaviti ();

drugo {

nOsigurati = _nZahtjev ;

m_nKvota -= _nZahtjev ;

m_eventHasQuota . postaviti ();

pauza;

povratak nOsigurati ;

Mala napomena. MFC biblioteka nije korištena u tom projektu, ali, kao što ste vjerojatno već pogodili, napravio sam svoju vlastitu CEvent klasu, omotač oko objekta kernela "event", sličan MFC "schnoy. Kao što sam rekao, takve jednostavne klase omotača su vrlo korisni kada postoji neki resurs (u ovom slučaju, objekt jezgre) koji se mora zapamtiti da se oslobodi na kraju rada. U ostalom, nije važno pišete li SetEvent(m_hEvent) ili m_event.Set( ).

Nadam se da će vam ovaj primjer pomoći da osmislite vlastitu vremensku shemu ako naiđete na netrivijalnu situaciju. Glavna stvar je analizirati svoju shemu što je pažljivije moguće. Može li doći do situacije u kojoj ne bi ispravno radila, konkretno, može li doći do blokade? Hvatanje takvih pogrešaka u debuggeru obično je beznadan posao, samo detaljna analiza ovdje pomaže.

Dakle, razmotrili smo osnovni alat sinkronizacija niti: objekti sinkronizacije jezgre. To je moćan i svestran alat. S njim možete izgraditi čak i vrlo složene sheme sinkronizacije. Srećom, takve netrivijalne situacije su rijetke. Osim toga, svestranost uvijek dolazi po cijenu performansi. Stoga se u mnogim slučajevima isplati koristiti druge značajke sinkronizacije niti dostupne u sustavu Windows, poput kritičnih odjeljaka i atomskih operacija. Nisu toliko univerzalni, ali su jednostavni i učinkoviti. O njima ćemo govoriti u sljedećem dijelu.

Proces je instanca programa učitanog u memoriju. Ova instanca može stvoriti niti, koje su niz instrukcija koje treba izvršiti. Važno je razumjeti da se ne izvode procesi, već niti.

Štoviše, svaki proces ima barem jednu nit. Ova se nit naziva glavna (main) nit aplikacije.

Budući da gotovo uvijek postoji puno više niti nego što ima fizičkih procesora za njihovo izvršavanje, niti se zapravo ne izvršavaju istovremeno, već redom (raspodjela procesorskog vremena događa se upravo između niti). Ali prebacivanje između njih događa se toliko često da se čini kao da rade paralelno.

Ovisno o situaciji, niti mogu biti u tri stanja. Prvo, nit se može pokrenuti kada joj se da CPU vrijeme, tj. možda je aktivan. Drugo, može biti neaktivan i čekati na dodjelu procesora, tj. biti u stanju pripravnosti. A postoji i treći, također vrlo važan uvjet- zaključano stanje. Kada je nit blokirana, uopće joj se ne dodjeljuje vrijeme. Obično se brava postavlja dok se čeka neki događaj. Kada se dogodi ovaj događaj, nit se automatski prebacuje iz blokiranog stanja u stanje pripravnosti. Na primjer, ako jedna nit izvodi izračune dok druga mora čekati da se rezultati spreme na disk. Drugi bi mogao koristiti petlju poput "while(!isCalcFinished) continue;", ali u praksi je lako vidjeti da je procesor 100% zauzet dok ova petlja radi (ovo se zove aktivno čekanje). Takve petlje treba izbjegavati kad god je to moguće, u čemu mehanizam za zaključavanje pruža neprocjenjivu pomoć. Druga nit se može blokirati sve dok prva nit ne postavi događaj koji signalizira da je čitanje završeno.

Sinkronizacija niti u Windows OS-u

Windows implementira preemptive multitasking, što znači da sustav u bilo kojem trenutku može prekinuti izvođenje jedne niti i prenijeti kontrolu na drugu. Prethodno se u Windowsima 3.1 koristila metoda organizacije nazvana cooperative multitasking: sustav je čekao dok sama nit ne prenese kontrolu na njega i zato se u slučaju zamrzavanja jedne aplikacije računalo moralo ponovno pokrenuti.

Sve niti koje pripadaju istom procesu dijele neke zajedničke resurse, poput RAM adresnog prostora ili otvorenih datoteka. Ti resursi pripadaju cijelom procesu, a time i svakoj njegovoj niti. Stoga svaka nit može raditi s tim resursima bez ikakvih ograničenja. Ali... Ako jedna nit još nije završila rad s bilo kojim zajedničkim resursom, a sustav se prebacio na drugu nit koristeći isti resurs, tada se rezultat rada ovih niti može znatno razlikovati od onoga što je namjeravano. Takvi sukobi također mogu nastati između niti koje pripadaju različitim procesima. Kad god dvije ili više niti koriste neku vrstu dijeljenog resursa, javlja se ovaj problem.

Primjer. Niti nisu sinkronizirane: Ako privremeno obustavite nit prikaza (pauza), nit za popunjavanje niza u pozadini nastavit će se izvoditi.

#uključi #uključi int a; RUČKA hThr; nepotpisani dugi uThrID; void Thread(void* pParams) ( int i, num = 0; while (1) ( for (i=0; i<5; i++) a[i] = num; num++; } } int main(void) { hThr=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)Thread,NULL,0,&uThrID); while(1) printf("%d %d %d %d %d\n", a, a, a, a, a); return 0; }

Zato je potreban mehanizam koji će omogućiti nitima da koordiniraju svoj rad sa zajedničkim resursima. Ovaj mehanizam se naziva mehanizam sinkronizacije niti.

Ovaj mehanizam je skup objekata operacijskog sustava koji su kreirani i kojima upravlja softver, zajednički su svim nitima u sustavu (neke dijele niti koje pripadaju istom procesu) i koriste se za koordinaciju pristupa resursima. Resursi mogu biti bilo što što mogu dijeliti dvije ili više niti - datoteka na disku, port, unos baze podataka, GDI objekt, pa čak i globalna programska varijabla (kojoj se može pristupiti iz niti koje pripadaju istom procesu).

Postoji nekoliko objekata sinkronizacije, od kojih su najvažniji mutex, kritični odjeljak, događaj i semafor. Svaki od ovih objekata implementira vlastitu metodu sinkronizacije. Također, sami procesi i niti se mogu koristiti kao sinkronizacijski objekti (kada jedna nit čeka završetak druge niti ili procesa); kao i datoteke, komunikacijske uređaje, unos konzole i obavijesti o promjenama.

Svaki objekt sinkronizacije može biti u takozvanom signaliziranom stanju. Za svaku vrstu objekta ovo stanje ima drugačije značenje. Niti mogu provjeriti trenutno stanje objekta i/ili čekati da se to stanje promijeni i tako koordinirati svoje radnje. Ovo osigurava da kada nit radi sa sinkronizacijskim objektima (stvara ih, mijenja stanje), sustav neće prekinuti njezino izvršenje dok ne dovrši ovu radnju. Stoga su sve konačne operacije na objektima sinkronizacije atomske (nedjeljive.

Rad s objektima sinkronizacije

Za stvaranje jednog ili drugog objekta sinkronizacije poziva se posebna WinAPI funkcija tipa Create... (npr. CreateMutex). Ovaj poziv vraća ručku objekta (HANDLE) koju mogu koristiti sve niti koje pripadaju danom procesu. Moguće je pristupiti sinkronizacijskom objektu iz drugog procesa, bilo nasljeđivanjem ručice objekta ili, po mogućnosti, pozivanjem funkcije Open... objekta. Nakon ovog poziva, proces će dobiti handle, koji se kasnije može koristiti za rad s objektom. Objektu, osim ako se ne namjerava koristiti unutar jednog procesa, mora se dati naziv. Imena svih objekata moraju biti različita (čak i ako su različitih vrsta). Ne možete, na primjer, kreirati događaj i semafor s istim imenom.

Po dostupnom deskriptoru objekta možete odrediti njegovo trenutno stanje. To se radi uz pomoć tzv. funkcije na čekanju. Najčešće korištena funkcija je WaitForSingleObject. Ova funkcija uzima dva parametra, prvi je rukovatelj objektom, drugi je vrijeme čekanja u ms. Funkcija vraća WAIT_OBJECT_0 ako je objekt u signaliziranom stanju, WAIT_TIMEOUT ako je isteklo vrijeme čekanja i WAIT_ABANDONED ako mutex nije oslobođen prije nego što je vlasnička nit prekinuta. Ako je vremensko ograničenje navedeno kao nula, funkcija se odmah vraća, u suprotnom čeka određeno vrijeme. Ako stanje objekta postane signalizirano prije isteka ovog vremena, funkcija će vratiti WAIT_OBJECT_0, inače će funkcija vratiti WAIT_TIMEOUT. Ako je simbolička konstanta INFINITE određena kao vrijeme, tada će funkcija čekati neograničeno dok stanje objekta ne postane signalizirano.

Vrlo je važno da poziv funkcije čekanja blokira trenutnu nit, tj. dok je nit u stanju mirovanja, procesorsko vrijeme joj se ne dodjeljuje.

Kritični dijelovi

Odjeljak kritičan za objekte pomaže programeru izolirati odjeljak koda gdje nit pristupa dijeljenom resursu i sprječava istodobnu upotrebu resursa. Prije korištenja resursa, nit ulazi u kritični odjeljak (poziva funkciju EnterCriticalSection). Ako bilo koja druga nit tada pokuša ući u isti kritični odjeljak, njezino će se izvršenje zaustaviti dok prva nit ne napusti odjeljak pozivom LeaveCriticalSection. Koristi se samo za niti u jednom procesu. Redoslijed ulaska u kritični dio nije definiran.

Tu je i funkcija TryEnterCriticalSection koja provjerava je li kritični odjeljak trenutno zauzet. Uz njegovu pomoć, nit u procesu čekanja na pristup resursu ne može se blokirati, ali izvršiti neke korisne radnje.

Primjer. Sinkronizacija niti pomoću kritičnih odjeljaka.

#uključi #uključi KRITIČNI_ODJELJAK cs; int a; RUČKA hThr; nepotpisani dugi uThrID; void Thread(void* pParams) ( int i, num = 0; while (1) ( EnterCriticalSection(&cs); for (i=0; i<5; i++) a[i] = num; num++; LeaveCriticalSection(&cs); } } int main(void) { InitializeCriticalSection(&cs); hThr=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)Thread,NULL,0,&uThrID); while(1) { EnterCriticalSection(&cs); printf("%d %d %d %d %d\n", a, a, a, a, a); LeaveCriticalSection(&cs); } return 0; }

Međusobno isključivanje

Objekti međusobnog isključivanja (muteksi, mutex - od MUTual EXclusion) omogućuju vam da koordinirate međusobno isključivanje pristupa zajedničkom resursu. Signalizirano stanje objekta (to jest, "postavljeno" stanje) odgovara točki u vremenu kada objekt ne pripada nijednoj niti i može biti "uhvaćen". Nasuprot tome, stanje "reset" (nije signalizirano) odgovara trenutku kada neka nit već posjeduje ovaj objekt. Pristup objektu je odobren kada ga nit koja posjeduje objekt oslobodi.

Dvije (ili više) niti mogu stvoriti mutex s istim imenom pozivanjem funkcije CreateMutex. Prva nit zapravo stvara mutex, a sljedeće niti dobivaju rukovanje već postojećim objektom. Ovo omogućuje višestrukim nitima da steknu držač za isti mutex, oslobađajući programera brige o tome tko zapravo stvara mutex. Ako se koristi ovaj pristup, poželjno je postaviti oznaku bInitialOwner na FALSE, inače će biti poteškoća u određivanju stvarnog kreatora muteksa.

Višestruke niti mogu dobiti rukovanje istim muteksom, čineći mogućom komunikaciju između procesa. Za ovaj pristup možete koristiti sljedeće mehanizme:

  • Podređeni proces kreiran pomoću funkcije CreateProcess može naslijediti rukovanje mutexom ako je parametar lpMutexAttributes naveden kada je mutex stvoren funkcijom CreateMutex.
  • Nit može dobiti duplikat postojećeg muteksa pomoću funkcije DuplicateHandle.
  • Nit može navesti naziv postojećeg mutexa kada poziva funkcije OpenMutex ili CreateMutex.

Da bi deklarirali mutex u vlasništvu trenutne niti, mora se pozvati jedna od funkcija na čekanju. Nit koja posjeduje objekt može ga "hvatati" više puta koliko god puta želi (ovo neće dovesti do samozaključavanja), ali će ga morati osloboditi koliko god puta pomoću funkcije ReleaseMutex.

Za sinkronizaciju niti jednog procesa, učinkovitije je koristiti kritične dijelove.

Primjer. Sinkronizacija niti pomoću muteksa.

#uključi #uključi HANDLE hMutex; int a; RUČKA hThr; nepotpisani dugi uThrID; void Thread(void* pParams) ( int i, num = 0; while (1) ( WaitForSingleObject(hMutex, INFINITE); for (i=0; i<5; i++) a[i] = num; num++; ReleaseMutex(hMutex); } } int main(void) { hMutex=CreateMutex(NULL, FALSE, NULL); hThr=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)Thread,NULL,0,&uThrID); while(1) { WaitForSingleObject(hMutex, INFINITE); printf("%d %d %d %d %d\n", a, a, a, a, a); ReleaseMutex(hMutex); } return 0; }

Razvoj događaja

Objekti događaja koriste se za obavještavanje niti koje čekaju da se događaj dogodio. Postoje dvije vrste događaja - s ručnim i automatskim resetiranjem. Ručno resetiranje izvodi funkcija ResetEvent. Događaji ručnog resetiranja koriste se za obavještavanje više niti odjednom. Kada koristite događaj automatskog resetiranja, samo će jedna nit na čekanju primiti obavijest i nastaviti s izvođenjem, a ostale će čekati dalje.

Funkcija CreateEvent stvara objekt događaja, SetEvent - postavlja događaj u stanje signala, ResetEvent - resetira događaj. Funkcija PulseEvent postavlja događaj i nakon nastavka rada niti koje čekaju na ovaj događaj (sve s ručnim resetiranjem i samo jedna s automatskim), resetira ga. Ako nema niti koje čekaju, PulseEvent jednostavno resetira događaj.

Primjer. Sinkronizacija niti pomoću događaja.

#uključi #uključi HANDLE hEvent1, hEvent2; int a; RUČKA hThr; nepotpisani dugi uThrID; void Thread(void* pParams) ( int i, num = 0; while (1) ( WaitForSingleObject(hEvent2, INFINITE); for (i=0; i<5; i++) a[i] = num; num++; SetEvent(hEvent1); } } int main(void) { hEvent1=CreateEvent(NULL, FALSE, TRUE, NULL); hEvent2=CreateEvent(NULL, FALSE, FALSE, NULL); hThr=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)Thread,NULL,0,&uThrID); while(1) { WaitForSingleObject(hEvent1, INFINITE); printf("%d %d %d %d %d\n", a, a, a, a, a); SetEvent(hEvent2); } return 0; }

semafori

Objekt semafora zapravo je mutex objekt s brojačem. Ovaj objekt dopušta da ga "zarobi" određeni broj niti. Nakon toga "hvatanje" će biti nemoguće sve dok ga jedna od prethodno "zarobljenih" niti semafora ne oslobodi. Semafori se koriste za ograničavanje broja niti koje mogu pristupiti resursu u isto vrijeme. Tijekom inicijalizacije, najveći broj niti se prenosi na objekt, nakon svakog "hvatanja" brojač semafora se smanjuje. Stanje signala odgovara vrijednosti brojača većoj od nule. Kada je brojač nula, semafor se smatra nepostavljenim (resetiranim).

Funkcija CreateSemaphore stvara objekt semafora s naznakom njegove najveće moguće početne vrijednosti, OpenSemaphore - vraća rukovatelj postojećem semaforu, semafor se hvata pomoću funkcija čekanja, dok se vrijednost semafora smanjuje za jedan, ReleaseSemaphore - oslobađa semafor s povećanje vrijednosti semafora za vrijednost navedenu u broju parametra.

Primjer. Sinkronizacija niti pomoću semafora.

#uključi #uključi RUČKA hSem; int a; RUČKA hThr; nepotpisani dugi uThrID; void Thread(void* pParams) ( int i, num = 0; while (1) ( WaitForSingleObject(hSem, INFINITE); for (i=0; i<5; i++) a[i] = num; num++; ReleaseSemaphore(hSem, 1, NULL); } } int main(void) { hSem=CreateSemaphore(NULL, 1, 1, "MySemaphore1"); hThr=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)Thread,NULL,0,&uThrID); while(1) { WaitForSingleObject(hSem, INFINITE); printf("%d %d %d %d %d\n", a, a, a, a, a); ReleaseSemaphore(hSem, 1, NULL); } return 0; }

Zaštićeni pristup varijablama

Postoji niz funkcija koje vam omogućuju rad s globalnim varijablama iz svih niti bez brige o sinkronizaciji, jer. te se funkcije same brinu za to - njihovo je izvršavanje atomsko. To su funkcije InterlockedIncrement, InterlockedDecrement, InterlockedExchange, InterlockedExchangeAdd i InterlockedCompareExchange. Na primjer, funkcija InterlockedIncrement atomski povećava vrijednost 32-bitne varijable za jedan, što je korisno za razne brojače.

Za dobivanje potpunih informacija o namjeni, upotrebi i sintaksi svih WIN32 API funkcija potrebno je koristiti MS SDK sustav pomoći, koji je dio Borland Delphi ili CBuilder programskih okruženja, kao i MSDN, koji se isporučuje u sklopu programski sustav Visual C.


Za programe koji koriste više niti ili procesa, potrebno je da svi izvršavaju funkcije koje su im dodijeljene u željenom redoslijedu. U okruženju Windows 9x, u tu svrhu, predlaže se korištenje nekoliko mehanizama koji osiguravaju nesmetan rad niti. Ti se mehanizmi nazivaju mehanizmi sinkronizacije. Pretpostavimo da razvijate program u kojem dvije niti rade paralelno. Svaka nit pristupa jednoj zajedničkoj globalnoj varijabli. Jedna nit, svaki put kada se pristupi ovoj varijabli, povećava je, a druga nit je smanjuje. Pri istovremenom asinkronom radu dretvi neizbježno se javlja sljedeća situacija: - prva nit je pročitala vrijednost globalne varijable u lokalnu; - OS ga prekida, jer je kvantum procesorskog vremena koji mu je dodijeljen završio, i prenosi kontrolu na drugu nit; - druga nit je također pročitala vrijednost globalne varijable u lokalnu, dekrementirala je i napisala novu vrijednost natrag; - OS ponovno prenosi kontrolu na prvu nit, koja, ne znajući ništa o radnjama druge niti, povećava svoju lokalnu varijablu i zapisuje njezinu vrijednost u globalnu. Očito će se izgubiti promjene koje je napravila druga nit. Kako bi se izbjegle takve situacije, potrebno je na vrijeme odvojiti korištenje zajedničkih podataka. U takvim slučajevima koriste se mehanizmi sinkronizacije koji osiguravaju ispravan rad više niti. Alati za sinkronizaciju u OS-uWindows: 1) kritični odjeljak (KritičnoOdjeljak) je objekt koji pripada procesu, a ne kernelu. To znači da ne može sinkronizirati niti iz različitih procesa. Tu su i funkcije za inicijalizaciju (kreiranje) i brisanje, ulaz i izlaz iz kritičnog odjeljka: kreiranje - InitializeCriticalSection(...), brisanje - DeleteCriticalSection(...), unos - EnterCriticalSection(...), izlaz - LeaveCriticalSection (...). Ograničenja: budući da nije objekt jezgre, nije vidljiv drugim procesima, tj. možete zaštititi samo niti vlastitog procesa. Kritični odjeljak raščlanjuje vrijednost posebne procesne varijable koja se koristi kao oznaka za sprječavanje višestrukih niti da izvrše dio koda u isto vrijeme. Među sinkronizirajućim objektima, kritične sekcije su najjednostavnije. 2) mutexpromjenjivisključiti. Ovo je kernel objekt, ima ime, što znači da se mogu koristiti za sinkronizaciju pristupa zajedničkim podacima iz nekoliko procesa, točnije, iz niti različitih procesa. Nijedna druga nit ne može dobiti mutex koji je već u vlasništvu jedne od niti. Ako mutex štiti neke zajedničke podatke, moći će obavljati svoju funkciju samo ako svaka nit provjeri stanje ovog mutexa prije pristupa tim podacima. Windows tretira mutex kao zajednički objekt koji se može signalizirati ili resetirati. Signalizirano stanje muteksa označava da je zauzet. Niti moraju neovisno analizirati trenutno stanje muteksa. Ako želite da mutex-u pristupaju niti iz drugih procesa, morate mu dati ime. Funkcije: CreateMutex(name) - stvaranje, hnd=OpenMutex(name) - otvaranje, WaitForSingleObject(hnd) - čekanje i zauzimanje, ReleaseMutex(hnd) - otpuštanje, CloseHandle(hnd) - zatvaranje. Može se koristiti za zaštitu od ponovnog pokretanja programa. 3) semafor -semafor. Objekt jezgre "semafor" koristi se za obračun resursa i služi za ograničavanje istovremenog pristupa resursu za nekoliko niti. Pomoću semafora možete organizirati rad programa na takav način da nekoliko niti može pristupiti resursu u isto vrijeme, ali broj tih niti će biti ograničen. Prilikom izrade semafora naveden je najveći broj niti koje mogu istovremeno raditi s resursom. Svaki put kada program pristupi semaforu, brojač resursa semafora se smanjuje za jedan. Kada vrijednost brojača resursa postane nula, semafor je nedostupan. kreirajte CreateSemaphore, otvorite OpenSemaphore, uzmite WaitForSingleObject, otpustite ReleaseSemaphore 4 ) događaj -događaj. Događaji obično samo obavještavaju o završetku neke operacije, oni su također objekti jezgre. Ne samo da možete eksplicitno otpustiti, već postoji i operacija postavljanja događaja. Događaji mogu biti ručni (manual) i pojedinačni (single). Pojedinačni događaj više je opća zastava. Događaj je u signaliziranom stanju ako ga je postavila neka nit. Ako program zahtijeva da samo jedna od niti reagira na njega u slučaju događaja, dok sve druge niti nastavljaju čekati, tada se koristi jedan događaj. Ručni događaj nije samo uobičajena zastavica u više niti. Obavlja nešto složenije funkcije. Bilo koja nit može postaviti ovaj događaj ili ga poništiti (obrisati). Jednom kada je događaj postavljen, ostat će u ovom stanju proizvoljno dugo vremena, bez obzira na to koliko niti čeka na postavljanje događaja. Kada sve niti koje čekaju ovaj događaj prime poruku da se događaj dogodio, automatski će se resetirati. Funkcije: SetEvent, ClearEvent, WaitForEvent. Vrste događaja: 1) događaj automatskog resetiranja: WaitForSingleEvent. 2) događaj s ručnim resetiranjem (ručno), tada se događaj mora resetirati: ReleaseEvent. Neki teoretičari izdvajaju još jedan objekt za sinkronizaciju: WaitAbleTimer je objekt jezgre OS-a koji se samostalno prebacuje u slobodno stanje nakon određenog vremenskog intervala (budilica).

Ponekad kada radite s više niti ili procesa, to postaje neophodno sinkronizirati izvođenje dvoje ili više njih. Razlog za to je najčešće taj što dvije ili više niti mogu zahtijevati pristup zajedničkom resursu koji stvarno ne može se pružiti za više niti odjednom. Zajednički resurs je resurs kojemu može pristupiti više zadataka koji se izvode u isto vrijeme.

Poziva se mehanizam koji osigurava proces sinkronizacije ograničenje pristupa. Potreba za njim također se javlja u slučajevima kada jedna nit čeka na događaj koji je generirala druga nit. Naravno, mora postojati neki način na koji će prva nit biti suspendirana dok se događaj ne dogodi. Nakon toga nit bi trebala nastaviti sa svojim izvođenjem.

Postoje dva opća stanja u kojima zadatak može biti. Prvo, zadatak može iznesen(ili biti spreman za izvršenje čim ima pristup resursima procesora). Drugo, zadatak može biti blokiran. U tom slučaju njegovo se izvršavanje obustavlja dok se ne oslobodi resurs koji mu je potreban ili dok se ne dogodi određeni događaj.

Windows ima posebne usluge koje vam omogućuju da ograničite pristup zajedničkim resursima na određeni način, jer bez pomoći operativnog sustava, zasebni proces ili nit ne može sam odrediti ima li jedini pristup resursu. Operativni sustav Windows sadrži proceduru koja u jednoj kontinuiranoj operaciji provjerava i, ako je moguće, postavlja oznaku pristupa resursu. Jezikom programera operativnih sustava takva se operacija naziva provjerite i instalirajte rad. Pozivaju se oznake koje se koriste za osiguranje sinkronizacije i kontrolu pristupa resursima semafori(semafor). Win32 API pruža podršku za semafore i druge sinkronizacijske objekte. MFC biblioteka također uključuje podršku za ove objekte.

Objekti sinkronizacije i mfc klase

Win32 sučelje podržava četiri tipa sinkronizacijskih objekata, a svi se na ovaj ili onaj način temelje na konceptu semafora.

Prva vrsta objekta je sam semafor, odn klasični (standardni) semafor. Omogućuje ograničenom broju procesa i niti pristup jednom resursu. U tom slučaju, pristup resursu je ili potpuno ograničen (jedna i samo jedna nit ili proces može pristupiti resursu u određenom vremenskom razdoblju), ili samo mali broj niti i procesa dobiva istovremeni pristup. Semafori su implementirani s brojačem koji se smanjuje kada se zadatku dodijeli semafor i povećava kada zadatak oslobodi semafor.

Druga vrsta sinkronizacijskih objekata je isključivi (mutex) semafor. Dizajniran je za potpuno ograničavanje pristupa resursu tako da samo jedan proces ili nit mogu pristupiti resursu u bilo kojem trenutku. Zapravo, ovo je posebna vrsta semafora.

Treći tip sinkronizacijskih objekata je događaj, ili objekt događaja. Koristi se za blokiranje pristupa resursu dok neki drugi proces ili nit ne izjavi da se resurs može koristiti. Dakle, ovaj objekt signalizira izvršenje traženog događaja.

Koristeći objekt sinkronizacije četvrtog tipa, moguće je zabraniti izvršavanje određenih dijelova programskog koda nekoliko niti istovremeno. Da biste to učinili, ove parcele moraju biti deklarirane kao kritični odjeljak. Kada jedna nit uđe u ovaj odjeljak, drugim je nitima zabranjeno učiniti isto dok prva nit ne izađe iz ovog odjeljka.

Kritični odjeljci, za razliku od drugih vrsta sinkronizacijskih objekata, koriste se samo za sinkronizaciju niti unutar jednog procesa. Druge vrste objekata mogu se koristiti za sinkronizaciju niti unutar procesa ili za sinkronizaciju procesa.

U MFC-u, mehanizam sinkronizacije koji pruža Win32 sučelje podržan je kroz sljedeće klase izvedene iz klase CSyncObject:

    CCriticalSection- implementira kritičnu sekciju.

    CEvent- implementira objekt događaja

    CMutex- implementira ekskluzivni semafor.

    CSemaphore- implementira klasični semafor.

Uz ove klase, MFC također definira dvije pomoćne sinkronizacijske klase: CSingleLock i CMultiLock. Oni kontroliraju pristup sinkronizacijskom objektu i sadrže metode koje se koriste za odobravanje i oslobađanje takvih objekata. Klasa CSingleLock kontrolira pristup jednom objektu sinkronizacije i klasi CMultiLock- na nekoliko objekata. U nastavku ćemo razmatrati samo klasu CSingleLock.

Kada se kreira bilo koji objekt za sinkronizaciju, pristup njemu može se kontrolirati pomoću klase CSingleLock. Da biste to učinili, prvo morate stvoriti objekt tipa CSingleLock pomoću konstruktora:

CSingleLock(CSyncObject* pObject, BOOL bInitialLock = FALSE);

Prvi parametar je pokazivač na objekt sinkronizacije, kao što je semafor. Vrijednost drugog parametra određuje hoće li konstruktor pokušati pristupiti danom objektu. Ako je ovaj parametar različit od nule, pristup će biti dopušten, inače se pristup neće pokušati. Ako je pristup odobren, tada je nit koja je stvorila objekt klase CSingleLock, bit će zaustavljen dok se metodom ne oslobodi odgovarajući objekt sinkronizacije Otključati razreda CSingleLock.

Nakon što se stvori objekt tipa CSingleLock, pristup objektu na koji ukazuje parametar pObject može se kontrolirati pomoću dvije funkcije: zaključati i Otključati razreda CSingleLock.

metoda zaključati dizajniran je za pristup objektu sinkronizacijskom objektu. Nit koja ju je pozvala suspendirana je dok se metoda ne završi, odnosno dok se ne pristupi resursu. Vrijednost parametra određuje koliko dugo će funkcija čekati da dobije pristup traženom objektu. Svaki put kada se metoda uspješno završi, vrijednost brojača povezana s objektom sinkronizacije smanjuje se za jedan.

metoda Otključati oslobađa sinkronizacijski objekt, dopuštajući drugim nitima korištenje resursa. U prvoj varijanti metode, vrijednost brojača pridruženog danom objektu povećava se za jedan. U drugoj opciji, prvi parametar određuje koliko treba povećati ovu vrijednost. Drugi parametar pokazuje na varijablu u koju će se upisati prethodna vrijednost brojača.

Prilikom rada s razredom CSingleLock Opći postupak za kontrolu pristupa resursu je sljedeći:

    stvoriti objekt tipa CSyncObj (na primjer, semafor) koji će se koristiti za kontrolu pristupa resursu;

    korištenjem stvorenog objekta sinkronizacije kreirajte objekt tipa CSingleLock;

    pozovite metodu Lock da dobijete pristup resursu;

    uputiti poziv resursu;

    pozovite metodu Otključaj za oslobađanje resursa.

Sljedeće opisuje kako stvoriti i koristiti semafore i objekte događaja. Nakon što razumijete ove koncepte, možete lako naučiti i koristiti druge dvije vrste sinkronizacijskih objekata: kritične sekcije i mutekse.