Gjendjet e fillit. Seksionet kritike Përfundimi i sinkronizimit në Windows OS

Ky objekt sinkronizimi mund të përdoret vetëm në nivel lokal brenda procesit që e ka krijuar. Pjesa tjetër e objekteve mund të përdoret për të sinkronizuar fijet e proceseve të ndryshme. Emri i objektit "seksioni kritik" shoqërohet me një përzgjedhje abstrakte të një pjese të kodit të programit (seksionit) që kryen disa operacione, rendi i të cilave nuk mund të shkelet. Kjo do të thotë, një përpjekje nga dy thread të ndryshëm për të ekzekutuar njëkohësisht kodin e këtij seksioni do të rezultojë në një gabim.

Për shembull, mund të jetë e përshtatshme për të mbrojtur funksionet e shkrimtarit me një seksion të tillë, pasi qasja e njëkohshme nga disa shkrimtarë duhet të përjashtohet.

Dy operacione janë prezantuar për seksionin kritik:

hyni në seksion Ndërsa çdo thread është në seksionin kritik, të gjitha temat e tjera automatikisht do të ndalojnë së prituri kur të përpiqen të hyjnë në të. Një fill që ka hyrë tashmë në këtë seksion mund të hyjë në të disa herë pa pritur që të lirohet.

largohu nga seksioni Kur një thread largohet nga një seksion, numëruesi i numrit të hyrjeve të këtij thread në seksion zvogëlohet, kështu që seksioni do të lirohet për fijet e tjera vetëm nëse filli del nga seksioni aq herë sa ka hyrë në të. Kur lëshohet një seksion kritik, vetëm një thread do të zgjohet, duke pritur për lejen për të hyrë në këtë seksion.

Në përgjithësi, në API-të e tjera jo-Win32 (si OS/2), seksioni kritik nuk trajtohet si një objekt sinkronizimi, por si një pjesë e kodit të programit që mund të ekzekutohet vetëm nga një thread aplikacioni. Domethënë, hyrja në seksionin kritik konsiderohet si mbyllje e përkohshme e mekanizmit të ndërrimit të fillit deri në daljen nga ky seksion. API Win32 i trajton seksionet kritike si objekte, gjë që çon në një farë konfuzioni -- ato janë shumë afër në vetitë e tyre me objektet ekskluzive të paemërtuar ( mutex, Shikoni më poshtë).

Kur përdorni seksione kritike, duhet pasur kujdes që të mos shpërndahen fragmente shumë të mëdha kodi në seksion, pasi kjo mund të çojë në vonesa të konsiderueshme në ekzekutimin e thread-eve të tjera.

Për shembull, në lidhje me grumbullimet e konsideruara tashmë, nuk ka kuptim të mbrohen të gjitha funksionet e grumbullit me një seksion kritik, pasi funksionet e lexuesit mund të ekzekutohen paralelisht. Për më tepër, përdorimi i një seksioni kritik edhe për sinkronizimin e shkrimtarëve në fakt duket të jetë i papërshtatshëm - pasi për të sinkronizuar një shkrimtar me lexuesit, ky i fundit do të duhet të hyjë në këtë seksion, i cili praktikisht çon në mbrojtjen e të gjitha funksioneve nga një i vetëm seksioni.

Ka disa raste të përdorimit efektiv të seksioneve kritike:

lexuesit nuk bien ndesh me shkrimtarët (vetëm shkrimtarët duhet të mbrohen);

të gjitha temat kanë të drejta përafërsisht të barabarta aksesi (të themi, nuk mund të veçosh shkrimtarët dhe lexuesit e pastër);

kur ndërtoni objekte sinkronizuese të përbërë, të përbërë nga disa standarde, për të mbrojtur operacionet vijuese në një objekt të përbërë.

Në pjesët e mëparshme të artikullit, fola për parimet e përgjithshme dhe metoda specifike për ndërtimin e aplikacioneve me shumë fije. Fijet e ndryshme pothuajse gjithmonë duhet të ndërveprojnë me njëri-tjetrin, dhe nevoja për sinkronizim lind në mënyrë të pashmangshme. Sot do t'i hedhim një vështrim mjetit më të rëndësishëm, më të fuqishëm dhe të gjithanshëm të sinkronizimit të Windows: Objektet e sinkronizimit të kernelit.

WaitForMultipleObjects dhe funksione të tjera të pritjes

Siç e mbani mend, për të sinkronizuar temat, zakonisht duhet të pezulloni përkohësisht ekzekutimin e njërit prej fijeve. Megjithatë, duhet të përkthehet me mjete sistemi operativ në një gjendje pritjeje ku nuk merr kohë CPU. Ne tashmë dimë dy funksione që mund ta bëjnë këtë: SuspendThread dhe ResumeThread. Por siç thashë në pjesën e mëparshme të artikullit, për shkak të disa veçorive, këto funksione nuk janë të përshtatshme për sinkronizim.

Sot do të shikojmë një funksion tjetër që gjithashtu e vendos thread-in në gjendje pritjeje, por ndryshe nga SuspendThread/ResumeThread, ai është krijuar posaçërisht për organizimin e sinkronizimit. Është WaitForMultipleObjects. Për shkak se kjo veçori është kaq e rëndësishme, unë do të devijoj pak nga rregulli im për të mos hyrë në detajet e API-së dhe do të flas për të në më shumë detaje, madje do të jap prototipin e tij:

DWORD WaitFor MultipleObjects (

DWORD nNumëroni , // numri i objekteve në grupin lpHandles

KONST DORËZUES * lpDorezat , // tregues për një grup të përshkruesve të objekteve të kernelit

BOOL bPrisni të gjithë , // flamuri që tregon nëse duhet pritur për të gjitha objektet apo vetëm një është i mjaftueshëm

DWORD dwMilisekonda // kohëzgjatje

Parametri kryesor i këtij funksioni është një tregues në një grup dorezash objektesh kernel. Se cilat janë këto objekte do të flasim më poshtë. Tani për tani, është e rëndësishme për ne të dimë se cilido nga këto objekte mund të jetë në një nga dy gjendjet: neutrale ose "sinjaluese" (gjendja e sinjalizuar). Nëse flamuri bWaitAll është FALSE, funksioni do të kthehet sapo të paktën një nga objektet të japë një sinjal. Dhe nëse flamuri është TRUE, kjo do të ndodhë vetëm kur të gjitha objektet të fillojnë të sinjalizojnë menjëherë (siç do të shohim, kjo është vetia më e rëndësishme e këtij funksioni). Në rastin e parë, me vlerën e kthyer, mund të zbuloni se cili prej objekteve dha sinjalin. Duhet të zbrisni konstantën WAIT_OBJECT_0 prej saj dhe merrni një indeks në grupin lpHandles. Nëse koha e skadimit tejkalon kohën e caktuar në parametrin e fundit, funksioni do të ndalojë së prituri dhe do të kthejë vlerën WAIT_TIMEOUT . Si një afat kohor, mund të specifikoni konstanten INFINITE , dhe më pas funksioni do të presë "derisa të ndalojë", ose mundeni anasjelltas 0, dhe pastaj filli nuk do të pezullohet fare. Në rastin e fundit, funksioni do të kthehet menjëherë, por rezultati i tij do t'ju tregojë gjendjen e objekteve. Teknika e fundit përdoret shumë shpesh. Siç mund ta shihni, ky funksion ka aftësi të pasura. Ka disa funksione të tjera WaitForXXX, por të gjitha janë variacione në temën kryesore. Në veçanti, WaitForSingleObject është vetëm një version i thjeshtuar i tij. Pjesa tjetër ka secila funksionalitetin e vet shtesë, por përdoret, në përgjithësi, më rrallë. Për shembull, ato bëjnë të mundur përgjigjen jo vetëm ndaj sinjaleve nga objektet e kernelit, por edhe për ardhjen e mesazheve të dritareve të reja në radhën e thread-it. Përshkrimin e tyre, si dhe informacione të hollësishme rreth WaitForMultipleObjects, do ta gjeni, si zakonisht, në MSDN.

Tani për atë që janë këto "objekte të bërthamës" misterioze. Për të filluar, këto përfshijnë vetë fijet dhe proceset. Ata hyjnë në gjendjen e sinjalizimit menjëherë pas përfundimit. Ky është një veçori shumë e rëndësishme sepse shpesh është e nevojshme të mbash gjurmët kur një fill ose proces ka përfunduar. Le të, për shembull, aplikacioni ynë i serverit me një grup thread-sh punëtor duhet të plotësohet. Në të njëjtën kohë, filli i kontrollit duhet të informojë fillesat e punëtorit në një farë mënyre se është koha për të përfunduar punën (për shembull, duke vendosur një flamur global), dhe më pas të presë derisa të përfundojnë të gjitha fijet, duke bërë gjithçka që është e nevojshme për përfundimin e duhur. i veprimit: lirimi i burimeve, informimi i klientëve për mbylljen, mbyllja e lidhjeve të rrjetit, etj.

Fakti që temat ndezin një sinjal në fund të punës e bën jashtëzakonisht të lehtë zgjidhjen e problemit të sinkronizimit me përfundimin e fillit:

// Për thjeshtësi, le të kemi vetëm një fije pune. Le ta ekzekutojmë:

TRAJTO hWorkerThread = :: KrijoThread (...);

// Para përfundimit të punës, duhet t'i tregojmë disi fillit të punëtorit se është koha për të ngarkuar.

// Prisni që tema të përfundojë:

DWORD dwPris Rezultati = :: WaitForSingleObject ( hWorkerThread , PAFUNDI );

nëse( dwPrisni Rezultatin != WAIT_OBJECT_0 ) { /* trajtimi i gabimeve */ }

// "Doreza" e rrjedhës mund të mbyllet:

VERIFIKONI (:: Mbylle Handle ( hWorkerThread );

/* Nëse CloseHandle dështon dhe kthen FALSE, nuk bëj përjashtim. Së pari, edhe nëse kjo do të ndodhte për shkak të një gabimi të sistemit, nuk do të kishte pasoja të drejtpërdrejta për programin tonë, sepse duke qenë se e mbyllim dorezën, atëherë nuk pritet punë me të në të ardhmen. Në realitet, dështimi i CloseHandle mund të nënkuptojë vetëm një gabim në programin tuaj. Prandaj, ne do të fusim makro VERIFY këtu në mënyrë që të mos e humbasim atë në fazën e korrigjimit të aplikacionit. */

Kodi që pret që procesi të përfundojë do të duket i ngjashëm.

Nëse nuk do të kishte një aftësi të tillë të integruar, filli i punëtorit do të duhej të kalonte disi informacionin për përfundimin e tij në vetë fillin kryesor. Edhe nëse do ta bënte këtë të fundit, filli kryesor nuk mund të ishte i sigurt se punëtori nuk kishte të paktën disa udhëzime të montimit për t'i ekzekutuar. AT situata individuale(për shembull, nëse kodi i fillit është në një DLL që duhet të shkarkohet kur të përfundojë) kjo mund të jetë fatale.

Dua t'ju kujtoj se edhe pasi një fill (ose proces) të ketë përfunduar, dorezat e tij mbeten ende në fuqi derisa të mbyllen në mënyrë eksplicite nga funksioni CloseHandle. (Meqë ra fjala, mos harroni ta bëni këtë!) Kjo është bërë vetëm në mënyrë që në çdo kohë të mund të kontrolloni statusin e fillit.

Pra, funksioni WaitForMultipleObjects (dhe analogët e tij) ju lejon të sinkronizoni ekzekutimin e një filli me gjendjen e objekteve të sinkronizimit, në veçanti, temat dhe proceset e tjera.

Objekte të veçanta të kernelit

Le të kalojmë në shqyrtimin e objekteve të kernelit, të cilat janë krijuar posaçërisht për sinkronizim. Këto janë ngjarje, semaforë dhe mutexes. Le të hedhim një vështrim të shkurtër në secilën prej tyre:

ngjarje

Ndoshta objekti më i thjeshtë dhe më themelor sinkronizues. Ky është vetëm një flamur që mund të vendoset me funksionet SetEvent / ResetEvent: sinjalizimi ose neutral. Një ngjarje është mënyra më e përshtatshme për t'i sinjalizuar një fije në pritje se një ngjarje ka ndodhur (prandaj quhet) dhe ju mund të vazhdoni të punoni. Duke përdorur një ngjarje, ne mund ta zgjidhim lehtësisht problemin e sinkronizimit kur inicializojmë një fill pune:

// Le ta mbajmë dorezën e ngjarjes në një ndryshore globale për thjeshtësi:

HANDLE g_hEventInitComplete = I PAVLEFSHËM ; // mos e lini kurrë një variabël të pa inicializuar!

{ // kodi në temën kryesore

// krijoni një ngjarje

g_hEventInitComplete = :: Krijo Ngjarje ( I PAVLEFSHËM,

I RREMË , // do të flasim për këtë parametër më vonë

I RREMË , // gjendja fillestare - neutrale

nëse(! g_hEventInitComplete ) { /* Mos harroni për trajtimin e gabimeve */ }

// krijoni një fill pune

DWORD idWorkerThread = 0 ;

TRAJTO hWorkerThread = :: KrijoThread ( I PAVLEFSHËM , 0 , & WorkerThreadProc , I PAVLEFSHËM , 0 , & idWorkerThread );

nëse(! hWorkerThread ) { /* trajtimi i gabimeve */ }

// prisni për një sinjal nga filli i punëtorit

DWORD dwPris Rezultati = :: WaitForSingleObject ( g_hEventInitComplete , PAFUNDI );

nëse( dwPrisni Rezultatin != WAIT_OBJECT_0 ) { /* gabim */ }

// tani mund të jeni i sigurt se filli i punëtorit ka përfunduar inicializimin.

VERIFIKONI (:: Mbylle Handle ( g_hEventInitComplete )); // mos harroni të mbyllni objektet e panevojshme

g_hEventInitComplete = I PAVLEFSHËM ;

// funksioni i rrjedhës së punës

DWORD WINAPI WorkerThreadProc ( LPVOID_parametri )

InitializeWorker (); // inicializimi

// sinjalizon se inicializimi ka përfunduar

BOOL është në rregull = :: SetEvent ( g_hEventInitComplete );

nëse(! është në rregull ) { /* gabim */ }

Duhet të theksohet se ekzistojnë dy lloje të ndryshme ngjarjesh. Ne mund të zgjedhim njërën prej tyre duke përdorur parametrin e dytë të funksionit CreateEvent. Nëse është E VËRTETË, krijohet një ngjarje, gjendja e së cilës kontrollohet vetëm manualisht, domethënë nga funksionet SetEvent/ResetEvent. Nëse është FALSE, do të krijohet një ngjarje e rivendosjes automatike. Kjo do të thotë që sapo një thread që pret për një ngjarje të caktuar lëshohet nga një sinjal nga kjo ngjarje, ai automatikisht do të rivendoset përsëri në një gjendje neutrale. Dallimi i tyre është më i theksuar në një situatë ku disa fije presin një ngjarje në të njëjtën kohë. Një ngjarje e kontrolluar me dorë është si një pistoletë nisjeje. Sapo të vendoset në gjendjen e sinjalizuar, të gjitha fijet do të lëshohen menjëherë. Një ngjarje e rivendosjes automatike, nga ana tjetër, është si një rrotullues metroje: do të lëshojë vetëm një rrjedhë dhe do të kthehet në një gjendje neutrale.

Mutex

Krahasuar me një ngjarje, ky është një objekt më i specializuar. Zakonisht përdoret për të zgjidhur një problem të zakonshëm sinkronizimi, siç është qasja në një burim të ndarë nga fije të shumta. Në shumë mënyra, është e ngjashme me një ngjarje të rivendosjes automatike. Dallimi kryesor është se ka një lidhje të veçantë me një fije specifike. Nëse mutex është në gjendje të sinjalizuar, do të thotë se është i lirë dhe nuk i përket asnjë thread. Sapo një fill i caktuar të ketë pritur për këtë mutex, ky i fundit rivendoset në një gjendje neutrale (këtu është tamam si një ngjarje e rivendosjes automatike) dhe filli bëhet pronar i tij derisa të lëshojë në mënyrë të qartë mutex-in me funksionin ReleaseMutex. ose përfundon. Kështu, për t'u siguruar që vetëm një thread punon me të dhëna të përbashkëta në të njëjtën kohë, të gjitha vendet ku kryhet një punë e tillë duhet të rrethohen nga një palë: WaitFor - ReleaseMutex :

DORËZUES g_hMutex ;

// Lëreni dorezën mutex të ruhet në një ndryshore globale. Natyrisht, ajo duhet të krijohet paraprakisht, përpara fillimit të fijeve të punëtorëve. Le të supozojmë se kjo tashmë është bërë.

ndër unë pres = :: WaitForSingleObject ( g_hMutex , PAFUNDI );

kaloni( unë pres ) {

rast WAIT_OBJECT_0 : // Cdo gje eshte ne rregull

thyej;

rast PRIT_ABANIZUAR : /* Disa tema mbaruan, duke harruar të telefononi ReleaseMutex. Me shumë mundësi, kjo do të thotë një gabim në programin tuaj! Prandaj, për çdo rast, ne do të fusim ASSERT këtu, por në versionin përfundimtar (lëshimin) do ta konsiderojmë këtë kod si të suksesshëm. */

pohon ( i rremë );

thyej;

default:

// Trajtimi i gabimeve duhet të jetë këtu.

// Një pjesë kodi e mbrojtur nga një mutex.

ProcessCommon Data ();

VERIFIKONI (:: ReleaseMutex ( g_hMutex ));

Pse është një mutex më i mirë se një ngjarje e rivendosjes automatike? Në shembullin e mësipërm, mund të përdoret gjithashtu, vetëm ReleaseMutex duhet të zëvendësohet me SetEvent. Megjithatë, mund të lindë vështirësia e mëposhtme. Më shpesh, ju duhet të punoni me të dhëna të përbashkëta në disa vende. Çfarë ndodh nëse ProcessCommonData në shembullin tonë thërret një funksion që punon me të njëjtat të dhëna dhe i cili tashmë ka çiftin e vet të WaitFor - ReleaseMutex (në praktikë kjo është shumë e zakonshme)? Nëse do të përdornim një ngjarje, programi padyshim do të varej, sepse brenda bllokut të mbrojtur, ngjarja është në një gjendje neutrale. Muteksi është më i ndërlikuar. Ai mbetet gjithmonë në gjendje sinjalizuese për fillin kryesor, edhe pse është në gjendje neutrale për të gjitha thread-et e tjera. Prandaj, nëse një thread ka marrë mutex, thirrja e funksionit WaitFor përsëri nuk do të bllokojë. Për më tepër, një numërues është gjithashtu i integruar në mutex, kështu që ReleaseMutex duhet të thirret po aq herë sa ka pasur thirrje në WaitFor. Kështu, ne mund të mbrojmë me siguri çdo pjesë të kodit që funksionon me të dhëna të përbashkëta me një çift WaitFor - ReleaseMute x pa u shqetësuar se ky kod mund të thirret në mënyrë rekursive. Kjo e bën mutex një mjet shumë të lehtë për t'u përdorur.

Semafor

Një objekt sinkronizimi edhe më specifik. Më duhet të rrëfej se në praktikën time nuk ka pasur ende një rast që do të ishte e dobishme. Një semafor është krijuar për të kufizuar numrin maksimal të fijeve që mund të punojnë në një burim në të njëjtën kohë. Në thelb, një semafor është një ngjarje me një numërues. Për sa kohë që ky numërues është më i madh se zero, semafori është në gjendje sinjalizimi. Megjithatë, çdo thirrje në WaitFor e zvogëlon këtë numërues me një derisa të bëhet zero dhe semafori shkon në gjendje neutrale. Ashtu si një mutex, një semafor ka një funksion ReleaseSemaphor që rrit një numërues. Megjithatë, ndryshe nga një mutex, një semafor nuk është i lidhur me fije, dhe thirrja përsëri WaitFor/ReleaseSemaphor do të zvogëlojë/shtojë numëruesin.

Si mund të përdoret një semafor? Për shembull, mund të përdoret për të kufizuar artificialisht multithreading. Siç e përmenda tashmë, shumë fije aktive në të njëjtën kohë mund të degradojnë dukshëm performancën e të gjithë sistemit për shkak të ndërrimeve të shpeshta të kontekstit. Dhe nëse do të duhej të krijonim shumë thread-e punëtore, ne mund ta kufizojmë numrin e thread-ve njëkohësisht aktivë në një numër sipas renditjes së numrit të procesorëve.

Çfarë tjetër mund të thuhet për objektet e sinkronizimit të kernelit? Është shumë e përshtatshme për t'u dhënë atyre emra. Të gjitha funksionet që krijojnë objekte sinkronizimi kanë parametrin përkatës: CreateEvent , CreateMutex , CreateSemaphore . Nëse telefononi, për shembull, CreateEvent dy herë, të dyja herët duke specifikuar të njëjtin emër jo bosh, atëherë herën e dytë funksioni, në vend që të krijojë një objekt të ri, do të kthejë dorezën e një ekzistuesi. Kjo do të ndodhë edhe nëse thirrja e dytë është bërë nga një proces tjetër. Ky i fundit është shumë i përshtatshëm në rastet kur dëshironi të sinkronizoni temat që i përkasin proceseve të ndryshme.

Kur nuk ju nevojitet më objekti i sinkronizimit, mos harroni të telefononi funksionin CloseHandle që përmenda më herët kur fola për temat. Në fakt, nuk do ta fshijë domosdoshmërisht objektin menjëherë. Çështja është se një objekt mund të ketë disa doreza, dhe më pas do të fshihet vetëm kur të mbyllet e fundit.

Unë dua t'ju kujtoj se Menyra me e mire për të siguruar që CloseHandle ose një funksion i ngjashëm "pastrimi" është i sigurt që do të thirret, edhe në rast të një situate jonormale, është ta vendosni atë në një destruktor. Nga rruga, kjo dikur ishte përshkruar mirë dhe në detaje në artikullin e Kirill Pleshivtsev "Shkatërruesi i zgjuar". Në shembujt e mësipërm, unë nuk e përdora këtë teknikë vetëm për qëllime edukative, në mënyrë që puna e funksioneve të API të ishte më vizuale. Në kodin real, duhet të përdorni gjithmonë klasa mbështjellëse me destruktorë inteligjentë për pastrim.

Nga rruga, me funksionin ReleaseMutex dhe të ngjashme, i njëjti problem lind vazhdimisht si me CloseHandle. Duhet të thirret në fund të punës me të dhëna të përbashkëta, pavarësisht se sa me sukses u krye kjo punë (në fund të fundit, mund të hidhet një përjashtim). Pasojat e “harresës” janë më të rënda këtu. Nëse nuk quhet CloseHandle, do të rrjedhë vetëm burime (gjë që është gjithashtu e keqe!), atëherë një mutex i papublikuar do të parandalojë që temat e tjera të punojnë me burimin e përbashkët deri në përfundimin e fillit të dështuar, gjë që ka shumë të ngjarë të mos lejojë që aplikacioni të funksionojë normalisht. Për të shmangur këtë, një klasë e trajnuar posaçërisht me një destruktor të zgjuar do të na ndihmojë përsëri.

Duke përfunduar rishikimin e objekteve të sinkronizimit, do të doja të përmendja një objekt që nuk është në API Win32. Shumë nga kolegët e mi pyesin pse Win32 nuk ka një objekt të specializuar "një shkruan, shumë lexon". Një lloj "mutex i avancuar", i cili siguron që vetëm një thread mund të ketë qasje në të dhënat e përbashkëta në të njëjtën kohë për të shkruar, dhe disa thread mund të lexohen vetëm në të njëjtën kohë. Një objekt i ngjashëm mund të gjendet në UNIX "ah. Disa biblioteka, për shembull nga Borland, ofrojnë ta imitojnë atë bazuar në objektet standarde të sinkronizimit. Megjithatë, përfitimi i vërtetë i emulimeve të tilla është shumë i dyshimtë. Një objekt i tillë mund të zbatohet në mënyrë efektive vetëm në niveli i kernelit të sistemit operativ.Por në kernel Windows nuk ofron një objekt të tillë.

Pse zhvilluesit e kernelit të Windows NT nuk u kujdesën për këtë? Pse jemi më keq se UNIX? Për mendimin tim, përgjigja është se thjesht nuk ka pasur ende një nevojë reale për një objekt të tillë për Windows. Në një makinë normale me një procesor, ku fijet ende fizikisht nuk mund të punojnë njëkohësisht, ajo do të jetë praktikisht ekuivalente me një mutex. Në një makinë me shumë procesor, ajo mund të përfitojë duke lejuar që thread-et e lexuesit të funksionojnë paralelisht. Në të njëjtën kohë, ky fitim do të bëhet i prekshëm vetëm kur probabiliteti i një "përplasjeje" të fijeve të leximit është i lartë. Pa dyshim, për shembull, në një makinë me procesor 1024, një objekt i tillë kernel do të jetë jetik. Makina të ngjashme ekzistojnë, por ato janë sisteme të specializuara që përdorin sisteme të specializuara operative. Shpesh, sisteme të tilla operative ndërtohen në bazë të UNIX, me siguri prej andej një objekt si "një shkruan, shumë lexon" hyn në versionet më të përdorura të këtij sistemi. Por në makinat x86 me të cilat jemi mësuar, si rregull, instalohen vetëm një dhe vetëm herë pas here dy procesorë. Dhe vetëm modelet më të avancuara të procesorëve si Intel Xeon mbështesin 4 ose edhe më shumë konfigurime të procesorëve, por sisteme të tilla mbeten ende ekzotike. Por edhe në një sistem kaq "të avancuar", një "mutex i avancuar" mund të japë një përfitim të dukshëm të performancës vetëm në situata shumë specifike.

Kështu, zbatimi i një mutex "të avancuar" thjesht nuk ia vlen telashet. Në një makinë "me procesor të ulët", ajo mund të jetë edhe më pak efikase për shkak të kompleksitetit të logjikës së objektit në krahasim me një mutex standard. Ju lutemi vini re se zbatimi i një objekti të tillë nuk është aq i thjeshtë sa mund të duket në shikim të parë. Me një zbatim të pasuksesshëm, nëse ka shumë tema leximi, filli i shkrimit thjesht "nuk do të kalojë" në të dhëna. Për këto arsye, unë gjithashtu nuk ju rekomandoj që të provoni të imitoni një objekt të tillë. Në aplikacionet reale në makinat reale, një seksion i rregullt mutex ose kritik (i cili do të diskutohet në pjesën tjetër të artikullit) do të përballojë në mënyrë të përkryer detyrën e sinkronizimit të aksesit në të dhënat e përbashkëta. Edhe pse, supozoj, me zhvillimin e Windows OS, objekti i kernelit "një shkruan, shumë lexon" do të shfaqet herët a vonë.

Shënim. Në fakt, objekti "një shkruan - shumë lexojnë" në Windows NT ekziston ende. Thjesht nuk e dija kur shkrova këtë artikull. Ky objekt quhet "burimet e kernelit" dhe nuk është i aksesueshëm për programet e modalitetit të përdoruesit, prandaj ndoshta nuk është i njohur mirë. Ngjashmëritë në lidhje me të mund të gjenden në DDK. Faleminderit Konstantin Manurin që ma theksoi këtë.

Bllokim

Tani le të kthehemi te funksioni WaitForMultipleObjects, më saktë, te parametri i tretë i tij, bWaitAll. Unë premtova t'ju them pse aftësia për të pritur disa objekte në të njëjtën kohë është kaq e rëndësishme.

Pse nevojitet një funksion për të pritur një nga disa objekte është e kuptueshme. Në mungesë të një funksioni të veçantë, kjo mund të bëhet, përveçse duke kontrolluar në mënyrë sekuenciale gjendjen e objekteve në një lak bosh, gjë që, natyrisht, është e papranueshme. Por nevoja për një funksion të veçantë që ju lejon të prisni momentin kur disa objekte hyjnë në gjendjen e sinjalit menjëherë nuk është aq e dukshme. Në të vërtetë, imagjinoni situatën tipike vijuese: në një moment të caktuar, filli ynë ka nevojë për qasje në dy grupe të dhënash të përbashkëta në të njëjtën kohë, secila prej të cilave është përgjegjëse për mutex-in e vet, le t'i quajmë A dhe B. Duket se filli mund të fillimisht prisni derisa të lëshohet mutex A, kapeni atë, më pas prisni që të lëshohet mutex B... Duket se mund të bëjmë disa thirrje në WaitForSingleObject. Në të vërtetë, kjo do të funksionojë, por vetëm për sa kohë që të gjitha fijet e tjera marrin mutekset në të njëjtin rend: së pari A, pastaj B. Çfarë ndodh nëse një fije e caktuar përpiqet të bëjë të kundërtën: së pari fitoni B, pastaj A? Herët a vonë, do të lindë një situatë kur një thread ka kapur mutex A, një tjetër B, i pari pret që B të lëshohet, i dyti A. Është e qartë se ata nuk do ta presin kurrë këtë dhe programi do të varet.

Ky lloj bllokimi është një gabim shumë i zakonshëm. Si të gjitha gabimet që lidhen me sinkronizimin, ai shfaqet vetëm herë pas here dhe mund të shkatërrojë shumë nerva për një programues. Në të njëjtën kohë, pothuajse çdo skemë që përfshin disa objekte sinkronizimi është e mbushur me bllokim. Prandaj, këtij problemi duhet t'i kushtohet vëmendje e veçantë në fazën e projektimit të një qarku të tillë.

Në shembullin e thjeshtë të dhënë, bllokimi është mjaft i lehtë për t'u shmangur. Është e nevojshme të kërkohet që të gjitha thread-et të marrin mutexes në një rend të caktuar: së pari A, pastaj B. Megjithatë, në një program kompleks, ku ka shumë objekte të lidhura me njëri-tjetrin në mënyra të ndryshme, kjo zakonisht nuk është aq e lehtë për t'u arritur. Jo dy, por shumë objekte dhe fije mund të përfshihen në një bravë. Prandaj, më së shumti mënyrë e besueshme Për të shmangur bllokimin në një situatë ku një thread ka nevojë për disa objekte sinkronizimi në të njëjtën kohë, duhet t'i kapni të gjithë me një thirrje në funksionin WaitForMultipleObjects me parametrin bWaitAll=TRUE. Të them të drejtën, në këtë rast, problemin e ngërçit thjesht e zhvendosim në bërthamën e sistemit operativ, por kryesorja është se ky nuk do të jetë më shqetësimi ynë. Megjithatë, në një program kompleks me shumë objekte, kur nuk është gjithmonë e mundur të tregohet menjëherë se cili prej tyre do të kërkohet për të kryer një operacion të caktuar, shpesh nuk është e lehtë të sillni të gjitha thirrjet WaitFor në një vend dhe t'i kombinoni ato gjithashtu.

Pra, ka dy mënyra për të shmangur bllokimin. Ju ose duhet të siguroheni që objektet e sinkronizimit janë kapur gjithmonë nga thread-at në të njëjtin rend, ose që ato janë kapur nga një thirrje e vetme në WaitForMultipleObjects. Metoda e fundit është më e thjeshtë dhe e preferuar. Sidoqoftë, në praktikë, me përmbushjen e të dy kërkesave, vazhdimisht lindin vështirësi, është e nevojshme të kombinohen të dyja këto qasje. Projektimi i qarqeve komplekse të kohës është shpesh një detyrë shumë jo e parëndësishme.

Shembull sinkronizimi

Në shumicën e situatave tipike, si ato që përshkrova më sipër, nuk është e vështirë të organizosh sinkronizimin, mjafton një ngjarje ose një mutex. Por periodikisht ka raste më komplekse ku zgjidhja e problemit nuk është aq e dukshme. Unë do të doja ta ilustroj këtë me një shembull konkret nga praktika ime. Siç do ta shihni, zgjidhja doli të ishte çuditërisht e thjeshtë, por para se ta gjeja, më duhej të provoja disa opsione të pasuksesshme.

Pra, detyra. Pothuajse të gjithë menaxherët modernë të shkarkimit, ose thjesht "karriget lëkundëse", kanë aftësinë të kufizojnë trafikun në mënyrë që "karrigia lëkundëse" që funksionon në sfond të mos ndërhyjë shumë në shfletimin e internetit të përdoruesit. Unë po zhvilloja një program të ngjashëm dhe më dhanë detyrën të zbatoja pikërisht një "funksion" të tillë. Karrigia ime lëkundëse funksionoi sipas skemës klasike të multithreading, kur çdo detyrë, në këtë rast, shkarkimi i një skedari specifik, trajtohet nga një fill i veçantë. Kufiri i trafikut duhet të ishte kumulativ për të gjitha flukset. Kjo do të thotë, ishte e nevojshme të sigurohej që gjatë një intervali të caktuar kohor të gjitha rrymat të lexojnë nga bazat e tyre jo më shumë se një numër i caktuar bajt. Thjesht ndarja e këtij kufiri në mënyrë të barabartë midis transmetimeve do të jetë padyshim joefikase, pasi shkarkimi i skedarëve mund të jetë shumë i pabarabartë, njëri do të shkarkohet shpejt, tjetri ngadalë. Prandaj, na duhet një numërues i përbashkët për të gjitha thread-ët, sa bajt janë lexuar dhe sa të tjera mund të lexohen. Kjo është ajo ku sinkronizimi vjen në ndihmë. Një kompleksitet shtesë i detyrës u dha nga kërkesa që në çdo kohë të mund të ndalohej ndonjë nga fijet e punës.

Le ta formulojmë problemin në mënyrë më të detajuar. Vendosa të mbyll sistemin e sinkronizimit në një klasë të veçantë. Këtu është ndërfaqja e saj:

klasës CKuota {

publike: // metodat

i pavlefshëm vendosur ( i panënshkruar int _nKuota );

i panënshkruar int Kërkesë ( i panënshkruar int _nBytesToRead , HANDLE_hStopEvent );

i pavlefshëm Lirimi ( i panënshkruar int _nBytesRevert , HANDLE_hStopEvent );

Periodikisht, le të themi një herë në sekondë, filli i kontrollit thërret metodën Set, duke vendosur kuotën e shkarkimit. Përpara se thread-i i punëtorit të lexojë të dhënat e marra nga rrjeti, thërret metodën Request, e cila kontrollon që kuota aktuale të mos jetë zero, dhe nëse po, kthen numrin e bajteve që mund të lexohen më pak se kuota aktuale. Kuota është reduktuar përkatësisht me këtë numër. Nëse kuota është zero kur thirret Kërkesa, filli i thirrjes duhet të presë derisa kuota të jetë e disponueshme. Ndonjëherë ndodh që realisht të merren më pak bajt se sa kërkohet, me ç'rast thread-i kthen një pjesë të kuotës që i është caktuar me metodën Release. Dhe, siç thashë, përdoruesi në çdo kohë mund të japë komandën për të ndaluar shkarkimin. Në këtë rast, pritja duhet të ndërpritet, pavarësisht nga prania e një kuote. Për këtë përdoret një ngjarje e veçantë: _hStopEvent. Për shkak se detyrat mund të nisen dhe të ndalen në mënyrë të pavarur, çdo fill pune ka ngjarjen e vet të ndalimit. Trajtimi i tij i kalohet metodave Kërkesë dhe Lëshim.

Në një nga opsionet e pasuksesshme, u përpoqa të përdor një kombinim të një mutex që sinkronizon aksesin në klasën CQuota dhe një ngjarje që sinjalizon ekzistencën e një kuote. Megjithatë, ngjarja e ndalimit nuk përshtatet në këtë skemë. Nëse një thread dëshiron të marrë një kuotë, atëherë gjendja e tij e pritjes duhet të kontrollohet nga një shprehje komplekse boolean: ((mutex AND quota event) OSE stop event). Por WaitForMultipleObjects nuk e lejon këtë, ju mund të kombinoni disa objekte kernel ose me një operacion AND ose OR, por jo të përziera. Përpjekja për të ndarë pritjen me dy thirrje të njëpasnjëshme në WaitForMultipleObjects rezulton në mënyrë të pashmangshme në një bllokim. Në përgjithësi, kjo rrugë doli të ishte një rrugë pa krye.

Nuk do ta lë më mjegullën dhe do t'ju them zgjidhjen. Siç thashë, një mutex është shumë i ngjashëm me një ngjarje të rivendosjes automatike. Dhe këtu kemi vetëm atë rast të rrallë kur është më i përshtatshëm për ta përdorur atë, por jo një, por dy menjëherë:

klasës CKuota {

private: // të dhëna

i panënshkruar int m_nKuota ;

CEvent m_eventHasQuota ;

CEvent m_eventNoQuota ;

Vetëm një nga këto ngjarje mund të caktohet në të njëjtën kohë. Çdo thread që manipulon kuotën duhet të vendosë ngjarjen e parë nëse kuota e mbetur nuk është zero dhe e dyta nëse kuota është shteruar. Një thread që dëshiron të marrë një kuotë duhet të presë për ngjarjen e parë. Fillimi i rritjes së kuotës duhet vetëm të presë për ndonjë nga këto ngjarje, sepse nëse të dyja janë në gjendje të rivendosur, kjo do të thotë se një fill tjetër po punon aktualisht me kuotën. Kështu, dy ngjarje kryejnë dy funksione njëherësh: sinkronizimi i aksesit të të dhënave dhe pritja. Më në fund, duke qenë se filli është duke pritur për një nga dy ngjarjet, ngjarja që sinjalizon të ndalet përfshihet lehtësisht.

Unë do të jap një shembull të zbatimit të metodës Kërkesë. Pjesa tjetër zbatohet në të njëjtën mënyrë. Unë thjeshtova pak kodin e përdorur në projektin real:

i panënshkruar int CKuota :: Kërkesë ( i panënshkruar int _nKërkesë , HANDLE_hStopEvent )

nëse(! _nKërkesë ) kthimi 0 ;

i panënshkruar int nSiguroni = 0 ;

TRAJTOJ NGJARJET [ 2 ];

hNgjarjet [ 0 ] = _hStopEvent ; // Ngjarja Stop ka përparësi më të lartë. E vendosëm të parën.

hNgjarjet [ 1 ] = m_ngjarjaKa Kuota ;

ndër iPris Rezultati = :: WaitFor Multiple Objects ( 2 , hNgjarjet , I RREMË , PAFUNDI );

kaloni( iPris Rezultati ) {

rast PRIT_ Dështoi :

// GABIM

hedh të re CWin32 Përjashtim ;

rast WAIT_OBJECT_0 :

// Ndaloni ngjarjen. Unë e trajtova atë me një përjashtim të personalizuar, por asgjë nuk më pengon ta zbatoj atë në një mënyrë tjetër.

hedh të re CStopException ;

rast WAIT_OBJECT_0 + 1 :

// Ngjarja "e disponueshme kuota"

pohon ( m_nKuota ); // Nëse sinjali është dhënë nga kjo ngjarje, por në fakt nuk ka kuotë, atëherë diku kemi gabuar. Duhet të kërkoj të metën!

nëse( _nKërkesë >= m_nKuota ) {

nSiguroni = m_nKuota ;

m_nKuota = 0 ;

m_ngjarja Jo Kuota . vendosur ();

tjetër {

nSiguroni = _nKërkesë ;

m_nKuota -= _nKërkesë ;

m_ngjarjaKa Kuota . vendosur ();

thyej;

kthimi nSiguroni ;

Një shënim i vogël. Biblioteka MFC nuk u përdor në atë projekt, por, siç e keni menduar tashmë, unë bëra klasën time CEvent, një mbështjellës rreth objektit të kernelit "event", i ngjashëm me MFC "schnoy. Siç thashë, klasa të tilla të thjeshta mbështjellëse janë shumë të dobishme kur ka ndonjë burim (në këtë rast, një objekt kernel) që duhet të mbahet mend për t'u çliruar në fund të punës. Në pjesën tjetër, nuk ka rëndësi nëse shkruani SetEvent(m_hEvent) ose m_event.Set( ).

Shpresoj se ky shembull do t'ju ndihmojë të hartoni skemën tuaj të kohës nëse hasni në një situatë jo të parëndësishme. Gjëja kryesore është të analizoni skemën tuaj sa më me kujdes. A mund të ketë një situatë në të cilën nuk do të funksiononte si duhet, në veçanti, a mund të ndodhte bllokimi? Kapja e gabimeve të tilla në korrigjues është zakonisht një biznes i pashpresë, vetëm analiza e detajuar ndihmon këtu.

Pra, ne kemi konsideruar mjet thelbësor sinkronizimi i fijeve: objektet e sinkronizimit të kernelit. Është një mjet i fuqishëm dhe i gjithanshëm. Me të, ju mund të ndërtoni edhe skema shumë komplekse sinkronizimi. Për fat të mirë, situata të tilla jo të parëndësishme janë të rralla. Për më tepër, shkathtësia vjen gjithmonë me koston e performancës. Prandaj, në shumë raste ia vlen të përdorni veçoritë e tjera të sinkronizimit të fijeve të disponueshme në Windows, të tilla si seksionet kritike dhe operacionet atomike. Ato nuk janë aq universale, por janë të thjeshta dhe efektive. Për to do të flasim në pjesën tjetër.

Një proces është një shembull i një programi të ngarkuar në memorie. Ky shembull mund të krijojë threads, të cilat janë një sekuencë udhëzimesh që duhen ekzekutuar. Është e rëndësishme të kuptohet se nuk janë proceset që po ekzekutohen, por thread-et.

Për më tepër, çdo proces ka të paktën një fije. Ky thread quhet filli kryesor (kryesor) i aplikacionit.

Meqenëse ka pothuajse gjithmonë shumë më shumë thread se sa ka procesorë fizikë për ekzekutimin e tyre, thread-et në të vërtetë ekzekutohen jo njëkohësisht, por nga ana tjetër (shpërndarja e kohës së procesorit ndodh pikërisht midis thread-ve). Por kalimi mes tyre ndodh aq shpesh sa duket sikur po vrapojnë paralelisht.

Në varësi të situatës, fijet mund të jenë në tre gjendje. Së pari, një thread mund të ekzekutohet kur i jepet koha e CPU-së, d.m.th. mund të jetë aktiv. Së dyti, mund të jetë joaktiv dhe të presë që të ndahet një procesor, d.m.th. të jetë në gjendje gatishmërie. Dhe ka një të tretë, gjithashtu shumë kusht i rëndësishëm- gjendja e bllokimit. Kur një thread bllokohet, nuk i ndahet fare kohë. Zakonisht, një bravë vendoset gjatë pritjes për ndonjë ngjarje. Kur ndodh kjo ngjarje, thread-i kalon automatikisht nga gjendja e bllokuar në gjendjen gati. Për shembull, nëse një thread po kryen llogaritjet ndërsa një tjetër duhet të presë që rezultatet të ruhen në disk. E dyta mund të përdorë një lak si "while(!isCalcFinished) continue;", por është e lehtë të shihet në praktikë se procesori është 100% i zënë ndërsa ky lak është në punë (kjo quhet pritje aktive). Sythe të tilla duhet të shmangen sa herë që është e mundur, në të cilat mekanizmi i mbylljes ofron ndihmë të paçmuar. Fillimi i dytë mund të bllokohet vetë derisa filli i parë të vendosë një ngjarje për të sinjalizuar që leximi ka përfunduar.

Sinkronizimi i temave në Windows OS

Windows zbaton multitasking parandalues, që do të thotë se në çdo kohë sistemi mund të ndërpresë ekzekutimin e një thread dhe të transferojë kontrollin në një tjetër. Më parë, në Windows 3.1, u përdor një metodë organizimi e quajtur multitasking bashkëpunues: sistemi priste derisa vetë filli të transferonte kontrollin tek ai, dhe kjo është arsyeja pse nëse një aplikacion ngrin, kompjuteri duhej të rifillohej.

Të gjitha thread-at që i përkasin të njëjtit proces ndajnë disa burime të përbashkëta, të tilla si hapësira e adresave të RAM-it ose skedarët e hapur. Këto burime i përkasin të gjithë procesit, dhe rrjedhimisht në secilën prej fijeve të tij. Prandaj, çdo thread mund të punojë me këto burime pa asnjë kufizim. Por... Nëse një thread ende nuk ka përfunduar së punuari me ndonjë burim të përbashkët, dhe sistemi ka kaluar në një fill tjetër duke përdorur të njëjtin burim, atëherë rezultati i punës së këtyre temave mund të jetë jashtëzakonisht i ndryshëm nga ai që synohej. Konflikte të tilla mund të lindin edhe ndërmjet fijeve që i përkasin proceseve të ndryshme. Sa herë që dy ose më shumë thread përdorin një lloj burimi të përbashkët, ky problem ndodh.

Shembull. Temat jashtë sinkronizimit: Nëse e pezulloni përkohësisht fillin e ekranit (pauzë), filli i mbushjes së grupit të sfondit do të vazhdojë të funksionojë.

#përfshi #përfshi int a; TRAJTO hThr; uThrID e gjatë e panënshkruar; void Thread(void* pParams) ( int i, num = 0; ndërsa (1) ( për (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; }

Kjo është arsyeja pse nevojitet një mekanizëm për të lejuar thread-ët të koordinojnë punën e tyre me burimet e përbashkëta. Ky mekanizëm quhet mekanizmi i sinkronizimit të fijeve.

Ky mekanizëm është një grup objektesh të sistemit operativ që krijohen dhe menaxhohen nga softueri, janë të përbashkëta për të gjitha thread-ët në sistem (disa janë të përbashkëta nga thread-et që i përkasin të njëjtit proces) dhe përdoren për të koordinuar aksesin në burime. Burimet mund të jenë çdo gjë që mund të ndahet nga dy ose më shumë thread - një skedar në disk, një port, një hyrje në bazën e të dhënave, një objekt GDI dhe madje edhe një variabël programi global (i cili mund të aksesohet nga temat që i përkasin të njëjtit proces).

Ka disa objekte sinkronizimi, më të rëndësishmet prej të cilave janë mutex, seksioni kritik, ngjarja dhe semafori. Secili prej këtyre objekteve zbaton metodën e vet të sinkronizimit. Gjithashtu, vetë proceset dhe thread-et mund të përdoren si objekte sinkronizimi (kur një thread pret përfundimin e një thread ose procesi tjetër); si dhe skedarët, pajisjet e komunikimit, hyrjet e konsolës dhe njoftimet e ndryshimit.

Çdo objekt sinkronizimi mund të jetë në të ashtuquajturën gjendje të sinjalizuar. Për çdo lloj objekti, kjo gjendje ka një kuptim të ndryshëm. Thread-et mund të kontrollojnë gjendjen aktuale të një objekti dhe/ose të presin që ajo gjendje të ndryshojë dhe kështu të koordinojnë veprimet e tyre. Kjo siguron që kur një thread punon me objektet e sinkronizimit (i krijon ato, ndryshon gjendjen), sistemi nuk do të ndërpresë ekzekutimin e tij derisa të përfundojë këtë veprim. Kështu, të gjitha operacionet përfundimtare në objektet e sinkronizimit janë atomike (të pandashme.

Puna me objektet e sinkronizimit

Për të krijuar një ose një objekt tjetër sinkronizimi, quhet një funksion i veçantë WinAPI i tipit Create... (p.sh. CreateMutex). Kjo thirrje kthen një dorezë objekti (HANDLE) që mund të përdoret nga të gjitha thread-et që i përkasin procesit të caktuar. Është e mundur të aksesoni objektin e sinkronizimit nga një proces tjetër, ose duke trashëguar dorezën e objektit ose, mundësisht, duke thirrur funksionin Open... të objektit. Pas kësaj thirrjeje, procesi do të marrë një dorezë, e cila më vonë mund të përdoret për të punuar me objektin. Një objekti, përveç nëse synohet të përdoret brenda një procesi të vetëm, duhet t'i jepet një emër. Emrat e të gjitha objekteve duhet të jenë të ndryshëm (edhe nëse janë të llojeve të ndryshme). Ju nuk mund, për shembull, të krijoni një ngjarje dhe një semafor me të njëjtin emër.

Nga përshkruesi i disponueshëm i një objekti, ju mund të përcaktoni gjendjen e tij aktuale. Kjo bëhet me ndihmën e të ashtuquajturit. funksionet në pritje. Funksioni më i përdorur është WaitForSingleObject. Ky funksion merr dy parametra, i pari është doreza e objektit, i dyti është skadimi në ms. Funksioni kthen WAIT_OBJECT_0 nëse objekti është në gjendjen e sinjalizuar, WAIT_TIMEOUT nëse koha e skaduar ka skaduar dhe WAIT_ABANDONED nëse mutex nuk është liruar përpara përfundimit të lidhjes zotëruese. Nëse skadimi specifikohet si zero, funksioni kthehet menjëherë, përndryshe ai pret kohën e caktuar. Nëse gjendja e objektit sinjalizohet përpara skadimit të kësaj kohe, funksioni do të kthehet WAIT_OBJECT_0, përndryshe funksioni do të kthehet WAIT_TIMEOUT. Nëse konstanta simbolike INFINITE specifikohet si kohë, atëherë funksioni do të presë pafundësisht derisa gjendja e objektit të sinjalizohet.

Është shumë e rëndësishme që thirrja në funksionin e pritjes të bllokojë thread-in aktual, d.m.th. ndërsa një thread është i papunë, nuk i ndahet kohë procesorit.

Seksione kritike

Një seksion kritik i objektit ndihmon programuesin të izolojë seksionin e kodit ku një thread akseson një burim të përbashkët dhe parandalon përdorimin e njëkohshëm të burimit. Përpara përdorimit të burimit, thread hyn në seksionin kritik (thërret funksioni EnterCriticalSection). Nëse ndonjë thread tjetër përpiqet të hyjë në të njëjtin seksion kritik, ekzekutimi i tij do të ndalet derisa filli i parë të largohet nga seksioni me një thirrje për LeaveCriticalSection. Përdoret vetëm për fije në një proces të vetëm. Rendi i hyrjes në seksionin kritik nuk është i përcaktuar.

Ekziston gjithashtu një funksion TryEnterCriticalSection që kontrollon nëse një seksion kritik është aktualisht i zënë. Me ndihmën e tij, filli në procesin e pritjes për qasje në burim nuk mund të bllokohet, por të kryejë disa veprime të dobishme.

Shembull. Sinkronizimi i fijeve duke përdorur seksione kritike.

#përfshi #përfshi CRITICAL_SECTION cs; int a; TRAJTO hThr; uThrID e gjatë e panënshkruar; void Thread(void* pParams) ( int i, num = 0; ndërsa (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; }

Përjashtimi i ndërsjellë

Objektet e përjashtimit të ndërsjellë (mutexes, mutex - nga MUTual EXclusion) ju lejojnë të koordinoni përjashtimin e ndërsjellë të aksesit në një burim të përbashkët. Gjendja e sinjalizuar e një objekti (d.m.th., gjendja "e vendosur") korrespondon me pikën në kohë kur objekti nuk i përket asnjë thread dhe mund të "kap". Anasjelltas, gjendja e "rivendosjes" (jo sinjalizuar) korrespondon me momentin kur një thread tashmë e zotëron këtë objekt. Qasja në një objekt jepet kur filli që zotëron objektin e lëshon atë.

Dy (ose më shumë) thread mund të krijojnë një mutex me të njëjtin emër duke thirrur funksionin CreateMutex. Fillimi i parë në të vërtetë krijon mutex, dhe fijet e ardhshme marrin një dorezë për një objekt tashmë ekzistues. Kjo bën të mundur që fijet e shumta të marrin një dorezë për të njëjtin mutex, duke e çliruar programuesin nga shqetësimi se kush e krijon në të vërtetë mutex. Nëse përdoret kjo qasje, është e dëshirueshme të vendosni flamurin bInitialOwner në FALSE, përndryshe do të ketë disa vështirësi në përcaktimin e krijuesit aktual të mutex.

Fijet e shumta mund të marrin një dorezë për të njëjtin mutex, duke bërë të mundur komunikimin midis proceseve. Ju mund të përdorni mekanizmat e mëposhtëm për këtë qasje:

  • Një proces fëmijë i krijuar duke përdorur funksionin CreateProcess mund të trashëgojë dorezën mutex nëse parametri lpMutexAttributes është specifikuar kur mutex është krijuar nga funksioni CreateMutex.
  • Një thread mund të marrë një dublikatë të një mutex ekzistues duke përdorur funksionin DuplicateHandle.
  • Një thread mund të specifikojë emrin e një mutex ekzistues kur thërret funksionet OpenMutex ose CreateMutex.

Për të deklaruar një mutex në pronësi të fillit aktual, duhet të thirret një nga funksionet në pritje. Fija që zotëron objektin mund ta "kap" atë në mënyrë të përsëritur sa herë të dojë (kjo nuk do të çojë në vetë-kyçje), por do të duhet ta lëshojë atë sa herë duke përdorur funksionin ReleaseMutex.

Për të sinkronizuar temat e një procesi, është më efikase të përdoren seksione kritike.

Shembull. Sinkronizimi i thread-eve duke përdorur mutexes.

#përfshi #përfshi DORËZO hMutex; int a; TRAJTO hThr; uThrID e gjatë e panënshkruar; void Thread(void* pParams) ( int i, num = 0; ndërsa (1) ( WaitForSingleObject(hMutex, INFINITE); për (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; }

Zhvillimet

Objektet e ngjarjes përdoren për të njoftuar fijet në pritje se një ngjarje ka ndodhur. Ekzistojnë dy lloje ngjarjesh - me rivendosje manuale dhe automatike. Rivendosja manuale kryhet nga funksioni ResetEvent. Ngjarjet e rivendosjes manuale përdoren për të njoftuar disa tema njëherësh. Kur përdorni një ngjarje të rivendosjes automatike, vetëm një fill pritës do të marrë njoftimin dhe do të vazhdojë ekzekutimin e tij, pjesa tjetër do të presë më tej.

Funksioni CreateEvent krijon një objekt ngjarjeje, SetEvent - vendos ngjarjen në gjendjen e sinjalit, ResetEvent - rivendos ngjarjen. Funksioni PulseEvent vendos ngjarjen dhe pasi rifillon temat që presin këtë ngjarje (të gjitha me një rivendosje manuale dhe vetëm një me një automatik), e rivendos atë. Nëse nuk ka fije në pritje, PulseEvent thjesht rivendos ngjarjen.

Shembull. Sinkronizimi i temave duke përdorur ngjarje.

#përfshi #përfshi TRAJTO hEvent1, hEvent2; int a; TRAJTO hThr; uThrID e gjatë e panënshkruar; void Thread(void* pParams) ( int i, num = 0; ndërsa (1) ( WaitForSingleObject(hEvent2, INFINITE); për (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; }

semaforë

Një objekt semafor është në fakt një objekt mutex me një numërues. Ky objekt e lejon veten të "kap" nga një numër i caktuar fijesh. Pas kësaj, "kapja" do të jetë e pamundur derisa një nga fijet e "kapura" më parë të semaforit ta lëshojë atë. Semaforët përdoren për të kufizuar numrin e temave që mund të kenë akses në një burim në të njëjtën kohë. Gjatë inicializimit, numri maksimal i thread-eve transferohet në objekt, pas çdo "kapjeje" numëruesi i semaforit zvogëlohet. Gjendja e sinjalit korrespondon me një vlerë numërues më të madhe se zero. Kur numëruesi është zero, semafori konsiderohet i pavendosur (rivendosur).

Funksioni CreateSemaphore krijon një objekt semafor me një tregues të vlerës fillestare maksimale të mundshme, OpenSemaphore - kthen një dorezë në një semafor ekzistues, semafori kapet duke përdorur funksionet e pritjes, ndërsa vlera e semaforit zvogëlohet me një, ReleaseSemaphore - lëshon semaforin me një rritje në vlerën e semaforit me vlerën e specifikuar në numrin e parametrit.

Shembull. Sinkronizimi i fijeve duke përdorur semaforë.

#përfshi #përfshi HENDLE hSem; int a; TRAJTO hThr; uThrID e gjatë e panënshkruar; void Thread(void* pParams) ( int i, num = 0; ndërsa (1) ( WaitForSingleObject(hSem, INFINITE); për (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; }

Qasje e mbrojtur në variabla

Ka një sërë funksionesh që ju lejojnë të punoni me variabla globale nga të gjitha temat pa u shqetësuar për sinkronizimin, sepse. këto funksione kujdesen vetë për të - ekzekutimi i tyre është atomik. Këto janë funksionet InterlockedIncrement, InterlockedDecrement, InterlockedExchange, InterlockedExchangeAdd dhe InterlockedCompareExchange. Për shembull, funksioni InterlockedIncrement rrit atomikisht vlerën e një ndryshoreje 32-bit me një, e cila është e dobishme për numërues të ndryshëm.

Për të marrë informacion të plotë në lidhje me qëllimin, përdorimin dhe sintaksën e të gjitha funksioneve WIN32 API, duhet të përdorni sistemin e ndihmës MS SDK, i cili është pjesë e mjediseve të programimit Borland Delphi ose CBuilder, si dhe MSDN, i cili ofrohet si pjesë e sistemi i programimit Visual C.


Për programet që përdorin fije ose procese të shumta, është e nevojshme që të gjithë të kryejnë funksionet që u janë caktuar në sekuencën e dëshiruar. Në mjedisin Windows 9x, për këtë qëllim, propozohet përdorimi i disa mekanizmave që sigurojnë funksionimin e qetë të fijeve. Këta mekanizma quhen mekanizmat e sinkronizimit. Supozoni se po zhvilloni një program në të cilin dy threads funksionojnë paralelisht. Çdo thread akseson një ndryshore të përbashkët globale. Një thread, sa herë që aksesohet kjo ndryshore, e rrit atë, dhe filli i dytë e zvogëlon atë. Me punën e njëkohshme asinkrone të fijeve, në mënyrë të pashmangshme lind situata e mëposhtme: - filli i parë ka lexuar vlerën e një variabli global në një lokal; - Sistemi operativ e ndërpret atë, pasi kuanti i kohës së procesorit që i është caktuar ka përfunduar dhe e transferon kontrollin në fillin e dytë; - filli i dytë gjithashtu lexoi vlerën e ndryshores globale në një lokale, e zvogëloi atë dhe e shkruajti vlerën e re përsëri; - OS përsëri transferon kontrollin në fillin e parë, i cili, duke mos ditur asgjë për veprimet e fillit të dytë, rrit variablin e tij lokal dhe e shkruan vlerën e tij në atë globale. Natyrisht, ndryshimet e bëra nga filli i dytë do të humbasin. Për të shmangur situata të tilla, është e nevojshme të veçohet përdorimi i të dhënave të përbashkëta në kohë. Në raste të tilla, përdoren mekanizma sinkronizimi që sigurojnë funksionimin e saktë të fijeve të shumta. Mjetet e sinkronizimit në OSDritaret: 1) seksion kritik (KritikeSeksioni) është një objekt që i përket procesit, jo kernelit. Kjo do të thotë se nuk mund të sinkronizojë thread-et nga procese të ndryshme. Ekzistojnë gjithashtu funksione për inicializimin (krijimin) dhe fshirjen, hyrjen dhe daljen nga një seksion kritik: krijimi - InitializeCriticalSection(...), fshirje - DeleteCriticalSection(...), hyrje - EnterCriticalSection(...), dalje - LeaveCriticalSection (...). Kufizimet: meqenëse nuk është një objekt kernel, ai nuk është i dukshëm për proceset e tjera, d.m.th. ju mund të mbroni vetëm temat e procesit tuaj. Seksioni kritik analizon vlerën e një ndryshoreje të veçantë të procesit që përdoret si një flamur për të parandaluar që thread-et e shumta të ekzekutojnë një pjesë të kodit në të njëjtën kohë. Ndër objektet sinkronizuese, seksionet kritike janë më të thjeshtat. 2) mutexi ndryshueshëmpërjashtojnë. Ky është një objekt kernel, ai ka një emër, që do të thotë se ato mund të përdoren për të sinkronizuar aksesin në të dhënat e përbashkëta nga disa procese, më saktësisht, nga fijet e proceseve të ndryshme. Asnjë thread tjetër nuk mund të marrë një mutex që tashmë është në pronësi të një prej thread-eve. Nëse një mutex mbron disa të dhëna të përbashkëta, ai do të jetë në gjendje të kryejë funksionin e tij vetëm nëse çdo thread kontrollon gjendjen e këtij mutex përpara se t'i qaset këtyre të dhënave. Windows trajton një mutex si një objekt të përbashkët që mund të sinjalizohet ose rivendoset. Gjendja e sinjalizuar e mutex tregon se është i zënë. Threads duhet të analizojnë në mënyrë të pavarur gjendjen aktuale të mutexes. Nëse dëshironi që mutex të aksesohet nga thread nga proceset e tjera, duhet t'i jepni një emër. Funksionet: CreateMutex(emri) - krijimi, hnd=OpenMutex(emri) - hapje, WaitForSingleObject(hnd) - pritje dhe zënë, ReleaseMutex(hnd) - lëshim, CloseHandle(hnd) - mbyllje. Mund të përdoret për të mbrojtur kundër rinisjes së programeve. 3) semafor -semafor. Objekti i kernelit "semafor" përdoret për llogaritjen e burimeve dhe shërben për të kufizuar aksesin e njëkohshëm në një burim nga disa thread. Duke përdorur një semafor, ju mund të organizoni punën e programit në atë mënyrë që disa fije të mund të hyjnë në burim në të njëjtën kohë, por numri i këtyre temave do të jetë i kufizuar. Kur krijoni një semafor, specifikohet numri maksimal i fijeve që mund të punojnë njëkohësisht me burimin. Sa herë që një program i qaset një semafori, numëruesi i burimeve të semaforit zvogëlohet me një. Kur vlera e numëruesit të burimit bëhet zero, semafori nuk është i disponueshëm. krijoni CreateSemaphore, hapni OpenSemaphore, merrni WaitForSingleObject, lëshoni ReleaseSemaphore 4 ) ngjarje -ngjarje. Ngjarjet zakonisht thjesht njoftojnë për përfundimin e disa operacioneve, ato janë gjithashtu objekte kernel. Ju jo vetëm që mund të lëshoni në mënyrë eksplicite, por ekziston edhe një operacion për vendosjen e ngjarjeve. Ngjarjet mund të jenë manuale (manuale) dhe të vetme (të vetme). Një ngjarje e vetme është më shumë një flamur i përgjithshëm. Një ngjarje është në gjendje të sinjalizuar nëse është caktuar nga ndonjë thread. Nëse programi kërkon që vetëm një nga thread-at të reagojë ndaj tij në rast të një ngjarjeje, ndërsa të gjitha thread-et e tjera vazhdojnë të presin, atëherë përdoret një ngjarje e vetme. Një ngjarje manuale nuk është thjesht një flamur i zakonshëm nëpër tema të shumta. Kryen funksione disi më komplekse. Çdo lidhje mund ta caktojë këtë ngjarje ose ta rivendosë (pastrojë) atë. Pasi të vendoset një ngjarje, ajo do të mbetet në këtë gjendje për një kohë të gjatë në mënyrë arbitrare, pavarësisht nga numri i temave që presin që ngjarja të vendoset. Kur të gjitha temat që presin këtë ngjarje marrin një mesazh se ngjarja ka ndodhur, ajo do të rivendoset automatikisht. Funksionet: SetEvent, ClearEvent, WaitForEvent. Llojet e ngjarjeve: 1) ngjarja e rivendosjes automatike: WaitForSingleEvent. 2) një ngjarje me një rivendosje manuale (manuale), atëherë ngjarja duhet të rivendoset: ReleaseEvent. Disa teoricienë veçojnë një objekt tjetër sinkronizimi: WaitAbleTimer është një objekt kernel OS që kalon në mënyrë të pavarur në një gjendje të lirë pas një intervali kohor të caktuar (ora me zile).

Ndonjëherë kur punoni me fije ose procese të shumta, bëhet e nevojshme sinkronizoni ekzekutimin dy ose më shumë prej tyre. Arsyeja për këtë është më shpesh se dy ose më shumë thread mund të kërkojnë qasje në një burim të përbashkët që vërtetë nuk mund të ofrohet në disa fije në të njëjtën kohë. Një burim i përbashkët është një burim që mund të arrihet nga disa detyra në të njëjtën kohë.

Mekanizmi që siguron procesin e sinkronizimit quhet kufizimi i aksesit. Nevoja për të lind edhe në rastet kur një thread pret një ngjarje të krijuar nga një thread tjetër. Natyrisht, duhet të ketë një mënyrë me të cilën filli i parë do të pezullohet derisa të ndodhë ngjarja. Pas kësaj, filli duhet të vazhdojë ekzekutimin e tij.

Ekzistojnë dy gjendje të përgjithshme në të cilat mund të jetë një detyrë. Së pari, detyra mund të kryhet(ose të jetë gati për të ekzekutuar sapo të ketë akses në burimet e procesorit). Së dyti, detyra mund të jetë bllokuar. Në këtë rast, ekzekutimi i tij pezullohet derisa burimi që i nevojitet të lëshohet ose të ndodhë një ngjarje e caktuar.

Windows ka shërbime speciale që ju lejojnë të kufizoni aksesin në burimet e përbashkëta në një mënyrë të caktuar, sepse pa ndihmën e sistemit operativ, një proces ose fije e veçantë nuk mund të përcaktojë vetë nëse ka qasje të vetme në një burim. Sistemi operativ Windows përmban një procedurë që, në një operacion të vazhdueshëm, kontrollon dhe, nëse është e mundur, vendos flamurin e aksesit në burim. Në gjuhën e zhvilluesve të sistemit operativ, një operacion i tillë quhet kontrolloni dhe instaloni funksionimin. Flamujt e përdorur për të siguruar sinkronizimin dhe kontrollin e aksesit në burime quhen semaforë(semafor). API Win32 ofron mbështetje për semaforët dhe objektet e tjera të sinkronizimit. Biblioteka MFC gjithashtu përfshin mbështetje për këto objekte.

Objektet e sinkronizimit dhe klasat mfc

Ndërfaqja Win32 mbështet katër lloje të objekteve të sinkronizimit, të gjitha të bazuara në një mënyrë ose në një tjetër në konceptin e një semafori.

Lloji i parë i objektit është vetë semafori, ose semafor klasik (standard).. Ai lejon një numër të kufizuar procesesh dhe thread për të hyrë në një burim të vetëm. Në këtë rast, qasja në burim është ose plotësisht e kufizuar (një dhe vetëm një fill ose proces mund të hyjë në burim në një periudhë të caktuar kohore), ose vetëm një numër i vogël thread-sh dhe procesesh marrin akses të njëkohshëm. Semaforët zbatohen me një numërues që zvogëlohet kur një detyrë i ndahet një semafor dhe rritet kur një detyrë lëshon semaforin.

Lloji i dytë i objekteve të sinkronizimit është semafor ekskluziv (mutex).. Ai është krijuar për të kufizuar plotësisht aksesin në një burim, në mënyrë që vetëm një proces ose fije të mund të hyjë në burim në çdo kohë të caktuar. Në fakt, ky është një lloj i veçantë semafori.

Lloji i tretë i objekteve të sinkronizimit është ngjarje, ose objekt ngjarjeje. Përdoret për të bllokuar aksesin në një burim derisa një proces ose fill tjetër të deklarojë se burimi mund të përdoret. Kështu, ky objekt sinjalizon ekzekutimin e ngjarjes së kërkuar.

Duke përdorur objektin e sinkronizimit të llojit të katërt, është e mundur të ndalohet ekzekutimi i disa seksioneve të kodit të programit nga disa fije njëkohësisht. Për ta bërë këtë, këto parcela duhet të deklarohen si seksion kritik. Kur një thread hyn në këtë seksion, temat e tjera ndalohen të bëjnë të njëjtën gjë derisa filli i parë të dalë nga ky seksion.

Seksionet kritike, ndryshe nga llojet e tjera të objekteve të sinkronizimit, përdoren vetëm për sinkronizimin e fijeve brenda një procesi të vetëm. Lloje të tjera objektesh mund të përdoren për të sinkronizuar thread-et brenda një procesi ose për të sinkronizuar proceset.

Në MFC, mekanizmi i sinkronizimit i ofruar nga ndërfaqja Win32 mbështetet përmes klasave të mëposhtme që rrjedhin nga klasa CSyncObject:

    Seksioni kritik- zbaton një seksion kritik.

    CEvent- zbaton objektin e ngjarjes

    CMutex- zbaton një semafor ekskluziv.

    CSemaphore- zbaton një semafor klasik.

Përveç këtyre klasave, MFC përcakton gjithashtu dy klasa sinkronizimi ndihmëse: CSingleLock dhe CMultiLock. Ata kontrollojnë aksesin në objektin e sinkronizimit dhe përmbajnë metodat e përdorura për të dhënë dhe lëshuar objekte të tilla. Klasa CSingleLock kontrollon aksesin në një objekt të vetëm sinkronizimi dhe klasën CMultiLock- për disa objekte. Në vijim, ne do të shqyrtojmë vetëm klasën CSingleLock.

Kur krijohet ndonjë objekt sinkronizimi, qasja në të mund të kontrollohet duke përdorur klasën CSingleLock. Për ta bërë këtë, së pari duhet të krijoni një objekt të llojit CSingleLock duke përdorur konstruktorin:

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

Parametri i parë është një tregues për një objekt sinkronizimi, siç është një semafor. Vlera e parametrit të dytë përcakton nëse konstruktori duhet të përpiqet të aksesojë objektin e dhënë. Nëse ky parametër është jo zero, atëherë do të jepet qasja, përndryshe nuk do të tentohet të ketë akses. Nëse jepet qasja, atëherë filli që krijoi objektin e klasës CSingleLock, do të ndalet derisa objekti përkatës i sinkronizimit të çlirohet nga metoda Hap klasës CSingleLock.

Pasi të jetë krijuar një objekt i tipit CSingleLock, qasja në objektin e treguar nga parametri pObject mund të kontrollohet duke përdorur dy funksione: bllokoj dhe Hap klasës CSingleLock.

Metoda bllokojështë projektuar për të hyrë në objekt në objektin e sinkronizimit. Fillimi që e thirri atë pezullohet derisa të përfundojë metoda, domethënë derisa të aksesohet burimi. Vlera e parametrit përcakton se sa kohë do të presë funksioni për të hyrë në objektin e kërkuar. Sa herë që metoda përfundon me sukses, vlera e numëruesit të lidhur me objektin e sinkronizimit zvogëlohet me një.

Metoda Hap lëshon objektin e sinkronizimit, duke lejuar thread-ët e tjerë të përdorin burimin. Në variantin e parë të metodës, vlera e numëruesit të lidhur me objektin e dhënë rritet me një. Në opsionin e dytë, parametri i parë përcakton se sa duhet të rritet kjo vlerë. Parametri i dytë tregon një ndryshore në të cilën do të shkruhet vlera e mëparshme e numëruesit.

Kur punoni me një klasë CSingleLock Procedura e përgjithshme për kontrollin e aksesit në një burim është si më poshtë:

    krijoni një objekt të tipit CSyncObj (për shembull, një semafor) që do të përdoret për të kontrolluar aksesin në burim;

    duke përdorur objektin e krijuar të sinkronizimit, krijoni një objekt të tipit CSingleLock;

    thirrni metodën Lock për të fituar akses në burim;

    bëni një telefonatë në burim;

    thirrni metodën e Zhbllokimit për të lëshuar burimin.

Më poshtë përshkruan se si të krijoni dhe përdorni semaforë dhe objekte ngjarjesh. Pasi t'i kuptoni këto koncepte, mund të mësoni dhe përdorni lehtësisht dy llojet e tjera të objekteve të sinkronizimit: seksionet kritike dhe mutexes.