Gijos būsenos. Kritinės dalys Sinchronizavimo pabaiga Windows OS

Šis sinchronizavimo objektas gali būti naudojamas tik vietoje jį sukūrusiame procese. Likę objektai gali būti naudojami sinchronizuoti skirtingų procesų gijas. Objekto pavadinimas „kritinė sekcija“ siejama su kažkokiu abstrakčiu programos kodo dalies (skilties) pasirinkimu, atliekančiu kai kurias operacijas, kurių eiliškumo pažeisti negalima. Tai reiškia, kad dviejų skirtingų gijų bandymas vienu metu vykdyti šio skyriaus kodą sukels klaidą.

Pavyzdžiui, gali būti patogu apsaugoti rašymo funkcijas naudojant tokią skiltį, nes kelių rašytojų prieiga vienu metu turėtų būti pašalinta.

Kritiniam skyriui pateikiamos dvi operacijos:

įeikite į skyrių Kol bet kuri gija yra kritinėje dalyje, visos kitos gijos automatiškai nustos laukti, kai bandys ją įvesti. Gija, kuri jau pateko į šį skyrių, gali ją įvesti kelis kartus nelaukdama, kol ji bus išlaisvinta.

palikti skyrių Kai gija palieka atkarpą, šios gijos įvedimų į sekciją skaitiklis sumažinamas, todėl sekcija bus atlaisvinta kitoms gijomis tik tuo atveju, jei gija išeis iš sekcijos tiek kartų, kiek į ją įėjo. Išleidus kritinę sekciją, bus pažadinta tik viena gija, laukianti leidimo įeiti į šią sekciją.

Paprastai kalbant, kitose ne Win32 API (pvz., OS/2) kritinė sekcija traktuojama ne kaip sinchronizacijos objektas, o kaip programos kodo dalis, kurią gali vykdyti tik viena programos gija. Tai yra, įėjimas į kritinę sekciją laikomas laikinu sriegio perjungimo mechanizmo išjungimu iki išėjimo iš šios sekcijos. „Win32“ API laiko svarbias dalis kaip objektus, o tai sukelia tam tikrą painiavą – savo savybėmis jie labai artimi neįvardytam išskirtiniam objektui ( mutex, žr. žemiau).

Naudojant kritines sekcijas, reikia pasirūpinti, kad skyriuje nebūtų per dideli kodo fragmentai, nes dėl to gali gerokai vėluoti kitų gijų vykdymas.

Pavyzdžiui, jau apsvarstytų krūvų atžvilgiu nėra prasmės saugoti visų krūvos funkcijų su kritine sekcija, nes skaitytuvo funkcijas galima vykdyti lygiagrečiai. Be to, kritinės dalies naudojimas net rašytojų sinchronizavimui iš tikrųjų atrodo nepatogus – kadangi norint sinchronizuoti rašytoją su skaitytojais, pastarieji vis tiek turės įeiti į šią sekciją, o tai praktiškai lemia visų funkcijų apsaugą vienu skyrius.

Yra keli veiksmingo kritinių skyrių naudojimo atvejai:

skaitytojai nekonfliktuoja su rašytojais (saugoti reikia tik rašytojus);

visos gijos turi maždaug vienodas prieigos teises (tarkim, negalima išskirti grynų rašytojų ir skaitytojų);

konstruojant sudėtinius sinchronizuojančius objektus, susidedančius iš kelių standartinių, siekiant apsaugoti nuoseklias operacijas su sudėtiniu objektu.

Ankstesnėse straipsnio dalyse aš kalbėjau apie Bendri principai ir specifiniai kelių sriegių taikomųjų programų kūrimo metodai. Skirtingoms gijomis beveik visada periodiškai reikia sąveikauti viena su kita, todėl sinchronizavimo poreikis neišvengiamai iškyla. Šiandien apžvelgsime svarbiausią, galingiausią ir universaliausią „Windows“ sinchronizavimo įrankį: branduolio sinchronizavimo objektus.

WaitForMultipleObjects ir kitos laukimo funkcijos

Kaip prisimenate, norint sinchronizuoti gijas, paprastai reikia laikinai sustabdyti vienos iš gijų vykdymą. Tačiau jis turi būti išverstas priemonėmis Operacinė sistemaį laukimo būseną, kai neužima CPU laiko. Jau žinome dvi funkcijas, kurios gali tai padaryti: SuspendThread ir ResumeThread . Tačiau, kaip sakiau ankstesnėje straipsnio dalyje, dėl kai kurių savybių šios funkcijos netinka sinchronizuoti.

Šiandien pažvelgsime į kitą funkciją, kuri taip pat perkelia giją į laukimo būseną, tačiau skirtingai nei SuspendThread/ResumeThread , ji yra specialiai sukurta sinchronizavimui organizuoti. Tai WaitForMultipleObjects. Kadangi ši funkcija yra labai svarbi, aš šiek tiek nukrypsiu nuo taisyklės nesigilinti į API detales ir pakalbėsiu apie tai išsamiau, net pateiksiu jos prototipą:

DWORD WaitForMultipleObjects (

DWORD nCount , // objektų skaičius lpHandles masyve

CONST RANKENA * lp Rankenėlės , // žymeklis į branduolio objektų deskriptorių masyvą

BOOL bPalaukViskas , // vėliavėlė, nurodanti, ar laukti visų objektų ar užtenka vieno

DWORD dwmilisekundės // laikas baigėsi

Pagrindinis šios funkcijos parametras yra žymeklis į branduolio objektų rankenėlių masyvą. Apie tai, kas yra šie objektai, kalbėsime žemiau. Kol kas mums svarbu žinoti, kad bet kuris iš šių objektų gali būti vienoje iš dviejų būsenų: neutralus arba „signalizuojantis“ (signalizuota būsena). Jei bWaitAll vėliavėlė yra FALSE, funkcija grįš, kai tik bent vienas iš objektų duos signalą. O jei vėliavėlė TRUE, tai įvyks tik tada, kai visi objektai pradės signalizuoti vienu metu (kaip matysime, tai yra svarbiausia šios funkcijos savybė). Pirmuoju atveju pagal grąžintą reikšmę galite sužinoti, kuris iš objektų davė signalą. Iš jo reikia atimti WAIT_OBJECT_0 konstantą ir gausite indeksą lpHandles masyve. Jei skirtasis laikas viršija paskutiniame parametre nurodytą skirtąjį laiką, funkcija nustos laukti ir grąžins reikšmę WAIT_TIMEOUT . Kaip skirtąjį laiką galite nurodyti konstantą INFINITE , tada funkcija lauks "kol sustos", arba galite atvirkščiai 0, tada gija iš viso nebus sustabdyta. Pastaruoju atveju funkcija iškart grįš, tačiau jos rezultatas parodys objektų būseną. Pastaroji technika naudojama labai dažnai. Kaip matote, ši funkcija turi daug galimybių. Yra keletas kitų „WaitForXXX“ funkcijų, tačiau visos jos yra pagrindinės temos variantai. Visų pirma, „WaitForSingleObject“ yra tik supaprastinta jo versija. Likusieji turi savo papildomas funkcijas, tačiau paprastai naudojami rečiau. Pavyzdžiui, jie leidžia reaguoti ne tik į signalus iš branduolio objektų, bet ir į naujų lango pranešimų atėjimą į gijos eilę. Jų aprašymą ir išsamią informaciją apie WaitForMultipleObjects, kaip įprasta, rasite MSDN.

Dabar apie tai, kas yra šie paslaptingi „branduolių objektai“. Pirmiausia tai apima pačias gijas ir procesus. Baigę jie iškart pereina į signalizacijos būseną. Tai labai svarbi savybė, nes dažnai reikia sekti, kada gijos ar proceso pabaiga. Tegul, pavyzdžiui, mūsų serverio programa su darbuotojo gijų rinkiniu turėtų būti baigta. Tuo pačiu metu valdymo gija turi tam tikru būdu informuoti darbuotojo gijas, kad laikas baigti darbą (pavyzdžiui, nustatant visuotinę vėliavėlę), o tada palaukti, kol visos gijos bus baigtos, darydamos viską, kas reikalinga teisingam užbaigimui. veiksmo: išteklių atlaisvinimas, klientų informavimas apie išjungimą, tinklo jungčių uždarymas ir kt.

Tai, kad gijos įjungia signalą darbo pabaigoje, labai lengva išspręsti sinchronizavimo problemą su gijos pabaiga:

// Paprastumo dėlei turėkime vieną darbininko giją. Paleiskite tai:

TVARKITE „hWorkerThread“. = :: Sukurti giją (...);

// Prieš baigiant darbą, turime kažkaip pasakyti darbuotojo gijai, kad laikas įkelti.

// Palaukite, kol gija baigsis:

DWORD dwWaitResult = :: WaitForSingleObject ( hWorkerThread , BEGALINIS );

jeigu( dwWaitResult != WAIT_OBJECT_0 ) { /* klaidų tvarkymas */ }

// Srauto „rankeną“ galima uždaryti:

PATIKRINTI (:: CloseHandle ( hWorkerThread );

/* Jei „CloseHandle“ nepavyksta ir grąžina FALSE, aš nedarau išimties. Pirma, net jei taip atsitiktų dėl sistemos klaidos, tai neturėtų tiesioginių pasekmių mūsų programai, nes kadangi rankeną uždarome, tai ateityje su ja dirbti nesitikima. Tiesą sakant, „CloseHandle“ gedimas gali reikšti tik jūsų programos klaidą. Todėl čia įterpsime makrokomandą VERIFY, kad jos nepraleistume programos derinimo etape. */

Kodas, laukiantis proceso pabaigos, atrodys panašiai.

Jei tokios integruotos galimybės nebūtų, darbuotojo gija turėtų kažkaip perduoti informaciją apie jos užbaigimą pačiai pagrindinei gijai. Net jei tai būtų padaryta paskutinį kartą, pagrindinė gija negalėjo būti tikra, kad darbuotojui liko įvykdyti bent pora surinkėjo instrukcijų. AT individualios situacijos(pavyzdžiui, jei gijos kodas yra DLL, kuris turi būti iškrautas jam pasibaigus), tai gali būti mirtina.

Noriu jums priminti, kad net pasibaigus gijai (ar procesui), jos rankenos vis tiek galioja tol, kol jas aiškiai uždaro funkcija CloseHandle. (Beje, nepamirškite to padaryti!) Tai daroma tik tam, kad bet kuriuo metu galėtumėte patikrinti gijos būseną.

Taigi, funkcija WaitForMultipleObjects (ir jos analogai) leidžia sinchronizuoti gijos vykdymą su sinchronizavimo objektų, ypač kitų gijų ir procesų, būsena.

Specialūs branduolio objektai

Pereikime prie branduolio objektų, kurie yra specialiai sukurti sinchronizavimui, svarstymo. Tai įvykiai, semaforai ir nutildymai. Trumpai apžvelkime kiekvieną iš jų:

renginys

Galbūt paprasčiausias ir esminis sinchronizuojantis objektas. Tai tik vėliavėlė, kurią galima nustatyti naudojant SetEvent / ResetEvent funkcijas: signalizaciją arba neutralią. Įvykis yra patogiausias būdas signalizuoti laukiančiai gijai, kad įvyko koks nors įvykis (todėl jis vadinamas) ir galite tęsti darbą. Naudodami įvykį galime lengvai išspręsti sinchronizavimo problemą inicijuodami darbuotojo giją:

// Kad būtų paprasčiau, įvykio rankenėlę palikime visuotiniame kintamajame:

HANDLE g_hEventInitComplete = NULL ; // Niekada nepalikite kintamojo inicijuoto!

{ // kodas pagrindinėje gijoje

// sukurti įvykį

g_hEventInitComplete = :: Sukurti įvykį ( NULL,

NETEISINGA , // apie šį parametrą pakalbėsime vėliau

NETEISINGA , // pradinė būsena – neutrali

jeigu(! g_hEventInitComplete ) { /* Nepamirškite apie klaidų apdorojimą */ }

// sukurti darbuotojo giją

DWORD idWorkerThread = 0 ;

TVARKITE „hWorkerThread“. = :: Sukurti giją ( NULL , 0 , & WorkerThreadProc , NULL , 0 , & idWorkerThread );

jeigu(! hWorkerThread ) { /* klaidų tvarkymas */ }

// laukite signalo iš darbuotojo gijos

DWORD dwWaitResult = :: WaitForSingleObject ( g_hEventInitComplete , BEGALINIS );

jeigu( dwWaitResult != WAIT_OBJECT_0 ) { /* klaida */ }

// dabar galite būti tikri, kad darbuotojo gijos inicijavimas baigtas.

PATIKRINTI (:: CloseHandle ( g_hEventInitComplete )); // nepamirškite uždaryti nereikalingų objektų

g_hEventInitComplete = NULL ;

// darbo eigos funkcija

DWORD WINAPI WorkerThreadProc ( LPVOID_parametras )

InitializeWorker (); // inicijavimas

// signalizuoja, kad inicijavimas baigtas

BOOL yra gerai = :: SetEvent ( g_hEventInitComplete );

jeigu(! viskas gerai ) { /* klaida */ }

Reikėtų pažymėti, kad yra dvi labai skirtingos įvykių rūšys. Vieną iš jų galime pasirinkti naudodami antrąjį CreateEvent funkcijos parametrą. Jei TRUE, sukuriamas įvykis, kurio būsena valdoma tik rankiniu būdu, ty SetEvent/ResetEvent funkcijomis. Jei tai FALSE, bus sugeneruotas automatinio nustatymo iš naujo įvykis. Tai reiškia, kad kai tik tam tikra gija, laukianti tam tikro įvykio, bus paleista signalu iš šio įvykio, ji automatiškai bus grąžinta į neutralią būseną. Jų skirtumas ryškiausias situacijoje, kai vieno įvykio vienu metu laukia kelios gijos. Rankiniu būdu valdomas įvykis yra tarsi startinis pistoletas. Kai tik jis bus nustatytas į signalinę būseną, visos gijos bus paleistos iš karto. Kita vertus, automatinio atstatymo įvykis yra kaip metro turniketas: jis išleis tik vieną srautą ir grįš į neutralią būseną.

Mutex

Palyginti su renginiu, tai labiau specializuotas objektas. Paprastai jis naudojamas sprendžiant įprastas sinchronizavimo problemas, pvz., prieiti prie išteklių, kuriuos bendrina kelios gijos. Daugeliu atžvilgių tai panašu į automatinio nustatymo iš naujo įvykį. Pagrindinis skirtumas yra tas, kad jis turi specialų surišimą su konkrečiu siūlu. Jei mutex yra signalizuotoje būsenoje, tai reiškia, kad jis yra laisvas ir nepriklauso jokiai gijai. Kai tik tam tikra gija laukia šio mutex, pastaroji atstatoma į neutralią būseną (čia tai yra kaip automatinio atstatymo įvykis), o gija tampa jos savininku, kol ji aiškiai išleidžia mutex su ReleaseMutex funkcija, arba baigiasi. Taigi, norint įsitikinti, kad su bendrai naudojamais duomenimis vienu metu dirba tik viena gija, visos vietos, kur toks darbas vyksta, turėtų būti apsuptos pora: WaitFor - ReleaseMutex :

RANKE G_hMutex ;

// Tegul mutex rankena yra saugoma visuotiniame kintamajame. Žinoma, jis turi būti sukurtas iš anksto, prieš pradedant darbininkų gijas. Tarkime, kad tai jau buvo padaryta.

tarpt aš laukiu = :: WaitForSingleObject ( g_hMutex , BEGALINIS );

jungiklis( aš laukiu ) {

atveju WAIT_OBJECT_0 : // Viskas gerai

pertrauka;

atveju WAIT_ABANDONED : /* Kai kurios gijos baigėsi, pamiršus iškviesti ReleaseMutex. Greičiausiai tai reiškia jūsų programos klaidą! Todėl tik tuo atveju čia įterpsime ASSERT, bet galutinėje versijoje (leidime) šį kodą laikysime sėkmingu. */

Tvirtinti ( klaidinga );

pertrauka;

numatytas:

// Klaidų apdorojimas turėtų būti čia.

// Kodo fragmentas, apsaugotas mutex.

ProcessCommonData ();

PATIKRINTI (:: ReleaseMutex ( g_hMutex ));

Kodėl „mutex“ yra geriau nei automatinio atstatymo įvykis? Aukščiau pateiktame pavyzdyje jis taip pat gali būti naudojamas, tik ReleaseMutex turėtų būti pakeistas SetEvent . Tačiau gali kilti šių sunkumų. Dažniausiai tenka dirbti su bendrai naudojamais duomenimis keliose vietose. Kas atsitiks, jei ProcessCommonData mūsų pavyzdyje iškviečia funkciją, kuri veikia su tais pačiais duomenimis ir kuri jau turi savo WaitFor - ReleaseMutex porą (praktikoje tai labai įprasta)? Jei naudotume įvykį, programa akivaizdžiai pakibtų, nes saugomo bloko viduje įvykis yra neutralios būsenos. Muteksas yra sudėtingesnis. Jis visada išlieka pagrindinės gijos signalizacijos būsenoje, nors visoms kitoms gijomis yra neutralioje būsenoje. Todėl, jei gija įgavo mutex, funkcijos WaitFor iškvietimas nebus blokuojamas. Be to, skaitiklis taip pat yra įmontuotas į mutex, todėl ReleaseMutex turi būti iškviestas tiek pat kartų, kiek buvo iškviesta WaitFor . Taigi galime saugiai apsaugoti kiekvieną kodo dalį, kuri veikia su bendrai naudojamais duomenimis, su WaitFor – ReleaseMute x pora, nesijaudindami, kad šis kodas gali būti iškviestas rekursyviai. Dėl to mutex yra labai paprastas įrankis.

Semaforas

Dar konkretesnis sinchronizacijos objektas. Turiu prisipažinti, kad mano praktikoje dar nebuvo atvejo, kai tai būtų naudinga. Semaforas skirtas apriboti maksimalų gijų, kurios vienu metu gali dirbti su ištekliu, skaičių. Iš esmės semaforas yra įvykis su skaitikliu. Kol šis skaitiklis didesnis už nulį, semaforas yra signalizacijos būsenoje. Tačiau kiekvienas iškvietimas į WaitFor sumažina šį skaitiklį vienu, kol jis tampa nuliu, o semaforas pereina į neutralią būseną. Kaip ir mutex, semaforas turi ReleaseSemaphor funkciją, kuri padidina skaitiklį. Tačiau, skirtingai nei mutex, semaforas nėra susietas su gijomis, o dar kartą iškvietus WaitFor/ReleaseSemaphor skaitiklis sumažės / padidins.

Kaip galima naudoti semaforą? Pavyzdžiui, jis gali būti naudojamas dirbtinai apriboti kelių sriegių naudojimą. Kaip jau minėjau, per daug vienu metu aktyvių gijų gali pastebimai pabloginti visos sistemos veikimą dėl dažno konteksto perjungimo. Ir jei turėtume sukurti per daug darbininkų gijų, galime apriboti vienu metu aktyvių gijų skaičių iki procesorių skaičiaus eilės.

Ką dar galima pasakyti apie branduolio sinchronizavimo objektus? Labai patogu duoti jiems vardus. Visos sinchronizavimo objektus kuriančios funkcijos turi atitinkamą parametrą: CreateEvent , CreateMutex , CreateSemaphore . Jei iškviečiate, pavyzdžiui, CreateEvent du kartus, abu kartus nurodydami tą patį netuščią pavadinimą, tada antrą kartą funkcija, užuot sukūrusi naują objektą, grąžins esamo rankenėlę. Taip atsitiks net jei antrasis skambutis buvo atliktas iš kito proceso. Pastarasis yra labai patogus tais atvejais, kai norite sinchronizuoti gijas, priklausančias skirtingiems procesams.

Kai jums nebereikia sinchronizavimo objekto, nepamirškite iškviesti funkcijos CloseHandle, kurią minėjau anksčiau, kai kalbėjau apie gijas. Tiesą sakant, jis nebūtinai ištrins objektą iš karto. Esmė ta, kad objektas gali turėti kelias rankenas, o tada jis bus ištrintas tik uždarius paskutinę.

Noriu jums tai priminti Geriausias būdas norint užtikrinti, kad CloseHandle ar panaši „valymo“ funkcija tikrai būtų iškviesta net ir susiklosčius neįprastai situacijai, reiškia įdėti ją į naikintuvą. Beje, tai kažkada buvo gerai ir labai išsamiai aprašyta Kirilo Plešivcevo straipsnyje „Išmanusis naikintojas“. Aukščiau pateiktuose pavyzdžiuose šios technikos nenaudojau vien tik švietimo tikslais, kad API funkcijų darbas būtų vizualesnis. Realiame kode valymui visada turėtumėte naudoti įvyniojimo klases su išmaniaisiais naikintuvais.

Beje, su ReleaseMutex funkcija ir panašiai nuolat iškyla ta pati problema kaip ir su CloseHandle . Jis turi būti iškviestas darbo pabaigoje su bendrai naudojamais duomenimis, nepriklausomai nuo to, kaip sėkmingai šis darbas buvo atliktas (juk galima išmesti išimtį). „Užmaršumo“ pasekmės čia rimtesnės. Jei jis nebus vadinamas CloseHandle, nutekės tik ištekliai (o tai taip pat yra blogai!), tada neišleistas mutex neleis kitoms gijomis dirbti su bendru ištekliu iki nepavykusios gijos nutraukimo, o tai greičiausiai neleis programai normaliai veikti. Norėdami to išvengti, mums vėl padės specialiai apmokyta klasė su išmaniuoju destruktoriumi.

Baigdamas sinchronizavimo objektų apžvalgą, norėčiau paminėti objektą, kurio nėra Win32 API. Daugelis mano kolegų stebisi, kodėl „Win32“ neturi specializuoto objekto „vienas rašo, daug skaito“. Tam tikras „išplėstinis mutex“, užtikrinantis, kad tik viena gija gali vienu metu pasiekti bendrinamus duomenis rašymui, o kelios gijos gali skaityti tik vienu metu. Panašų objektą galima rasti ir UNIX "ah. Kai kurios bibliotekos, pavyzdžiui iš Borlando, siūlo jį emuliuoti pagal standartinius sinchronizavimo objektus. Tačiau reali tokių emuliacijų nauda labai abejotina. Tokį objektą galima efektyviai įgyvendinti tik operacinės sistemos branduolio lygį.Tačiau Windows branduolyje tokio objekto nenumatyta.

Kodėl Windows NT branduolio kūrėjai tuo nepasirūpino? Kodėl mes blogesni už UNIX? Mano nuomone, atsakymas yra toks, kad tiesiog dar nebuvo realaus poreikio tokiam objektui Windows. Įprastame vieno procesoriaus įrenginyje, kuriame gijos vis dar negali fiziškai veikti vienu metu, tai praktiškai prilygs mutex. Daugiaprocesoriniame įrenginyje tai gali būti naudinga, nes leidžia skaitytuvo gijomis veikti lygiagrečiai. Tuo pačiu metu šis padidėjimas taps apčiuopiamas tik tada, kai skaitymo gijų „susidūrimo“ tikimybė yra didelė. Be jokios abejonės, pavyzdžiui, 1024 procesoriaus įrenginyje toks branduolio objektas bus gyvybiškai svarbus. Yra panašių mašinų, tačiau tai yra specializuotos sistemos, kuriose veikia specializuotos operacinės sistemos. Dažnai tokios operacinės sistemos yra kuriamos UNIX pagrindu, tikriausiai iš ten toks objektas kaip „rašo vienas, daug skaito“ pateko į dažniau naudojamas šios sistemos versijas. Tačiau x86 įrenginiuose, kaip taisyklė, yra įdiegtas tik vienas ir tik kartais du procesoriai. Ir tik pažangiausi procesorių modeliai, tokie kaip Intel Xeon, palaiko 4 ar net daugiau procesorių konfigūracijų, tačiau tokios sistemos vis tiek išlieka egzotika. Tačiau net ir tokioje „pažangioje“ sistemoje „pažangus mutex“ gali duoti pastebimą našumo padidėjimą tik labai konkrečiose situacijose.

Taigi, diegti „pažangų“ mutex tiesiog neverta vargti. Mažo procesoriaus įrenginyje jis gali būti net mažiau efektyvus dėl objekto logikos sudėtingumo, palyginti su standartiniu mutex. Atkreipkite dėmesį, kad tokio objekto įgyvendinimas nėra toks paprastas, kaip gali pasirodyti iš pirmo žvilgsnio. Nesėkmingai įgyvendinus, jei skaitymo gijų yra per daug, rašymo gija tiesiog „nepateks“ į duomenis. Dėl šių priežasčių taip pat nerekomenduoju bandyti mėgdžioti tokio objekto. Realiose programose realiose mašinose įprastas „mutex“ arba kritinis skyrius (kuris bus aptartas kitoje straipsnio dalyje) puikiai susidoros su užduotimi sinchronizuoti prieigą prie bendrinamų duomenų. Nors, manau, tobulėjant „Windows“ OS, anksčiau ar vėliau atsiras branduolio objektas „rašo vienas, daug skaito“.

Pastaba. Tiesą sakant, „Windows NT“ objektas „rašo vienas – daugelis skaito“ vis dar egzistuoja. Rašydamas šį straipsnį aš tiesiog apie tai nežinojau. Šis objektas vadinamas „branduolio ištekliais“ ir nėra pasiekiamas vartotojo režimo programoms, todėl tikriausiai dėl to jis nėra gerai žinomas. Panašumų apie tai galima rasti DDK. Ačiū Konstantinui Manurinui, kad atkreipė dėmesį į tai.

Aklavietė

Dabar grįžkime prie funkcijos WaitForMultipleObjects, tiksliau, prie jos trečiojo parametro bWaitAll. Pažadėjau papasakoti, kodėl taip svarbu galimybė laukti kelių objektų vienu metu.

Suprantama, kodėl reikia funkcijos laukti vieno iš kelių objektų. Jei nėra specialios funkcijos, tai būtų galima padaryti, nebent nuosekliai tikrinant objektų būseną tuščioje kilpoje, o tai, žinoma, yra nepriimtina. Tačiau specialios funkcijos, leidžiančios laukti momento, kai keli objektai vienu metu pereis į signalo būseną, poreikis nėra toks akivaizdus. Iš tiesų, įsivaizduokite tokią tipišką situaciją: tam tikru momentu mūsų gijai reikia vienu metu prieiti prie dviejų bendrinamų duomenų rinkinių, kurių kiekvienas yra atsakingas už savo mutex, pavadinkime juos A ir B. Atrodytų, kad gija gali pirmiausia palaukite, kol bus paleistas mutex A, užfiksuokite jį , tada palaukite, kol bus paleistas mutex B... Atrodo, kad galime padaryti keletą iškvietimų į WaitForSingleObject . Iš tiesų, tai veiks, bet tik tol, kol visos kitos gijos įgis mutexus ta pačia tvarka: pirmiausia A, tada B. Kas atsitiks, jei tam tikra gija bandys daryti priešingai: pirmiausia įgyja B, tada A? Anksčiau ar vėliau susiklostys situacija, kai viena gija užfiksavo mutex A, kita B, pirmoji laukia, kol bus paleista B, antra A. Aišku, kad šito jie niekada nelauks ir programa pakibs.

Toks aklavietės tipas yra labai dažna klaida. Kaip ir visos su sinchronizavimu susijusios klaidos, ji pasirodo tik retkarčiais ir gali sugadinti daug nervų programuotojui. Tuo pačiu metu beveik bet kuri schema, apimanti kelis sinchronizavimo objektus, yra aklavietėje. Todėl tokios grandinės projektavimo etape šiai problemai reikia skirti ypatingą dėmesį.

Pateiktame paprastame pavyzdyje blokavimo gana lengva išvengti. Reikia reikalauti, kad visos gijos įgytų mutexus tam tikra tvarka: pirmiausia A, paskui B. Tačiau sudėtingoje programoje, kur yra daug objektų, įvairiais būdais tarpusavyje susijusių, dažniausiai tai nėra taip paprasta pasiekti. Spynoje gali būti ne du, o daug objektų ir gijų. Todėl labiausiai patikimu būdu Norint išvengti aklavietės situacijoje, kai gijai vienu metu reikia kelių sinchronizavimo objektų, juos visus reikia užfiksuoti vienu iškvietimu į funkciją WaitForMultipleObjects su parametru bWaitAll=TRUE. Tiesą sakant, šiuo atveju mes tiesiog perkeliame aklavietės problemą į operacinės sistemos branduolį, tačiau svarbiausia, kad tai nebebus mūsų rūpestis. Tačiau sudėtingoje programoje su daugybe objektų, kai ne visada galima iš karto pasakyti, kurio iš jų reikės norint atlikti konkrečią operaciją, dažnai nėra lengva visus WaitFor skambučius sujungti į vieną vietą ir juos sujungti.

Taigi yra du būdai, kaip išvengti aklavietės. Turite užtikrinti, kad sinchronizavimo objektai visada būtų užfiksuoti gijomis tiksliai ta pačia tvarka, arba kad jie būtų užfiksuoti vienu iškvietimu į WaitForMultipleObjects . Pastarasis metodas yra paprastesnis ir pageidaujamas. Tačiau praktikoje, vykdant abu reikalavimus, nuolat kyla sunkumų, būtina derinti abu šiuos metodus. Sudėtingų laiko grandinių projektavimas dažnai yra labai nebanali užduotis.

Sinchronizavimo pavyzdys

Daugumoje tipiškų situacijų, pvz., aprašytų aukščiau, sinchronizaciją organizuoti nėra sunku, užtenka įvykio ar mutex. Tačiau periodiškai pasitaiko sudėtingesnių atvejų, kai problemos sprendimas nėra toks akivaizdus. Norėčiau tai iliustruoti konkrečiu pavyzdžiu iš savo praktikos. Kaip matysite, sprendimas pasirodė stebėtinai paprastas, tačiau kol jį radau, teko išbandyti keletą nesėkmingų variantų.

Taigi užduotis. Beveik visos šiuolaikinės atsisiuntimų tvarkyklės arba tiesiog „supamosios kėdės“ turi galimybę apriboti srautą, kad fone veikianti „supama kėdė“ labai netrukdytų vartotojui naršyti internete. Aš kūriau panašią programą ir gavau užduotį įdiegti būtent tokią „funkciją“. Mano supamoji kėdė veikė pagal klasikinę multithreading schemą, kai kiekviena užduotis, šiuo atveju, konkretaus failo atsisiuntimas, yra tvarkoma atskira gija. Eismo limitas turėjo būti bendras visiems srautams. Tai yra, reikėjo užtikrinti, kad per tam tikrą laiko intervalą visi srautai iš savo lizdų nuskaitytų ne daugiau kaip tam tikrą baitų skaičių. Tiesiog padalyti šią ribą po lygiai srautams akivaizdžiai bus neefektyvu, nes failų atsisiuntimas gali būti labai netolygus, vienas atsisiunčiamas greitai, kitas lėtai. Todėl mums reikia bendro visų gijų skaitiklio, kiek baitų buvo perskaityta ir kiek dar galima nuskaityti. Čia sinchronizavimas praverčia. Papildomą užduoties sudėtingumą suteikė reikalavimas, kad bet kuriuo metu būtų galima sustabdyti bet kurią darbininko giją.

Suformuluosime problemą išsamiau. Sinchronizacijos sistemą nusprendžiau įtraukti į specialią klasę. Čia yra jo sąsaja:

klasė CQuota {

viešas: // metodai

tuštuma rinkinys ( nepasirašytas tarpt _nKvota );

nepasirašytas tarpt Prašymas ( nepasirašytas tarpt _nBytesToRead , HANDLE_hStopEvent );

tuštuma Paleisti ( nepasirašytas tarpt _nBytesRevert , HANDLE_hStopEvent );

Periodiškai, tarkime, kartą per sekundę, valdymo gija iškviečia metodą Set, nustatydama atsisiuntimo kvotą. Prieš tai, kai darbuotojo gija nuskaito iš tinklo gautus duomenis, ji iškviečia užklausos metodą, kuris patikrina, ar dabartinė kvota nėra nulis, o jei taip, grąžina baitų, kuriuos galima nuskaityti, skaičių, mažesnį nei dabartinė kvota. Šiuo skaičiumi atitinkamai sumažinama kvota. Jei kvota lygi nuliui, kai iškviečiama užklausa, skambinanti gija turi palaukti, kol kvota bus laisva. Kartais nutinka taip, kad realiai gaunama mažiau baitų nei prašoma, tokiu atveju gija grąžina dalį jai Release metodu skirtos kvotos. Ir, kaip sakiau, vartotojas gali bet kada duoti komandą sustabdyti atsisiuntimą. Tokiu atveju laukimas turi būti nutrauktas, neatsižvelgiant į tai, ar yra kvota. Tam naudojamas specialus įvykis: _hStopEvent. Kadangi užduotis galima pradėti ir sustabdyti nepriklausomai, kiekviena darbuotojo gija turi savo sustabdymo įvykį. Jo rankena perduodama užklausos ir atleidimo metodams.

Viename iš nesėkmingų variantų bandžiau naudoti mutex, kuris sinchronizuoja prieigą prie CQuota klasės, ir įvykio, kuris signalizuoja apie kvotos egzistavimą, derinį. Tačiau sustabdymo įvykis netelpa į šią schemą. Jei gija nori gauti kvotą, tada jos laukimo būsena turi būti valdoma sudėtinga logine išraiška: ((mutex AND quota event) ARBA sustabdymo įvykis). Bet WaitForMultipleObjects to neleidžia, galite sujungti kelis branduolio objektus su AND arba OR operacija, bet ne maišyti. Bandymas padalyti laukimą dviem iš eilės skambučiais į WaitForMultipleObjects neišvengiamai atsiduria aklavietėje. Apskritai šis kelias pasirodė esąs aklavietė.

Nebeleisiu į rūką ir nepasakysiu sprendimo. Kaip sakiau, mutex yra labai panašus į automatinio atstatymo įvykį. Ir čia turime tik tą retą atvejį, kai patogiau jį naudoti, bet ne vieną, o du iš karto:

klasė CQuota {

privatus: // duomenys

nepasirašytas tarpt m_nKvota ;

CEvent m_eventHasQuota ;

CEvent m_eventNoQuota ;

Vienu metu galima nustatyti tik vieną iš šių įvykių. Bet kuri kvota manipuliuojanti gija turi nustatyti pirmąjį įvykį, jei likusi kvota yra ne nulis, ir antrąjį, jei kvota išnaudota. Gija, kuri nori gauti kvotą, turi laukti pirmojo įvykio. Kvotą didinančiai gijai tereikia palaukti bet kurio iš šių įvykių, nes jei jie abu yra iš naujo nustatyti, tai reiškia, kad su kvota šiuo metu dirba kita gija. Taigi du įvykiai vienu metu atlieka dvi funkcijas: duomenų prieigos sinchronizavimą ir laukimą. Galiausiai, kadangi gija laukia vieno iš dviejų įvykių, įvykis, signalizuojantis sustoti, lengvai įtraukiamas.

Pateiksiu Prašymo metodo įgyvendinimo pavyzdį. Likusieji įgyvendinami panašiai. Šiek tiek supaprastinau realiame projekte naudojamą kodą:

nepasirašytas tarpt CQuota :: Prašymas ( nepasirašytas tarpt _nPrašymas , HANDLE_hStopEvent )

jeigu(! _nPrašymas ) grąžinti 0 ;

nepasirašytas tarpt nPateikite = 0 ;

TVARKYKITE ĮVYKIUS [ 2 ];

hĮvykiai [ 0 ] = _hStopEvent ; // Sustabdymo įvykis turi didesnį prioritetą. Mes jį dedame pirmiausia.

hĮvykiai [ 1 ] = m_eventHasQuota ;

tarpt iWaitResult = :: WaitForMultipleObjects ( 2 , hĮvykiai , NETEISINGA , BEGALINIS );

jungiklis( iWaitResult ) {

atveju WAIT_FAILED :

// KLAIDA

mesti naujas CWin32 išimtis ;

atveju WAIT_OBJECT_0 :

// Sustabdyti įvykį. Aš jį tvarkiau su pasirinkta išimtimi, bet niekas netrukdo man to įgyvendinti kitu būdu.

mesti naujas CStopException ;

atveju WAIT_OBJECT_0 + 1 :

// Įvykis "laisva kvota"

Tvirtinti ( m_nKvota ); // Jei signalą davė šis įvykis, bet iš tikrųjų kvotos nėra, tai kažkur padarėme klaidą. Reikia ieškoti klaidos!

jeigu( _nPrašymas >= m_nKvota ) {

nPateikite = m_nKvota ;

m_nKvota = 0 ;

m_eventNoQuota . rinkinys ();

Kitas {

nPateikite = _nPrašymas ;

m_nKvota -= _nPrašymas ;

m_eventHasQuota . rinkinys ();

pertrauka;

grąžinti nPateikite ;

Maža pastaba. MFC biblioteka tame projekte nenaudota, bet, kaip jau turbūt atspėjote, aš sukūriau savo CEvent klasę, apvyniojimą aplink "event" branduolio objektą, panašų į MFC "schnoy. Kaip sakiau, tokios paprastos įvyniojimo klasės yra labai naudingi, kai yra koks nors išteklius (šiuo atveju branduolio objektas), kurį reikia nepamiršti atlaisvinti darbo pabaigoje. Likusioje dalyje nesvarbu, ar rašote SetEvent(m_hEvent) ar m_event.Set( ).

Tikiuosi, kad šis pavyzdys padės jums sukurti savo laiko schemą, jei susidursite su nereikšminga situacija. Svarbiausia yra kuo kruopščiau išanalizuoti savo schemą. Ar gali būti situacija, kai ji tinkamai neveiks, ypač ar gali atsirasti blokavimas? Pagauti tokias klaidas debugeryje dažniausiai yra beviltiškas reikalas, čia padeda tik išsami analizė.

Taigi mes svarstėme esminis įrankis gijų sinchronizavimas: branduolio sinchronizavimo objektai. Tai galingas ir universalus įrankis. Su juo galite sukurti net labai sudėtingas sinchronizavimo schemas. Laimei, tokių nereikšmingų situacijų pasitaiko retai. Be to, universalumas visada kainuoja našumą. Todėl daugeliu atvejų verta naudoti kitas "Windows" prieinamas gijų sinchronizavimo funkcijas, tokias kaip kritinės sekcijos ir atominės operacijos. Jie nėra tokie universalūs, bet yra paprasti ir veiksmingi. Apie juos kalbėsime kitoje dalyje.

Procesas yra į atmintį įkeltos programos egzempliorius. Šis egzempliorius gali sukurti gijas, kurios yra vykdomų instrukcijų seka. Svarbu suprasti, kad veikia ne procesai, o gijos.

Be to, bet kuris procesas turi bent vieną giją. Ši gija vadinama pagrindine (pagrindine) programos gija.

Kadangi gijų beveik visada yra daug daugiau nei fizinių procesorių joms vykdyti, gijos iš tikrųjų vykdomos ne vienu metu, o paeiliui (procesoriaus laiko pasiskirstymas vyksta būtent tarp gijų). Tačiau perjungimas tarp jų vyksta taip dažnai, kad atrodo, kad jie veikia lygiagrečiai.

Priklausomai nuo situacijos, siūlai gali būti trijų būsenų. Pirma, siūlas gali paleisti, kai jai duotas CPU laikas, t.y. jis gali būti aktyvus. Antra, jis gali būti neaktyvus ir laukti, kol bus paskirstytas procesorius, t.y. būti pasirengimo būsenoje. Ir yra trečias, taip pat labai svarbi sąlyga- užrakto būsena. Kai gija užblokuota, jam visiškai neskiriamas laikas. Dažniausiai laukiant kokio nors įvykio uždedama spyna. Kai įvyksta šis įvykis, gija automatiškai perkeliama iš užblokuotos būsenos į parengties būseną. Pavyzdžiui, jei viena gija atlieka skaičiavimus, o kita turi laukti, kol rezultatai bus išsaugoti diske. Antrasis galėtų naudoti ciklą, pvz., "while(!isCalcFinished) tęsti;", tačiau praktiškai nesunku pastebėti, kad procesorius yra 100% užimtas, kol veikia ši kilpa (tai vadinama aktyviu laukimu). Kai tik įmanoma, reikėtų vengti tokių kilpų, kuriose fiksavimo mechanizmas suteikia neįkainojamą pagalbą. Antroji gija gali užblokuoti save tol, kol pirmoji gija nustato įvykį, signalizuojantį, kad skaitymas baigtas.

Gijų sinchronizavimas Windows OS

„Windows“ įgyvendina prevencinį kelių užduočių atlikimą, o tai reiškia, kad bet kuriuo metu sistema gali nutraukti vienos gijos vykdymą ir perduoti valdymą kitai. Anksčiau Windows 3.1 buvo naudojamas organizavimo būdas, vadinamas kooperatyviu daugiafunkciniu: sistema laukė, kol pati gija jai perduos valdymą, todėl užstrigus vienai programai, kompiuterį reikėjo paleisti iš naujo.

Visos gijos, priklausančios tam pačiam procesui, turi bendrų išteklių, pvz., RAM adresų erdvę arba atidarytus failus. Šie ištekliai priklauso visam procesui, taigi ir kiekvienai jo gijai. Todėl kiekviena gija gali dirbti su šiais ištekliais be jokių apribojimų. Bet... Jei viena gija dar nebaigė dirbti su jokiu bendrinamu ištekliu, o sistema perėjo į kitą giją naudodama tą patį šaltinį, tai šių gijų darbo rezultatas gali labai skirtis nuo numatyto. Tokie konfliktai gali kilti ir tarp gijų, priklausančių skirtingiems procesams. Ši problema iškyla kiekvieną kartą, kai dvi ar daugiau gijų naudoja tam tikrus bendrinamus išteklius.

Pavyzdys. Nesinchronizuojamos gijos: jei laikinai sustabdote rodymo giją (pristabdote), fono masyvo užpildymo gija ir toliau bus vykdoma.

#įtraukti #įtraukti int a; HANDLE hThr; nepasirašytas ilgas 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; }

Štai kodėl reikalingas mechanizmas, leidžiantis gijomis koordinuoti savo darbą su bendrais ištekliais. Šis mechanizmas vadinamas gijų sinchronizavimo mechanizmu.

Šis mechanizmas yra operacinės sistemos objektų, kuriuos sukuria ir valdo programinė įranga, rinkinys, kurie yra bendri visoms sistemos gijomis (kai kuriuos dalijasi tam pačiam procesui priklausančios gijos) ir yra naudojami prieigai prie išteklių koordinuoti. Ištekliai gali būti bet kas, ką gali bendrinti dvi ar daugiau gijų – failas diske, prievadas, duomenų bazės įrašas, GDI objektas ir netgi visuotinis programos kintamasis (kuris gali būti pasiekiamas iš gijų, priklausančių tam pačiam procesui).

Yra keli sinchronizavimo objektai, iš kurių svarbiausi yra mutex, kritinė sekcija, įvykis ir semaforas. Kiekvienas iš šių objektų įgyvendina savo sinchronizacijos metodą. Taip pat patys procesai ir gijos gali būti naudojami kaip sinchronizavimo objektai (kai viena gija laukia kitos gijos ar proceso užbaigimo); taip pat failus, ryšio įrenginius, konsolės įvestį ir pranešimus apie pakeitimus.

Bet kuris sinchronizavimo objektas gali būti taip vadinamoje signalizuotoje būsenoje. Kiekvienam objekto tipui ši būsena turi skirtingą reikšmę. Gijos gali patikrinti esamą objekto būseną ir (arba) laukti, kol ta būsena pasikeis ir taip koordinuoti savo veiksmus. Tai užtikrina, kad gijai dirbant su sinchronizavimo objektais (juos sukuriant, keičiant būseną), sistema nenutrauks vykdymo, kol neatliks šio veiksmo. Taigi visos galutinės operacijos su sinchronizavimo objektais yra atominės (nedalomos.

Darbas su sinchronizavimo objektais

Norint sukurti vieną ar kitą sinchronizacijos objektą, iškviečiama speciali WinAPI funkcija Create... tipo (pvz. CreateMutex). Šis iškvietimas grąžina objekto rankenėlę (HANDLE), kurią gali naudoti visos duotam procesui priklausančios gijos. Sinchronizacijos objektą galima pasiekti iš kito proceso, paveldint objekto rankenėlę arba, pageidautina, iškvietus objekto funkciją Atidaryti.... Po šio skambučio procesas gaus rankenėlę, kurią vėliau bus galima naudoti dirbant su objektu. Objektui, nebent jis skirtas naudoti viename procese, turi būti suteiktas pavadinimas. Visų objektų pavadinimai turi būti skirtingi (net jei jie yra skirtingų tipų). Pavyzdžiui, negalite sukurti įvykio ir semaforo tuo pačiu pavadinimu.

Pagal turimą objekto deskriptorių galite nustatyti dabartinę jo būseną. Tai atliekama vadinamųjų pagalba. laukiančias funkcijas. Dažniausiai naudojama funkcija yra WaitForSingleObject. Šiai funkcijai reikia dviejų parametrų: pirmasis yra objekto rankena, antrasis yra skirtasis laikas ms. Funkcija grąžina WAIT_OBJECT_0, jei objektas yra signalizuotos būsenos, WAIT_TIMEOUT, jei laikas pasibaigė, ir WAIT_ABANDONED, jei mutex nebuvo atlaisvintas prieš pasibaigiant nuosavybės gijai. Jei skirtasis laikas nurodytas kaip nulis, funkcija grįžta iš karto, kitu atveju laukiama nurodyto laiko. Jei objekto būsena pranešama nepasibaigus šiam laikui, funkcija grąžins WAIT_OBJECT_0, kitu atveju funkcija grąžins WAIT_TIMEOUT. Jei simbolinė konstanta INFINITE nurodoma kaip laikas, tada funkcija lauks neribotą laiką, kol bus pranešta apie objekto būseną.

Labai svarbu, kad iškvietimas į laukimo funkciją blokuotų esamą giją, t.y. kol gija neveikia, jai neskiriamas joks procesoriaus laikas.

Kritinės sekcijos

Objektui svarbi sekcija padeda programuotojui atskirti kodo sekciją, kurioje gija pasiekia bendrinamą šaltinį ir neleidžia vienu metu naudoti išteklių. Prieš naudojant šaltinį, gija patenka į kritinę sekciją (iškviečia funkciją EnterCriticalSection). Jei kuri nors kita gija bandys įeiti į tą pačią svarbią sekciją, jos vykdymas bus pristabdytas, kol pirmoji gija išeis iš sekcijos su iškvietimu į LeaveCriticalSection. Naudojamas tik sriegiams viename procese. Įėjimo į kritinę sekciją tvarka nėra apibrėžta.

Taip pat yra „TryEnterCriticalSection“ funkcija, kuri patikrina, ar svarbi dalis šiuo metu užimta. Su jo pagalba gijos, laukiančios prieigos prie šaltinio, negalima užblokuoti, tačiau atlikti keletą naudingų veiksmų.

Pavyzdys. Gijų sinchronizavimas naudojant svarbias dalis.

#įtraukti #įtraukti CRITICAL_SECTION cs; int a; HANDLE hThr; nepasirašytas ilgas 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; }

Abipusė atskirtis

Abipusio išskyrimo objektai (mutexes, mutex - iš MUTual EXclusion) leidžia koordinuoti abipusį prieigos prie bendro išteklių pašalinimą. Signalizuota objekto būsena (tai yra „nustatyta“ būsena) atitinka momentą, kai objektas nepriklauso jokiai gijai ir gali būti „užfiksuotas“. Atvirkščiai, „iš naujo“ (nesignalizuota) būsena atitinka momentą, kai kai kuriai gijai jau priklauso šis objektas. Prieiga prie objekto suteikiama, kai gija, kuriai priklauso objektas, jį išleidžia.

Dvi (ar daugiau) gijų gali sukurti mutex tuo pačiu pavadinimu, iškviečiant funkciją CreateMutex. Pirmoji gija iš tikrųjų sukuria mutex, o kitos gijos gauna jau esamo objekto rankenėlę. Tai leidžia daugeliui gijų gauti rankenėlę to paties mutex, todėl programuotojas neturi nerimauti dėl to, kas iš tikrųjų sukuria mutex. Jei naudojamas šis metodas, pageidautina nustatyti bInitialOwner vėliavėlę į FALSE, kitaip kils tam tikrų sunkumų nustatant tikrąjį mutex kūrėją.

Kelios gijos gali įgyti to paties mutex rankeną, todėl įmanoma bendrauti tarp procesų. Šiam metodui galite naudoti šiuos mechanizmus:

  • Antrinis procesas, sukurtas naudojant funkciją CreateProcess, gali paveldėti mutex rankeną, jei parametras lpMutexAttributes buvo nurodytas, kai mutex buvo sukurta naudojant funkciją CreateMutex.
  • Gija gali gauti esamo mutex dublikatą naudojant funkciją DuplicateHandle.
  • Iškviečiant OpenMutex arba CreateMutex funkcijas, gija gali nurodyti esamo mutex pavadinimą.

Norint paskelbti, kad mutex priklauso dabartinei gijai, reikia iškviesti vieną iš laukiančių funkcijų. Siūlas, kuriam priklauso objektas, gali jį pakartotinai „užfiksuoti“ tiek kartų, kiek jai patinka (tai nesukels savaiminio užrakinimo), tačiau ji turės tiek kartų ją paleisti naudojant funkciją ReleaseMutex.

Norint sinchronizuoti vieno proceso gijas, efektyviau naudoti svarbias dalis.

Pavyzdys. Gijų sinchronizavimas naudojant mutexes.

#įtraukti #įtraukti RANKENĖ hMutex; int a; HANDLE hThr; nepasirašytas ilgas uThrID; void Thread(void* pParams) ( int i, skaičius = 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; }

Vystymai

Įvykių objektai naudojami pranešti laukiančioms gijomis, kad įvyko įvykis. Yra dviejų tipų įvykiai – su rankiniu ir automatiniu atstatymu. Rankinį atstatymą atlieka funkcija ResetEvent. Neautomatinio nustatymo iš naujo įvykiai naudojami pranešimui kelioms gijomis vienu metu. Naudojant automatinio nustatymo iš naujo įvykį, tik viena laukianti gija gaus pranešimą ir tęs jo vykdymą, o likusi dalis lauks toliau.

Funkcija CreateEvent sukuria įvykio objektą, SetEvent – ​​nustato įvykio signalo būseną, ResetEvent – ​​iš naujo nustato įvykį. Funkcija PulseEvent nustato įvykį, o atnaujinus gijas, laukiančias šio įvykio (visos su rankiniu atstatymu ir tik viena su automatiniu), atstato jį iš naujo. Jei nėra laukiančių gijų, PulseEvent tiesiog iš naujo nustato įvykį.

Pavyzdys. Gijų sinchronizavimas naudojant įvykius.

#įtraukti #įtraukti TVARKITE hEvent1, hEvent2; int a; HANDLE hThr; nepasirašytas ilgas uThrID; void Thread(void* pParams) ( int i, skaičius = 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; }

semaforai

Semaforo objektas iš tikrųjų yra mutex objektas su skaitikliu. Šis objektas leidžiasi „pagauti“ tam tikru gijų skaičiumi. Po to „pagauti“ bus neįmanoma, kol viena iš anksčiau „pagautų“ semaforo gijų jo nepaleis. Semaforai naudojami siekiant apriboti gijų, kurios vienu metu gali pasiekti išteklius, skaičių. Inicializacijos metu į objektą perkeliamas maksimalus gijų skaičius, po kiekvieno „užfiksavimo“ semaforų skaitiklis mažėja. Signalo būsena atitinka skaitiklio vertę, didesnę už nulį. Kai skaitiklis yra nulis, semaforas laikomas išjungtu (nustatytas iš naujo).

Funkcija CreateSemaphore sukuria semaforo objektą su nuoroda į jo didžiausią galimą pradinę vertę, OpenSemaphore – grąžina rankenėlę esamam semaforui, semaforas užfiksuojamas naudojant laukimo funkcijas, o semaforo reikšmė sumažinama vienu, ReleaseSemaphore – atleidžia semaforą su semaforo reikšmės padidėjimas parametro numeriu nurodyta reikšme.

Pavyzdys. Gijų sinchronizavimas naudojant semaforus.

#įtraukti #įtraukti RANKENĖ hSem; int a; HANDLE hThr; nepasirašytas ilgas uThrID; void Thread(void* pParams) ( int i, skaičius = 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; }

Apsaugota prieiga prie kintamųjų

Yra daugybė funkcijų, leidžiančių dirbti su globaliais kintamaisiais iš visų gijų, nesijaudinant dėl ​​sinchronizavimo, nes. šios funkcijos tuo pasirūpina pačios – jų vykdymas atominis. Tai yra funkcijos InterlockedIncrement, InterlockedDecrement, InterlockedExchange, InterlockedExchangeAdd ir InterlockedCompareExchange. Pavyzdžiui, funkcija InterlockedIncrement atomiškai padidina 32 bitų kintamojo reikšmę vienu, o tai naudinga įvairiems skaitikliams.

Norėdami gauti išsamią informaciją apie visų WIN32 API funkcijų paskirtį, naudojimą ir sintaksę, turite naudoti MS SDK pagalbos sistemą, kuri yra Borland Delphi arba CBuilder programavimo aplinkų dalis, taip pat MSDN, kuris tiekiamas kaip dalis Visual C programavimo sistema.


Programoms, kurios naudoja kelias gijas ar procesus, būtina, kad visos atliktų joms priskirtas funkcijas norima seka. Windows 9x aplinkoje šiam tikslui siūloma panaudoti kelis mechanizmus, kurie užtikrina sklandų gijų veikimą. Šie mechanizmai vadinami sinchronizacijos mechanizmai. Tarkime, kad kuriate programą, kurioje lygiagrečiai veikia dvi gijos. Kiekviena gija pasiekia vieną bendrinamą visuotinį kintamąjį. Viena gija kiekvieną kartą, kai pasiekiamas šis kintamasis, jį padidina, o antroji gija sumažina. Vienu metu asinchroniškai dirbant gijomis, neišvengiamai susidaro tokia situacija: - pirmoji gija perskaitė globalaus kintamojo reikšmę į lokalų; - OS ją nutraukia, nes jai skirtas procesoriaus laiko kvantas baigėsi, ir perduoda valdymą antrajai gijai; - antroji gija taip pat perskaitė globalaus kintamojo reikšmę į lokalią, ją sumažino ir naują reikšmę parašė atgal; - OS vėl perduoda valdymą pirmai gijai, kuri, nieko nežinodama apie antrosios gijos veiksmus, padidina savo vietinį kintamąjį ir įrašo jo reikšmę į pasaulinę. Akivaizdu, kad antrosios gijos atlikti pakeitimai bus prarasti. Norint išvengti tokių situacijų, būtina laiku atskirti bendrinamų duomenų naudojimą. Tokiais atvejais naudojami sinchronizavimo mechanizmai, užtikrinantys teisingą kelių gijų veikimą. Sinchronizavimo įrankiai OSWindows: 1) kritinis skyrius (KritinisSkyrius) yra objektas, priklausantis procesui, o ne branduoliui. Tai reiškia, kad ji negali sinchronizuoti gijų iš skirtingų procesų. Taip pat yra inicijavimo (sukūrimo) ir ištrynimo, įėjimo ir išėjimo iš kritinės sekcijos funkcijos: kūrimas - InitializeCriticalSection(...), trynimas - DeleteCriticalSection(...), įvedimas - EnterCriticalSection(...), išėjimas - LeaveCriticalSection (...). Apribojimai: kadangi tai nėra branduolio objektas, jis nėra matomas kitiems procesams, t.y. galite apsaugoti tik savo proceso gijas. Kritinė dalis analizuoja specialaus proceso kintamojo, kuris naudojamas kaip vėliavėlė, reikšmę, kad kelios gijos negalėtų vykdyti kodo dalies vienu metu. Tarp sinchronizuojamų objektų svarbiausios dalys yra paprasčiausios. 2) mutexkintamasNeįtraukti. Tai yra branduolio objektas, jis turi pavadinimą, o tai reiškia, kad jie gali būti naudojami sinchronizuoti prieigą prie bendrinamų duomenų iš kelių procesų, tiksliau, iš skirtingų procesų gijų. Jokia kita gija negali įgyti mutex, kuris jau priklauso vienai iš gijų. Jei mutex apsaugo kai kuriuos bendrinamus duomenis, ji galės atlikti savo funkciją tik tada, jei kiekviena gija patikrins šio mutex būseną prieš pasiekiant šiuos duomenis. „Windows“ traktuoja „mutex“ kaip bendrinamą objektą, apie kurį galima pranešti arba nustatyti iš naujo. Signalizuota mutex būsena rodo, kad jis užimtas. Gijos turi savarankiškai analizuoti esamą mutexų būseną. Jei norite, kad mutex būtų pasiekiama gijomis iš kitų procesų, turite suteikti jam pavadinimą. Funkcijos: CreateMutex(name) – kūrimas, hnd=OpenMutex(name) – atidarymas, WaitForSingleObject(hnd) – laukimas ir okupavimas, ReleaseMutex(hnd) – atleidimas, CloseHandle(hnd) – uždarymas. Jis gali būti naudojamas apsisaugoti nuo programų paleidimo iš naujo. 3) semaforas -semaforas. Branduolio objektas „semaforas“ naudojamas resursų apskaitai ir skirtas apriboti vienalaikę prieigą prie resurso keliomis gijomis. Naudodami semaforą galite organizuoti programos darbą taip, kad prie šaltinio vienu metu galėtų patekti kelios gijos, tačiau šių gijų skaičius bus ribotas. Kuriant semaforą nurodomas maksimalus gijų, kurios vienu metu gali dirbti su ištekliu, skaičius. Kiekvieną kartą, kai programa pasiekia semaforą, semaforo išteklių skaitiklis sumažinamas vienu. Kai išteklių skaitiklio reikšmė tampa nuliu, semaforas nepasiekiamas. sukurkite CreateSemaphore, atidarykite OpenSemaphore, paimkite WaitForSingleObject, atleiskite ReleaseSemaphore 4 ) renginys -renginys. Įvykiai dažniausiai tik praneša apie kokios nors operacijos pabaigą, jie taip pat yra branduolio objektai. Galite ne tik aiškiai išleisti, bet taip pat yra įvykio nustatymo operacija. Įvykiai gali būti rankiniai (rankiniai) ir pavieniai (vienkartiniai). Vienas įvykis yra labiau bendra vėliava. Įvykis yra signalizuotos būsenos, jei jį nustatė kokia nors gija. Jei programa reikalauja, kad įvykus įvykiui į ją reaguotų tik viena iš gijų, o visos kitos gijos toliau lauktų, tada naudojamas vienas įvykis. Rankinis įvykis nėra tik įprasta vėliavėlė keliose gijose. Jis atlieka šiek tiek sudėtingesnes funkcijas. Bet kuri grupė gali nustatyti šį įvykį arba iš naujo nustatyti (išvalyti). Nustačius įvykį, jis išliks tokioje būsenoje savavališkai ilgą laiką, nepaisant to, kiek gijų laukia, kol įvykis bus nustatytas. Kai visos gijos, laukiančios šio įvykio, gaus pranešimą, kad įvykis įvyko, jis bus automatiškai nustatytas iš naujo. Funkcijos: SetEvent, ClearEvent, WaitForEvent. Įvykių tipai: 1) automatinio atstatymo įvykis: WaitForSingleEvent. 2) įvykis su rankiniu atstatymu (rankiniu būdu), tada įvykis turi būti nustatytas iš naujo: ReleaseEvent. Kai kurie teoretikai išskiria dar vieną sinchronizavimo objektą: WaitAbleTimer yra OS branduolio objektas, kuris savarankiškai persijungia į laisvą būseną po nurodyto laiko intervalo (žadintuvas).

Kartais tai tampa būtina dirbant su keliomis gijomis ar procesais sinchronizuoti vykdymą du ar daugiau iš jų. To priežastis dažniausiai yra ta, kad dviem ar daugiau gijų gali prireikti prieigos prie bendrinamo šaltinio, kuris tikrai negalima vienu metu pateikti kelioms gijomis. Bendrinamas išteklius yra išteklius, kurį vienu metu gali pasiekti kelios vykdomos užduotys.

Mechanizmas, užtikrinantis sinchronizacijos procesą, vadinamas prieigos apribojimas. Jos poreikis atsiranda ir tais atvejais, kai viena gija laukia kitos gijos sugeneruoto įvykio. Žinoma, turi būti koks nors būdas, kuriuo pirmoji gija bus sustabdyta, kol įvyks įvykis. Po to siūlas turėtų tęsti vykdymą.

Yra dvi bendros būsenos, kuriose gali būti užduotis. Pirma, užduotis gali būti atliktas(arba būkite pasirengęs vykdyti, kai tik turės prieigą prie procesoriaus išteklių). Antra, užduotis gali būti užblokuotas. Tokiu atveju jo vykdymas sustabdomas, kol bus išleistas reikalingas išteklius arba įvyks tam tikras įvykis.

„Windows“ turi specialias paslaugas, kurios leidžia tam tikru būdu apriboti prieigą prie bendrinamų išteklių, nes be operacinės sistemos pagalbos atskiras procesas ar gija negali pats nustatyti, ar turi vienintelę prieigą prie resurso. „Windows“ operacinėje sistemoje yra procedūra, kuri vienos nepertraukiamos operacijos metu patikrina ir, jei įmanoma, nustato išteklių prieigos vėliavėlę. Operacinių sistemų kūrėjų kalba tokia operacija vadinama patikrinkite ir įdiekite veikimą. Iškviečiamos vėliavėlės, naudojamos sinchronizavimui užtikrinti ir prieigai prie išteklių valdyti semaforai(semaforas). „Win32“ API palaiko semaforus ir kitus sinchronizavimo objektus. MFC bibliotekoje taip pat yra šių objektų palaikymas.

Sinchronizavimo objektai ir mfc klasės

Win32 sąsaja palaiko keturių tipų sinchronizavimo objektus, kurie visi vienaip ar kitaip pagrįsti semaforo koncepcija.

Pirmasis objekto tipas yra pats semaforas arba klasikinis (standartinis) semaforas. Tai leidžia ribotam procesų ir gijų skaičiui pasiekti vieną šaltinį. Tokiu atveju prieiga prie resurso yra arba visiškai apribota (viena ir tik viena gija arba procesas gali pasiekti resursą per tam tikrą laikotarpį), arba tik nedaugelis gijų ir procesų gauna vienu metu prieigą. Semaforai įgyvendinami naudojant skaitiklį, kuris mažėja, kai užduočiai priskiriamas semaforas, ir didėja, kai užduotis atleidžia semaforą.

Antrasis sinchronizavimo objektų tipas yra išskirtinis (mutex) semaforas. Jis skirtas visiškai apriboti prieigą prie šaltinio, kad tik vienas procesas arba gija galėtų pasiekti išteklius bet kuriuo metu. Tiesą sakant, tai yra ypatinga semaforo rūšis.

Trečiasis sinchronizavimo objektų tipas yra renginys, arba įvykio objektas. Jis naudojamas blokuoti prieigą prie ištekliaus, kol koks nors kitas procesas ar gija paskelbs, kad išteklius gali būti naudojamas. Taigi šis objektas signalizuoja apie reikiamo įvykio įvykdymą.

Naudojant ketvirtojo tipo sinchronizacijos objektą, galima uždrausti vykdyti tam tikras programos kodo dalis keliomis gijomis vienu metu. Norėdami tai padaryti, šie sklypai turi būti deklaruoti kaip kritinis skyrius. Kai viena gija patenka į šią sekciją, kitoms gijomis draudžiama daryti tą patį, kol pirmoji gija išeis iš šios sekcijos.

Kritinės sekcijos, skirtingai nuo kitų tipų sinchronizavimo objektų, naudojamos tik sinchronizuoti gijas viename procese. Kitų tipų objektai gali būti naudojami sinchronizuoti proceso gijas arba sinchronizuoti procesus.

MFC sistemoje Win32 sąsajos teikiamas sinchronizavimo mechanizmas palaikomas per šias klases, gautas iš CSyncObject klasės:

    CCriticalSection- įgyvendina kritinį skyrių.

    CEįvykis- įgyvendina renginio objektą

    CMutex- įgyvendina išskirtinį semaforą.

    CSemaforas- įgyvendina klasikinį semaforą.

Be šių klasių, MFC taip pat apibrėžia dvi pagalbines sinchronizavimo klases: CSingleLock ir CMultiLock. Jie kontroliuoja prieigą prie sinchronizavimo objekto ir apima metodus, naudojamus suteikti ir atleisti tokius objektus. Klasė CSingleLock kontroliuoja prieigą prie vieno sinchronizavimo objekto ir klasės CMultiLock- į kelis objektus. Toliau nagrinėsime tik klasę CSingleLock.

Sukūrus bet kurį sinchronizavimo objektą, prieigą prie jo galima valdyti naudojant klasę CSingleLock. Norėdami tai padaryti, pirmiausia turite sukurti tipo objektą CSingleLock naudojant konstruktorių:

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

Pirmasis parametras yra rodyklė į sinchronizacijos objektą, pvz., semaforą. Antrojo parametro reikšmė nustato, ar konstruktorius turi bandyti pasiekti nurodytą objektą. Jei šis parametras yra ne nulis, tada prieiga bus suteikta, kitu atveju nebus bandoma pasiekti. Jei prieiga suteikiama, tada gija, kuri sukūrė klasės objektą CSingleLock, bus sustabdytas, kol atitinkamas sinchronizavimo objektas bus atlaisvintas naudojant metodą Atrakinti klasė CSingleLock.

Sukūrus CSingleLock tipo objektą, prieigą prie objekto, į kurį nukreipia parametras pObject, galima valdyti naudojant dvi funkcijas: užraktas ir Atrakinti klasė CSingleLock.

Metodas užraktas skirta prieiti prie objekto prie sinchronizavimo objekto. Ją iškvietusi gija sustabdoma, kol metodas bus baigtas, tai yra, kol bus pasiektas išteklius. Parametro reikšmė nustato, kiek laiko funkcija lauks, kol gaus prieigą prie reikiamo objekto. Kiekvieną kartą sėkmingai užbaigus metodą, su sinchronizavimo objektu susieto skaitiklio reikšmė sumažinama vienu.

Metodas Atrakinti išleidžia sinchronizavimo objektą, leisdamas kitoms gijomis naudoti išteklius. Pirmajame metodo variante su nurodytu objektu susieto skaitiklio reikšmė padidinama vienu. Antrajame variante pirmasis parametras nustato, kiek ši vertė turėtų būti padidinta. Antrasis parametras nurodo kintamąjį, į kurį bus įrašyta ankstesnė skaitiklio reikšmė.

Dirbant su klase CSingleLock Bendra prieigos prie išteklių valdymo procedūra yra tokia:

    sukurti CSyncObj tipo objektą (pavyzdžiui, semaforą), kuris bus naudojamas prieigai prie resurso valdyti;

    naudojant sukurtą sinchronizacijos objektą, sukurti CSingleLock tipo objektą;

    iškvieskite užrakto metodą, kad gautumėte prieigą prie šaltinio;

    paskambinti šaltiniui;

    iškvieskite atrakinimo metodą, kad paleistumėte išteklius.

Toliau aprašoma, kaip kurti ir naudoti semaforus ir įvykių objektus. Kai suprasite šias sąvokas, galėsite lengvai išmokti ir naudoti kitus dviejų tipų sinchronizavimo objektus: svarbias dalis ir nutildymus.