Lõime olekud. Kriitilised jaotised Sünkroonimise lõpetamine Windows OS-is

Seda sünkroonimisobjekti saab kasutada ainult lokaalselt selle loomise protsessis. Ülejäänud objekte saab kasutada erinevate protsesside lõimede sünkroniseerimiseks. Objekti nimetus “kriitiline sektsioon” on seotud mingi abstraktse valikuga programmikoodi osast (sektsioonist), mis sooritab mingeid toiminguid, mille järjekorda ei saa rikkuda. See tähendab, et kahe erineva lõime katse selle jaotise koodi samaaegselt käivitada põhjustab tõrke.

Näiteks võib olla mugav kaitsta kirjutaja funktsioone sellise jaotisega, kuna mitme kirjutaja samaaegne juurdepääs tuleks välistada.

Kriitilise jaotise jaoks tehakse kaks toimingut:

sisestage jaotis Kui mis tahes lõim on kriitilises jaotises, lõpetavad kõik muud lõimed automaatselt ootamise, kui nad proovivad sellesse siseneda. Sellesse jaotisesse juba sisenenud lõim saab sellesse mitu korda siseneda, ootamata selle vabastamist.

lahkuda sektsioonist Kui lõime sektsioonist lahkub, vähendatakse selle lõime sektsiooni sisenemiste arvu loendurit, nii et lõik vabaneb teiste lõimede jaoks ainult siis, kui lõim väljub jaotisest nii mitu korda, kui see sinna sisenes. Kui kriitiline sektsioon vabastatakse, äratatakse ainult üks lõim, mis ootab luba sellesse sektsiooni sisenemiseks.

Üldiselt ei käsitleta teistes mitte-Win32 API-des (nt OS/2) kriitilist jaotist sünkroonimisobjektina, vaid programmikoodi osana, mida saab käivitada ainult üks rakenduse lõim. See tähendab, et kriitilisse sektsiooni sisenemist peetakse keerme vahetamise mehhanismi ajutiseks väljalülitamiseks kuni sellest sektsioonist väljumiseni. Win32 API käsitleb kriitilisi sektsioone objektidena, mis tekitab segadust – need on oma omadustelt väga lähedased nimetamata eksklusiivsetele objektidele ( mutex, vt allpool).

Kriitiliste sektsioonide kasutamisel tuleb jälgida, et sektsioonis ei eraldataks liiga suuri koodifragmente, kuna see võib põhjustada olulisi viivitusi teiste lõimede täitmisel.

Näiteks juba vaadeldud kuhjade puhul pole mõtet kaitsta kõiki hunniku funktsioone kriitilise sektsiooniga, kuna lugeja funktsioone saab täita paralleelselt. Pealegi tundub kriitilise sektsiooni kasutamine isegi kirjutajate sünkroonimiseks tegelikult ebamugav olevat – kuna kirjutaja sünkroonimiseks lugejatega peavad viimased ikkagi sellesse sektsiooni sisenema, mis praktiliselt viib kõigi funktsioonide kaitsmiseni üheainsa abil. osa.

Kriitiliste sektsioonide tõhusaks kasutamiseks on mitu juhtumit:

lugejad ei lähe kirjanikega vastuollu (kaitsma tuleb ainult kirjanikke);

kõigil lõimedel on ligikaudu võrdsed juurdepääsuõigused (ütleme, et te ei saa eraldi välja tuua puhtaid kirjutajaid ja lugejaid);

mitmest standardobjektist koosnevate liitsünkroniseerivate objektide konstrueerimisel, et kaitsta järjestikuseid tehteid liitobjektil.

Artikli eelmistes osades rääkisin sellest üldised põhimõtted ja spetsiifilised meetodid mitme keermega rakenduste loomiseks. Erinevad lõimed peavad peaaegu alati perioodiliselt üksteisega suhtlema ja paratamatult tekib vajadus sünkroonimise järele. Täna vaatame kõige olulisemat, võimsaimat ja mitmekülgsemat Windowsi sünkroonimistööriista: Kerneli sünkroonimise objektid.

WaitForMultipleObjects ja muud ootefunktsioonid

Nagu mäletate, peate lõimede sünkroonimiseks tavaliselt mõne lõime täitmise ajutiselt peatama. Seda tuleb aga tõlkida vahenditega operatsioonisüsteem ooteolekusse, kus see ei võta protsessori aega. Teame juba kahte funktsiooni, mis seda teha saavad: SuspendThread ja ResumeThread . Kuid nagu ma artikli eelmises osas ütlesin, ei sobi need funktsioonid mõne funktsiooni tõttu sünkroonimiseks.

Täna vaatleme veel üht funktsiooni, mis paneb lõime samuti ooteolekusse, kuid erinevalt SuspendThread/ResumeThreadist on see spetsiaalselt loodud sünkroonimise korraldamiseks. See on WaitForMultipleObjects. Kuna see funktsioon on nii oluline, kaldun ma pisut kõrvale oma reeglist mitte laskuda API üksikasjadesse ja räägin sellest üksikasjalikumalt, annan isegi selle prototüübi:

DWORD WaitForMultipleObjects (

DWORD nCount , // objektide arv massiivi lpHandles

CONST KÄEPIDE * lpkäepidemed , // osutab kerneli objektide deskriptorite massiivile

BOOL bOota kõik , // lipp, mis näitab, kas oodata kõiki objekte või piisab ühest

DWORD dwMillisekundit // aeg maha

Selle funktsiooni põhiparameeter on osuti kerneli objektide käepidemete massiivile. Sellest, millised need objektid on, räägime allpool. Praegu on meile oluline teada, et ükskõik milline neist objektidest võib olla ühes kahest olekust: neutraalne või "signaali andev" (signaliseeritud olek). Kui bWaitAll lipp on FALSE, naaseb funktsioon niipea, kui vähemalt üks objektidest annab signaali. Ja kui lipp on TRUE, juhtub see ainult siis, kui kõik objektid hakkavad korraga signaali andma (nagu näeme, on see selle funktsiooni kõige olulisem omadus). Esimesel juhul saate tagastatud väärtuse järgi teada, milline objekt andis signaali. Peate sellest lahutama konstandi WAIT_OBJECT_0 ja saate indeksi massiivi lpHandles. Kui ajalõpp ületab viimases parameetris määratud ajalõpu, lõpetab funktsioon ootamise ja tagastab väärtuse WAIT_TIMEOUT . Ajalõpuks saab määrata konstantse LÕPETUSE ja siis funktsioon ootab "kuni peatub" või siis vastupidi 0 ja siis lõime ei peatata üldse. Viimasel juhul naaseb funktsioon kohe, kuid selle tulemus ütleb teile objektide oleku. Viimast tehnikat kasutatakse väga sageli. Nagu näete, on sellel funktsioonil rikkalikud võimalused. On mitmeid teisi WaitForXXX funktsioone, kuid need kõik on põhiteema variatsioonid. Eelkõige on WaitForSingleObject lihtsalt selle lihtsustatud versioon. Ülejäänutel on igaühel oma lisafunktsioonid, kuid neid kasutatakse üldiselt harvemini. Näiteks võimaldavad need reageerida mitte ainult tuumaobjektide signaalidele, vaid ka uute aknateadete saabumisele lõime järjekorda. Nende kirjelduse ja üksikasjaliku teabe WaitForMultipleObjectsi kohta leiate nagu tavaliselt MSDN-ist.

Nüüd sellest, mis need salapärased "tuumaobjektid" on. Alustuseks hõlmavad need lõime ja protsesse ise. Nad sisenevad signaalimisolekusse kohe pärast lõpetamist. See on väga oluline funktsioon, sest sageli on vaja jälgida, millal lõime või protsess on lõppenud. Näiteks peaks meie serverirakendus koos töölõimede komplektiga valmis saama. Samal ajal peab juhtlõng teavitama töötaja lõime mingil viisil, et on aeg töö lõpetada (näiteks määrates globaalse lipu) ja seejärel ootama, kuni kõik lõimed on lõpetatud, tehes kõik õigeks lõpetamiseks vajaliku tegevusest: ressursside vabastamine, klientide teavitamine sulgemisest, võrguühenduste sulgemine jne.

Asjaolu, et lõimed lülitavad töö lõpus signaali sisse, muudab keerme lõppemisega sünkroonimise probleemi lahendamise äärmiselt lihtsaks:

// Lihtsuse huvides olgu üks töölõng. Käitame seda:

HWorkerThreadi KÄSITSEMINE = :: Loo lõim (...);

// Enne töö lõppu peame kuidagi töötaja lõimele ütlema, et on aeg üles laadida.

// Oodake lõime lõppu:

DWORD dwWaitResult = :: WaitForSingleObject ( hWorkerThread , LÕPMATU );

kui( dwWaitResult != WAIT_OBJECT_0 ) { /* vigade käsitlemine */ }

// Voo "käepide" saab sulgeda:

KINNITA (:: CloseHandle ( hWorkerThread );

/* Kui CloseHandle ebaõnnestub ja tagastab FALSE, ei tee ma erandit. Esiteks, isegi kui see juhtuks süsteemivea tõttu, ei oleks sellel meie programmile otseseid tagajärgi, sest kuna me sulgeme käepideme, siis pole sellega tulevikus tööd oodata. Tegelikkuses võib CloseHandle'i rike tähendada ainult viga teie programmis. Seetõttu sisestame siia makro VERIFY, et seda rakenduse silumise etapis mitte vahele jätta. */

Protsessi lõppu ootav kood näeb välja sarnane.

Kui sellist sisseehitatud võimalust poleks, peaks töölõim oma valmimise kohta teabe kuidagi põhilõimele endale edastama. Isegi kui see juhtus viimasena, ei saanud põhilõng olla kindel, et töötajal pole täitmiseks jäänud vähemalt paar montaažikäsku. AT üksikud olukorrad(näiteks kui lõime kood on DLL-is, mis tuleb selle lõppedes maha laadida), võib see olla saatuslik.

Tahan teile meelde tuletada, et isegi pärast lõime (või protsessi) lõpetamist jäävad selle käepidemed endiselt kehtima, kuni funktsioon CloseHandle need selgesõnaliselt sulgeb. (Muide, ärge unustage seda teha!) Seda tehakse lihtsalt selleks, et saaksite lõime olekut igal ajal kontrollida.

Niisiis võimaldab funktsioon WaitForMultipleObjects (ja selle analoogid) sünkroonida lõime täitmist sünkroonimisobjektide, eriti teiste lõimede ja protsesside olekuga.

Spetsiaalsed tuumaobjektid

Liigume edasi tuumaobjektide käsitlemise juurde, mis on loodud spetsiaalselt sünkroonimiseks. Need on sündmused, semaforid ja mutexid. Heidame neile igaühele lühiülevaate:

sündmus

Võib-olla kõige lihtsam ja põhilisem sünkroniseeriv objekt. See on lihtsalt lipp, mida saab seadistada SetEvent / ResetEvent funktsioonidega: signaalimine või neutraalne. Sündmus on kõige mugavam viis ootavale lõimele märku anda, et mõni sündmus on toimunud (sellepärast seda kutsutaksegi) ja saate tööd jätkata. Sündmust kasutades saame hõlpsasti lahendada sünkroonimisprobleemi töölõime lähtestamisel:

// Lihtsuse huvides hoiame sündmuse pidet globaalses muutujas:

HANDLE g_hEventInitComplete = NULL ; // ärge kunagi jätke muutujat initsialiseerimata!

{ // kood põhilõimes

// sündmuse loomine

g_hEventInitComplete = :: Loo Sündmus ( NULL,

VALE , // sellest parameetrist räägime hiljem

VALE , // algolek - neutraalne

kui(! g_hEventInitComplete ) { /* Ärge unustage veakäsitlust */ }

// looge töötaja lõim

DWORD idWorkerThread = 0 ;

HWorkerThreadi KÄSITSEMINE = :: Loo lõim ( NULL , 0 , & WorkerThreadProc , NULL , 0 , & idWorkerThread );

kui(! hWorkerThread ) { /* vigade käsitlemine */ }

// ootame signaali töölõngast

DWORD dwWaitResult = :: WaitForSingleObject ( g_hEventInitComplete , LÕPMATU );

kui( dwWaitResult != WAIT_OBJECT_0 ) { /* viga */ }

// nüüd võite olla kindel, et töötaja lõim on lähtestamise lõpetanud.

KINNITA (:: CloseHandle ( g_hEventInitComplete )); // ärge unustage mittevajalikke objekte sulgeda

g_hEventInitComplete = NULL ;

// töövoo funktsioon

DWORD WINAPI WorkerThreadProc ( LPVOID_parameeter )

InitializeWorker (); // initsialiseerimine

// annab märku, et initsialiseerimine on lõppenud

BOOL on OK = :: SetEvent ( g_hEventInitComplete );

kui(! on ok ) { /* viga */ }

Tuleb märkida, et sündmusi on kaks märkimisväärselt erinevat sorti. Saame valida ühe neist funktsiooni CreateEvent teise parameetri abil. Kui see on TRUE, luuakse sündmus, mille olekut juhitakse ainult käsitsi, st funktsioonide SetEvent/ResetEvent abil. Kui see on FALSE, genereeritakse automaatse lähtestamise sündmus. See tähendab, et niipea, kui mõni teatud sündmust ootav lõim selle sündmuse signaaliga vabastatakse, lähtestatakse see automaatselt tagasi neutraalsesse olekusse. Nende erinevus tuleb kõige selgemini välja olukorras, kus ühte sündmust ootab korraga mitu lõime. Käsitsi juhitav sündmus on nagu stardipüstol. Niipea, kui see on seatud signaali olekusse, vabastatakse kõik lõimed korraga. Teisest küljest on automaatse lähtestamise sündmus nagu metroo pöördvärav: see vabastab ainult ühe voolu ja naaseb neutraalsesse olekusse.

Mutex

Võrreldes sündmusega on see rohkem spetsialiseerunud objekt. Tavaliselt kasutatakse seda tavalise sünkroonimisprobleemi lahendamiseks, nagu juurdepääs mitme lõimega jagatud ressursile. See on paljuski sarnane automaatse lähtestamise sündmusega. Peamine erinevus seisneb selles, et sellel on spetsiaalne sidumine konkreetse niidiga. Kui mutex on signaalitud olekus, tähendab see, et see on vaba ega kuulu ühelegi lõimele. Niipea, kui teatud lõim on seda mutexi oodanud, lähtestatakse viimane neutraalsesse olekusse (siin on see nagu automaatse lähtestamise sündmus) ja lõim saab selle omanikuks, kuni see Mutexi funktsiooniga ReleaseMutex selgesõnaliselt vabastab, või lõpetab. Seega, et olla kindel, et jagatud andmetega töötab korraga ainult üks lõim, peaksid kõik kohad, kus selline töö toimub, olema ümbritsetud paariga: WaitFor - ReleaseMutex :

KÄSITSEMINE g_hMutex ;

// Mutexi käepide salvestatakse globaalsesse muutujasse. Loomulikult tuleb see eelnevalt luua, enne töölõimede algust. Oletame, et seda on juba tehtud.

int ma ootan = :: WaitForSingleObject ( g_hMutex , LÕPMATU );

lüliti( ma ootan ) {

juhtum WAIT_OBJECT_0 : // Kõik on korras

murda;

juhtum WAIT_ABANDONED : /* Mõni lõim lõppes, unustades helistada ReleaseMutexile. Tõenäoliselt tähendab see teie programmis viga! Seetõttu sisestame igaks juhuks siia ASSERT-i, kuid lõplikus versioonis (väljalaskes) loeme selle koodi õnnestunuks. */

KINNITA ( vale );

murda;

vaikimisi:

// Vigade käsitlemine peaks olema siin.

// Mutexiga kaitstud koodijupp.

Töötle CommonData ();

KINNITA (:: ReleaseMutex ( g_hMutex ));

Miks on mutex parem kui automaatse lähtestamise sündmus? Ülaltoodud näites võiks seda ka kasutada, ainult ReleaseMutex tuleks asendada SetEventiga. Siiski võivad tekkida järgmised raskused. Enamasti tuleb jagatud andmetega töötada mitmes kohas. Mis juhtub, kui ProcessCommonData meie näites kutsub välja funktsiooni, mis töötab samade andmetega ja millel on juba oma paar WaitFor - ReleaseMutex (praktikas on see väga levinud)? Kui me kasutaksime sündmust, siis programm ilmselt ripuks, sest kaitstud ploki sees on sündmus neutraalses olekus. Muteks on keerulisem. Pealõime jaoks jääb see alati signaalimisolekusse, kuigi kõigi teiste lõimede puhul on see neutraalses olekus. Seega, kui lõim on omandanud mutexi, ei blokeeri funktsiooni WaitFor uuesti kutsumine. Lisaks on mutexisse sisse ehitatud ka loendur, nii et ReleaseMutexi tuleb kutsuda sama palju kordi, kui oli WaitFori kõnesid. Seega saame turvaliselt kaitsta iga jagatud andmetega töötavat kooditükki WaitFor - ReleaseMute x paariga, muretsemata, et seda koodi saab rekursiivselt välja kutsuda. See muudab Mutexi kasutamise väga lihtsaks tööriistaks.

Semafor

Veelgi spetsiifilisem sünkroniseerimisobjekt. Pean tunnistama, et minu praktikas pole veel juhtunud, et sellest kasu oleks olnud. Semafor on loodud piirama maksimaalset lõimede arvu, mis võivad ressursiga samal ajal töötada. Sisuliselt on semafor loenduriga sündmus. Kuni see loendur on suurem kui null, on semafor signaalimisolekus. Kuid iga WaitFor-i kõne vähendab seda loendurit ühe võrra, kuni see muutub nulliks ja semafor läheb neutraalsesse olekusse. Nagu mutexil, on ka semaforil funktsioon ReleaseSemaphor, mis suurendab loendurit. Erinevalt mutexist ei ole semafor aga lõimega seotud ja WaitFor/ReleaseSemaphori uuesti kutsumine vähendab/suurendab loendurit.

Kuidas saab semafori kasutada? Näiteks saab seda kasutada mitme keermestamise kunstlikuks piiramiseks. Nagu ma juba mainisin, võib liiga palju samaaegselt aktiivseid lõime sagedaste kontekstilülituste tõttu kogu süsteemi jõudlust märgatavalt halvendada. Ja kui peaksime looma liiga palju töölõime, saame piirata samaaegselt aktiivsete lõimede arvu protsessorite arvu järjekorras.

Mida veel saab öelda tuuma sünkroonimisobjektide kohta? Väga mugav on neile nimesid panna. Kõigil funktsioonidel, mis loovad sünkroonimisobjekte, on vastav parameeter: CreateEvent , CreateMutex , CreateSemaphore . Kui kutsute näiteks funktsiooni CreateEvent kaks korda, määrates mõlemal korral sama mittetühja nime, siis teisel korral tagastab funktsioon uue objekti loomise asemel olemasoleva käepideme. See juhtub isegi siis, kui teine ​​kõne tehti teisest protsessist. Viimane on väga mugav juhtudel, kui soovite sünkroonida erinevatesse protsessidesse kuuluvaid lõime.

Kui te enam sünkroonimisobjekti ei vaja, ärge unustage kutsuda funktsiooni CloseHandle, mida lõimedest rääkides varem mainisin. Tegelikult ei pruugi see objekti kohe kustutada. Asi on selles, et objektil võib olla mitu käepidet ja siis see kustutatakse alles siis, kui viimane on suletud.

Ma tahan teile seda meelde tuletada Parim viis tagamaks, et CloseHandle'i või sarnase "puhastus" funktsiooni kutsutakse kindlasti välja isegi ebanormaalse olukorra korral, on selle panemine hävitajasse. Muide, seda kirjeldas kunagi hästi ja väga üksikasjalikult Kirill Pleshivtsevi artikkel “Nutikas hävitaja”. Ülaltoodud näidetes ei kasutanud ma seda tehnikat ainult hariduslikel eesmärkidel, nii et API funktsioonide töö oli visuaalsem. Päriskoodis peaksite puhastamiseks alati kasutama nutikate hävitajatega ümbrisklasse.

Muide, funktsiooniga ReleaseMutex jms tekib pidevalt sama probleem, mis CloseHandle . See tuleb ühisandmetega töö lõpus välja kutsuda, olenemata sellest, kui edukalt see töö lõpetati (võib ju erandi teha). "Unustuse" tagajärjed on siin tõsisemad. Kui CloseHandle'i ei kutsuta, lekib see ainult ressursse (mis on samuti halb!), siis avaldamata mutex takistab teistel lõimedel jagatud ressursiga töötamast kuni ebaõnnestunud lõime lõpetamiseni, mis tõenäoliselt ei võimalda rakendusel normaalselt töötada. Selle vältimiseks on meid jällegi abiks spetsiaalse väljaõppega klass nutika hävitajaga.

Sünkroonimisobjektide ülevaate lõpetades tahaksin mainida objekti, mida Win32 API-s pole. Paljud mu kolleegid imestavad, miks Win32-l pole spetsiaalset objekti "üks kirjutab, palju loeb". Omamoodi "täiustatud mutex", mis tagab, et ainult üks lõim pääseb samaaegselt juurde kirjutamiseks jagatud andmetele ja mitu lõime saavad korraga lugeda. Sarnase objekti võib leida ka UNIX-is "ah. Mõned teegid, näiteks Borlandilt, pakuvad selle emuleerimist standardsete sünkroonimisobjektide põhjal. Selliste emulatsioonide tegelik kasu on aga väga kaheldav. Sellist objekti saab tõhusalt rakendada alles operatsioonisüsteemi tuuma tase, kuid Windowsi kernel sellist objekti ei paku.

Miks Windows NT kerneli arendajad selle eest ei hoolitsenud? Miks me oleme UNIXist hullemad? Minu arvates on vastus, et Windowsi jaoks pole lihtsalt veel reaalset vajadust sellise objekti järele olnud. Tavalises üheprotsessorilises masinas, kus lõimed veel füüsiliselt ei saa samaaegselt töötada, on see praktiliselt samaväärne mutexiga. Mitme protsessoriga masinas saab sellest kasu, lubades lugeja lõimedel paralleelselt töötada. Samal ajal muutub see kasu käegakatsutavaks alles siis, kui lugemislõngade "kokkupõrke" tõenäosus on suur. Kahtlemata on näiteks 1024 protsessoriga masinal selline kerneli objekt eluliselt vajalik. Sarnased masinad on olemas, kuid need on spetsiaalsed süsteemid, mis kasutavad spetsiaalseid operatsioonisüsteeme. Tihti on sellised operatsioonisüsteemid ehitatud UNIX-i baasil, ilmselt sealt sattus selle süsteemi enamkasutatavatesse versioonidesse selline objekt nagu “üks kirjutab, paljud loevad”. Kuid x86 masinatele, millega oleme harjunud, paigaldatakse reeglina ainult üks ja ainult aeg-ajalt kaks protsessorit. Ja ainult kõige arenenumad protsessorite mudelid, nagu Intel Xeon, toetavad 4 või isegi enamat protsessori konfiguratsiooni, kuid sellised süsteemid jäävad siiski eksootilisteks. Kuid isegi sellisel "täiustatud" süsteemil võib "täiustatud mutex" anda märgatava jõudluse kasvu ainult väga spetsiifilistes olukordades.

Seega pole "täiustatud" mutexi rakendamine lihtsalt vaeva väärt. "Madala protsessoriga" masinal võib see olla isegi vähem efektiivne objekti loogika keerukuse tõttu võrreldes tavalise mutexiga. Pange tähele, et sellise objekti rakendamine pole nii lihtne, kui esmapilgul võib tunduda. Ebaõnnestunud juurutamise korral, kui lugemislõime on liiga palju, ei jõua kirjutuslõng lihtsalt andmeteni. Nendel põhjustel ei soovita ma ka proovida sellist objekti jäljendada. Päris masinate reaalsetes rakendustes saab tavaline mutex või kriitiline jaotis (mida arutatakse artikli järgmises osas) suurepäraselt hakkama jagatud andmetele juurdepääsu sünkroonimise ülesandega. Kuigi ma arvan, et Windows OS-i arenedes ilmub varem või hiljem kerneli objekt "üks kirjutab, palju loeb".

Märge. Tegelikult on Windows NT-s objekt "üks kirjutab – paljud loevad" endiselt olemas. Ma lihtsalt ei teadnud sellest artiklit kirjutades. Seda objekti nimetatakse "kerneli ressurssideks" ja see pole kasutajarežiimi programmidele juurdepääsetav, mistõttu pole see tõenäoliselt hästi tuntud. Sarnasusi selle kohta võib leida DDK-st. Aitäh Konstantin Manurinile, et ta mulle sellele tähelepanu juhtis.

Ummik

Nüüd pöördume tagasi funktsiooni WaitForMultipleObjects, täpsemalt selle kolmanda parameetri bWaitAll juurde. Lubasin teile rääkida, miks on nii oluline võimalus oodata mitut objekti korraga.

Miks on vaja funktsiooni ühe mitmest objektist ootamiseks, on arusaadav. Spetsiaalse funktsiooni puudumisel saaks seda teha, välja arvatud tühjas tsüklis olevate objektide oleku järjestikuse kontrollimisega, mis on muidugi vastuvõetamatu. Kuid vajadus spetsiaalse funktsiooni järele, mis võimaldab teil oodata hetke, mil mitu objekti korraga signaali olekusse läheb, pole nii ilmne. Tõepoolest, kujutage ette järgmist tüüpilist olukorda: teatud hetkel vajab meie lõim juurdepääsu korraga kahele jagatud andmete komplektile, millest igaüks vastutab oma mutexi eest, nimetagem neid A-ks ja B-ks. Näib, et lõim suudab esmalt oodake, kuni mutex A vabastatakse, jäädvustage see , seejärel oodake, kuni mutex B vabastatakse... Tundub, et saame hakkama paari kõnega WaitForSingleObject . Tõepoolest, see toimib, kuid ainult seni, kuni kõik teised lõimed omandavad mutexid samas järjekorras: kõigepealt A, seejärel B. Mis juhtub, kui teatud lõime üritab teha vastupidist: kõigepealt omandab B, seejärel A? Varem või hiljem tekib olukord, kus üks lõime on haaranud mutexi A, teine ​​B, esimene ootab B vabastamist, teine ​​A. Selge on see, et nad ei oota seda kunagi ja programm jääb hanguma.

Selline tupik on väga levinud viga. Nagu kõik sünkroonimisega seotud vead, ilmneb see ainult aeg-ajalt ja võib programmeerija jaoks palju närve rikkuda. Samal ajal on peaaegu iga skeem, mis hõlmab mitut sünkroonimisobjekti, täis ummikseisu. Seetõttu tuleks sellise vooluringi kavandamise etapis sellele probleemile pöörata erilist tähelepanu.

Toodud lihtsas näites on blokeerimist üsna lihtne vältida. On vaja nõuda, et kõik lõimed omandaksid mutexid kindlas järjekorras: esmalt A, siis B. Keerulises programmis, kus on aga palju üksteisega erineval viisil seotud objekte, pole seda tavaliselt nii lihtne saavutada. Lukku saab kaasata mitte kaks, vaid palju objekte ja niite. Seetõttu kõige rohkem usaldusväärne viis Vältimaks ummikseisu olukorras, kus lõim vajab korraga mitut sünkroonimisobjekti, tuleb need kõik lüüa ühe väljakutsega funktsioonile WaitForMultipleObjects parameetriga bWaitAll=TRUE. Tõtt-öelda nihutame sel juhul ummikseisu probleemi lihtsalt operatsioonisüsteemi tuumale, kuid peaasi, et see pole enam meie mure. Kui aga keerukas programmis, kus on palju objekte, ei ole alati võimalik kohe öelda, milline neist on konkreetse toimingu tegemiseks vajalik, ei ole sageli lihtne kõiki WaitFori kõnesid ühte kohta tuua ja ka kombineerida.

Seega on ummikseisu vältimiseks kaks võimalust. Peate kas tagama, et sünkroonimisobjektid jäädvustatakse lõimedega alati täpselt samas järjekorras või et need püütakse kinni ühe WaitForMultipleObjectsi kutsega. Viimane meetod on lihtsam ja eelistatum. Praktikas tekib aga mõlema nõude täitmisel pidevalt raskusi, mõlemat lähenemist on vaja kombineerida. Keeruliste ajastusahelate kavandamine on sageli väga mittetriviaalne ülesanne.

Sünkroonimise näide

Enamikes tüüpilistes olukordades, nagu need, mida ma eespool kirjeldasin, ei ole sünkroonimise korraldamine keeruline, piisab sündmusest või mutexist. Kuid perioodiliselt on keerulisemaid juhtumeid, kus probleemi lahendus pole nii ilmne. Tahaksin seda illustreerida konkreetse näitega oma praktikast. Nagu näete, osutus lahendus üllatavalt lihtsaks, kuid enne selle leidmist pidin proovima mitut ebaõnnestunud võimalust.

Seega ülesanne. Peaaegu kõigil kaasaegsetel allalaadimishalduritel või lihtsalt "kiiktoolidel" on võimalus liiklust piirata nii, et taustal töötav "kiiktool" ei segaks oluliselt kasutaja veebis surfamist. Arendasin sarnast programmi ja mulle anti ülesandeks just selline “funktsioon” juurutada. Minu kiiktool töötas klassikalise multithreading skeemi järgi, kui iga ülesandega, antud juhul konkreetse faili allalaadimisega, tegeleb eraldi lõime. Liikluspiirang oleks pidanud olema kõikide voogude kumulatiivne. See tähendab, et oli vaja tagada, et antud ajaintervalli jooksul loeksid kõik vood oma pesadest mitte rohkem kui teatud arv baite. Selle limiidi lihtsalt võrdseks jagamine voogude vahel on ilmselt ebaefektiivne, kuna failide allalaadimine võib olla väga ebaühtlane, üks laadib alla kiiresti, teine ​​aeglaselt. Seetõttu vajame kõigi lõimede jaoks ühist loendurit, kui palju baite on loetud ja kui palju saab veel lugeda. Siin on sünkroonimine kasulik. Ülesande täiendava keerukuse andis nõue, et mis tahes töötaja lõime võib igal ajal peatada.

Sõnastame probleemi üksikasjalikumalt. Otsustasin sünkroniseerimissüsteemi lisada eriklassi. Siin on selle liides:

klass CQuota {

avalik: // meetodid

tühine seatud ( allkirjastamata int _nKvoot );

allkirjastamata int Taotlus ( allkirjastamata int _nBytesToRead , HANDLE_hStopEvent );

tühine Vabasta ( allkirjastamata int _nBytesRevert , HANDLE_hStopEvent );

Aeg-ajalt, näiteks kord sekundis, kutsub juhtlõng välja määramismeetodi, määrates allalaadimiskvoodi. Enne kui töötaja lõim võrgust saadud andmeid loeb, kutsub see välja meetodi Request, mis kontrollib, et praegune kvoot poleks null, ja kui jah, siis tagastab baitide arvu, mida saab lugeda praegusest kvoodist vähem. Selle numbri võrra vähendatakse kvooti vastavalt. Kui taotluse väljakutsumisel on kvoot null, peab kutsuv lõim ootama, kuni kvoot on saadaval. Mõnikord juhtub, et tegelikult laekub vähem baite, kui nõutud, ja sel juhul tagastab lõim osa talle Release meetodiga eraldatud kvoodist. Ja nagu ma ütlesin, saab kasutaja igal ajal anda käsu allalaadimise lõpetamiseks. Sel juhul tuleb ootamine katkestada, olenemata kvoodi olemasolust. Selleks kasutatakse spetsiaalset sündmust: _hStopEvent. Kuna ülesandeid saab käivitada ja peatada iseseisvalt, on igal töötaja lõimel oma peatamissündmus. Selle käepide edastatakse meetoditele Request and Release.

Ühes ebaõnnestunud valikus proovisin kasutada kombinatsiooni mutexist, mis sünkroonib juurdepääsu klassile CQuota, ja sündmusest, mis annab märku kvoodi olemasolust. Peatusüritus aga sellesse skeemi ei mahu. Kui lõim soovib omandada kvooti, ​​peab selle ooteolekut juhtima kompleksne tõeväärtusavaldis: ((mutex AND quota event) VÕI peata sündmus). Aga WaitForMultipleObjects seda ei luba, mitu kerneli objekti saab kombineerida kas AND- või VÕI-operatsiooniga, kuid mitte segada. Kui proovite ooteaega jagada kahe järjestikuse WaitForMultipleObjectsi kõnega, tekib paratamatult ummikseisu. Üldiselt osutus see tee tupiktee.

Ma ei lase enam udu sisse ja ütlen teile lahenduse. Nagu ma ütlesin, on mutex väga sarnane automaatse lähtestamise sündmusega. Ja siin on meil just see haruldane juhtum, kui seda on mugavam kasutada, kuid mitte üks, vaid kaks korraga:

klass CQuota {

privaatne: // andmed

allkirjastamata int m_nKvoot ;

CEvent m_eventHasQuota ;

CEvent m_eventNoQuota ;

Korraga saab määrata ainult ühe neist sündmustest. Iga kvoodiga manipuleeriv lõim peab määrama esimese sündmuse, kui järelejäänud kvoot ei ole null, ja teise sündmuse, kui kvoot on ammendatud. Lõim, mis soovib kvooti saada, peab ootama esimest sündmust. Kvooti suurendav lõim peab ainult mõnda neist sündmustest ootama, sest kui mõlemad on lähtestatud olekus, tähendab see, et kvoodiga töötab praegu mõni teine ​​lõim. Seega täidavad kaks sündmust korraga kahte funktsiooni: andmete juurdepääsu sünkroonimine ja ootamine. Lõpuks, kuna lõim ootab ühte kahest sündmusest, on sündmus, mis annab märku peatumisest, hõlpsasti kaasata.

Toon näite Request meetodi rakendamisest. Ülejäänud rakendatakse sarnasel viisil. Lihtsustasin veidi reaalses projektis kasutatud koodi:

allkirjastamata int CQuota :: Taotlus ( allkirjastamata int _nTaotlus , HANDLE_hStopEvent )

kui(! _nTaotlus ) tagasi 0 ;

allkirjastamata int nPaku = 0 ;

KÄSITLEMA sündmusi [ 2 ];

Sündmused [ 0 ] = _hStopEvent ; // Peatussündmusel on kõrgem prioriteet. Panime selle esikohale.

Sündmused [ 1 ] = m_eventHasQuota ;

int iWaitResult = :: WaitForMultipleObjects ( 2 , Sündmused , VALE , LÕPMATU );

lüliti( iWaitResult ) {

juhtum WAIT_FAILED :

// VIGA

viska uus CWin32 erand ;

juhtum WAIT_OBJECT_0 :

// Peatage sündmus. Käsitlesin seda kohandatud erandiga, kuid miski ei takista mind seda muul viisil rakendamast.

viska uus CStopException ;

juhtum WAIT_OBJECT_0 + 1 :

// Sündmus "kvoot saadaval"

KINNITA ( m_nKvoot ); // Kui signaali andis see sündmus, aga kvooti tegelikult pole, siis kuskil tegime vea. Peab viga otsima!

kui( _nTaotlus >= m_nKvoot ) {

nPaku = m_nKvoot ;

m_nKvoot = 0 ;

m_eventNoQuota . seatud ();

muidu {

nPaku = _nTaotlus ;

m_nKvoot -= _nTaotlus ;

m_eventHasQuota . seatud ();

murda;

tagasi nPaku ;

Väike märkus. MFC teeki selles projektis ei kasutatud, kuid nagu te ilmselt juba arvasite, tegin ma oma CEvent klassi, ümbrise "sündmus" tuumaobjekti ümber, sarnaselt MFC "schnoyga. Nagu ma ütlesin, sellised lihtsad ümbrisklassid on väga kasulikud, kui on mõni ressurss (antud juhul kerneli objekt), mis tuleb meeles pidada, et see tuleb töö lõpus vabastada. Ülejäänud puhul pole vahet, kas kirjutate SetEvent(m_hEvent) või m_event.Set( ).

Loodan, et see näide aitab teil koostada oma ajastusskeemi, kui teil tekib mittetriviaalne olukord. Peaasi on oma skeemi võimalikult hoolikalt analüüsida. Kas võib olla olukord, kus see ei tööta korralikult, eelkõige võib tekkida blokeerimine? Selliste vigade tabamine siluris on tavaliselt lootusetu ettevõtmine, siin aitab vaid detailne analüüs.

Nii et oleme kaalunud hädavajalik tööriist lõime sünkroonimine: kerneli sünkroonimisobjektid. See on võimas ja mitmekülgne tööriist. Selle abil saate koostada isegi väga keerulisi sünkroonimisskeeme. Õnneks tuleb selliseid mittetriviaalseid olukordi ette harva. Lisaks on mitmekülgsus alati jõudluse hinnaga. Seetõttu tasub paljudel juhtudel kasutada teisi Windowsis saadaolevaid lõime sünkroonimisfunktsioone, näiteks kriitilisi sektsioone ja aatomioperatsioone. Need ei ole nii universaalsed, kuid on lihtsad ja tõhusad. Nendest räägime järgmises osas.

Protsess on mällu laaditud programmi eksemplar. See eksemplar võib luua lõime, mis on käivitatavate juhiste jada. Oluline on mõista, et ei tööta mitte protsessid, vaid lõimed.

Lisaks on igal protsessil vähemalt üks lõime. Seda lõime nimetatakse rakenduse peamiseks (peamiseks) lõimeks.

Kuna lõime on peaaegu alati palju rohkem, kui nende täitmiseks on füüsilisi protsessoreid, siis tegelikult ei täideta lõime mitte üheaegselt, vaid kordamööda (protsessori aja jaotus toimub täpselt lõimede vahel). Kuid nende vahel vahetamine toimub nii sageli, et tundub, nagu töötaksid need paralleelselt.

Olenevalt olukorrast võivad lõimed olla kolmes olekus. Esiteks saab lõime käivitada, kui sellele on antud CPU aeg, st. see võib olla aktiivne. Teiseks võib see olla passiivne ja oodata protsessori eraldamist, st. olla valmisolekus. Ja on veel kolmas, samuti väga oluline tingimus- luku olek. Kui lõime on blokeeritud, ei eraldata sellele üldse aega. Tavaliselt pannakse lukk mõnda sündmust oodates. Kui see sündmus toimub, viiakse lõim automaatselt blokeeritud olekust valmisolekusse. Näiteks kui üks lõim teeb arvutusi, samal ajal kui teine ​​peab ootama tulemuste kettale salvestamist. Teine võiks kasutada tsüklit nagu "while(!isCalcFinished) Jätka;", kuid praktikas on lihtne näha, et protsessor on selle tsükli töötamise ajal 100% hõivatud (seda nimetatakse aktiivseks ootamiseks). Võimaluse korral tuleks vältida selliseid silmuseid, milles lukustusmehhanism pakub hindamatut abi. Teine lõim võib end blokeerida, kuni esimene lõim määrab sündmuse, mis annab märku lugemise lõppemisest.

Lõime sünkroonimine Windows OS-is

Windows rakendab ennetavat multitegumtöötlust, mis tähendab, et süsteem võib igal ajal katkestada ühe lõime täitmise ja anda juhtimise üle teisele. Varem kasutati Windows 3.1-s organiseerimismeetodit, mida kutsuti kooperatiivseks multitegumtööks: süsteem ootas, kuni lõim ise kontrolli talle üle annab ja seetõttu tuli ühe rakenduse hangumise korral arvuti taaskäivitada.

Kõik samasse protsessi kuuluvad lõimed jagavad ühiseid ressursse, nagu RAM-i aadressiruum või avatud failid. Need ressursid kuuluvad kogu protsessi ja seega iga selle lõime juurde. Seetõttu saab iga lõim nende ressurssidega ilma piiranguteta töötada. Aga... Kui üks lõime ei ole veel ühegi jagatud ressursiga töötamist lõpetanud ja süsteem on sama ressurssi kasutavale teisele lõimele üle läinud, siis võib nende lõimede töö tulemus olla kavandatust äärmiselt erinev. Sellised konfliktid võivad tekkida ka erinevatesse protsessidesse kuuluvate lõimede vahel. See probleem ilmneb alati, kui kaks või enam lõime kasutavad mõnda jagatud ressurssi.

Näide. Lõimede sünkroonimisest väljas: kui peatate kuvamise lõime ajutiselt (peatate), jätkab taustamassiivi täitmise lõime töötamist.

#kaasa #kaasa int a; HANDLE hThr; allkirjastamata pikk 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; }

Seetõttu on vaja mehhanismi, mis võimaldaks lõimedel oma tööd jagatud ressurssidega kooskõlastada. Seda mehhanismi nimetatakse lõime sünkroniseerimismehhanismiks.

See mehhanism on operatsioonisüsteemi objektide kogum, mida loob ja haldab tarkvara, mis on ühised kõikidele süsteemi lõimedele (mõnda jagavad samasse protsessi kuuluvad lõimed) ja mida kasutatakse ressurssidele juurdepääsu koordineerimiseks. Ressursid võivad olla kõik, mida saab jagada kahe või enama lõimega – kettal olev fail, port, andmebaasi kirje, GDI objekt ja isegi globaalne programmimuutuja (millele pääseb ligi samasse protsessi kuuluvatest lõimedest).

Sünkroonimisobjekte on mitu, millest olulisemad on mutex, kriitiline sektsioon, sündmus ja semafor. Igaüks neist objektidest rakendab oma sünkroonimismeetodit. Samuti saab protsesse ja lõime endid kasutada sünkroonimisobjektidena (kui üks lõim ootab teise lõime või protsessi valmimist); samuti failid, sideseadmed, konsooli sisend ja muudatuste teatised.

Iga sünkroniseerimisobjekt võib olla nn signaalitud olekus. Iga objektitüübi puhul on sellel olekul erinev tähendus. Lõimed saavad kontrollida objekti hetkeolekut ja/või oodata selle oleku muutumist ning seeläbi oma tegevusi koordineerida. See tagab, et kui lõim töötab sünkroonimisobjektidega (loob neid, muudab olekut), ei katkesta süsteem selle täitmist enne, kui see toiming on lõpule viidud. Seega on kõik lõpptoimingud sünkroonimisobjektidega aatomilised (jagamatud.

Sünkroonimisobjektidega töötamine

Ühe või teise sünkroonimisobjekti loomiseks kutsutakse välja spetsiaalne WinAPI funktsioon Loo... tüüpi (nt CreateMutex). See kõne tagastab objektikäepideme (HANDLE), mida saavad kasutada kõik antud protsessi kuuluvad lõimed. Sünkroonimisobjektile on võimalik ligi pääseda mõnest teisest protsessist, kas pärides objekti käepidet või eelistatult kutsudes välja objekti funktsiooni Open.... Pärast seda kõnet saab protsess käepideme, mida saab hiljem kasutada objektiga töötamiseks. Objektile tuleb anda nimi, välja arvatud juhul, kui see on ette nähtud kasutamiseks ühe protsessi raames. Kõikide objektide nimed peavad olema erinevad (isegi kui need on erinevat tüüpi). Näiteks ei saa luua sama nimega sündmust ja semafori.

Objekti saadaoleva deskriptori järgi saate määrata selle hetkeoleku. Seda tehakse nn. ootel olevad funktsioonid. Kõige sagedamini kasutatav funktsioon on WaitForSingleObject. See funktsioon võtab kaks parameetrit, millest esimene on objekti käepide, teine ​​on ajalõpp ms-des. Funktsioon tagastab WAIT_OBJECT_0, kui objekt on signaalitud olekus, WAIT_TIMEOUT, kui aeg on aegunud, ja WAIT_ABANDONED, kui mutexi ei vabastatud enne omava lõime lõpetamist. Kui ajalõpp on määratud nulliks, naaseb funktsioon kohe, vastasel juhul ootab see määratud aja. Kui objekti olek antakse märku enne selle aja möödumist, tagastab funktsioon WAIT_OBJECT_0, vastasel juhul tagastab funktsioon WAIT_TIMEOUT. Kui kellaajaks on määratud sümboolne konstant LÕPETUS, siis funktsioon ootab lõputult, kuni objekti olek saab signaali.

On väga oluline, et ootefunktsiooni väljakutse blokeeriks praeguse lõime, st. kui lõim on jõude, ei eraldata sellele protsessori aega.

Kriitilised lõigud

Objektikriitiline jaotis aitab programmeerijal isoleerida koodiosa, kus lõim pääseb juurde jagatud ressursile, ja takistab ressursi samaaegset kasutamist. Enne ressursi kasutamist siseneb lõime kriitilisse sektsiooni (kutsub funktsiooni EnterCriticalSection). Kui mõni teine ​​lõim proovib seejärel siseneda samasse kriitilisse sektsiooni, peatub selle täitmine, kuni esimene lõim lahkub jaotisest, kutsudes välja LeaveCriticalSection. Kasutatakse ainult niitide jaoks ühes protsessis. Kriitilise sektsiooni sisenemise järjekord pole määratletud.

Samuti on olemas funktsioon TryEnterCriticalSection, mis kontrollib, kas kriitiline sektsioon on praegu hõivatud. Selle abiga ei saa ressursile juurdepääsu ootavat lõime blokeerida, vaid teha mõned kasulikud toimingud.

Näide. Lõimede sünkroonimine kriitiliste sektsioonide abil.

#kaasa #kaasa CRITICAL_SECTION cs; int a; HANDLE hThr; allkirjastamata pikk 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; }

Vastastikune välistamine

Vastastikuse välistamise objektid (mutexid, mutex - MUTual EXclusionist) võimaldavad teil koordineerida jagatud ressursile juurdepääsu vastastikust välistamist. Objekti signaalitud olek (st "seadistatud" olek) vastab ajahetkele, mil objekt ei kuulu ühtegi lõime ja seda saab "püüda". Ja vastupidi, "reset" (mittesignaliseeritud) olek vastab hetkele, mil mõnele lõimele see objekt juba kuulub. Juurdepääs objektile antakse siis, kui objekti omav lõim selle vabastab.

Kaks (või enam) lõime saavad luua sama nimega Mutexi, kutsudes esile funktsiooni CreateMutex. Esimene lõime loob tegelikult mutexi ja järgmised lõimed saavad käepideme juba olemasolevale objektile. See võimaldab mitmel lõimel omandada sama mutexi käepideme, vabastades programmeerija vajadusest muretseda selle pärast, kes tegelikult mutexi loob. Kui seda lähenemisviisi kasutatakse, on soovitav määrata lipu bInitialOwner väärtuseks FALSE, vastasel juhul on mutexi tegeliku looja kindlaksmääramisel raskusi.

Mitu lõime võivad omandada sama mutexi käepideme, muutes protsessidevahelise suhtluse võimalikuks. Selle lähenemisviisi jaoks saate kasutada järgmisi mehhanisme:

  • Funktsiooni CreateProcess abil loodud alamprotsess võib mutexi käepideme pärida, kui funktsiooni CreateMutex abil mutexi loomisel määrati parameeter lpMutexAttributes.
  • Lõim saab funktsiooni DuplicateHandle abil saada olemasoleva mutexi duplikaadi.
  • Funktsioonide OpenMutexi või CreateMutexi kutsumisel võib lõim määrata olemasoleva Mutexi nime.

Praegusele lõimele kuuluva mutexi deklareerimiseks tuleb kutsuda üks ootel olevatest funktsioonidest. Objekti omav lõim võib seda korduvalt "jäädvustada" nii palju kordi, kui talle meeldib (see ei põhjusta iselukustumist), kuid see peab selle funktsiooni ReleaseMutexi abil vabastama nii mitu korda.

Ühe protsessi lõimede sünkroonimiseks on tõhusam kasutada kriitilisi sektsioone.

Näide. Lõimede sünkroonimine mutexide abil.

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

Arengud

Sündmusobjekte kasutatakse ootavate lõimede teavitamiseks sündmuse toimumisest. Sündmusi on kahte tüüpi – käsitsi ja automaatse lähtestamisega. Käsitsi lähtestamise teostab funktsioon ResetEvent. Käsitsi lähtestamise sündmusi kasutatakse mitme lõime korraga teavitamiseks. Automaatse lähtestamise sündmuse kasutamisel saab teatise ja jätkab selle täitmist ainult üks ootel lõim, ülejäänud ootavad edasi.

Funktsioon CreateEvent loob sündmuse objekti, SetEvent - määrab sündmuse signaali olekusse, ResetEvent - lähtestab sündmuse. Funktsioon PulseEvent määrab sündmuse ja pärast seda sündmust ootavate lõimede jätkamist (kõik käsitsi lähtestamisega ja ainult üks automaatse lähtestamisega) lähtestab selle. Kui ühtegi lõime ei oota, lähtestab PulseEvent sündmuse lihtsalt.

Näide. Lõimede sünkroonimine sündmuste abil.

#kaasa #kaasa HANDLE hEvent1, hSündmus2; int a; HANDLE hThr; allkirjastamata pikk uThrID; void Thread(void* pParams) ( int i, num = 0; while (1) ( WaitForSingleObject(hEvent2, INFINITE); for (i=0; i<5; i++) a[i] = num; num++; SetEvent(hEvent1); } } int main(void) { hEvent1=CreateEvent(NULL, FALSE, TRUE, NULL); hEvent2=CreateEvent(NULL, FALSE, FALSE, NULL); hThr=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)Thread,NULL,0,&uThrID); while(1) { WaitForSingleObject(hEvent1, INFINITE); printf("%d %d %d %d %d\n", a, a, a, a, a); SetEvent(hEvent2); } return 0; }

semaforid

Semaforobjekt on tegelikult loenduriga mutex-objekt. See objekt laseb end teatud arvu lõimede poolt "kinni püüda". Pärast seda on "püüdmine" võimatu, kuni üks semafori varem "püütud" lõimedest selle vabastab. Semafoore kasutatakse ressursile samaaegselt juurde pääsevate lõimede arvu piiramiseks. Initsialiseerimisel kantakse objektile üle maksimaalne niitide arv, peale igat "püüdmist" semafori loendur väheneb. Signaali olek vastab loenduri väärtusele, mis on suurem kui null. Kui loendur on null, loetakse semafor väljalülitatuks (lähtestatuks).

Funktsioon CreateSemaphore loob semaforiobjekti, mis näitab selle maksimaalset võimalikku algväärtust, OpenSemaphore - tagastab käepideme olemasolevale semaforile, semafor jäädvustatakse ootefunktsioonide abil, samal ajal kui semafori väärtust vähendatakse ühe võrra, ReleaseSemaphore - vabastab semafori koos semafoori väärtuse suurenemine parameetri numbris määratud väärtuse võrra.

Näide. Lõimede sünkroniseerimine semaforide abil.

#kaasa #kaasa KÄEPIDE hSem; int a; HANDLE hThr; allkirjastamata pikk uThrID; void Thread(void* pParams) ( int i, num = 0; while (1) ( WaitForSingleObject(hSem, INFINITE); for (i=0; i<5; i++) a[i] = num; num++; ReleaseSemaphore(hSem, 1, NULL); } } int main(void) { hSem=CreateSemaphore(NULL, 1, 1, "MySemaphore1"); hThr=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)Thread,NULL,0,&uThrID); while(1) { WaitForSingleObject(hSem, INFINITE); printf("%d %d %d %d %d\n", a, a, a, a, a); ReleaseSemaphore(hSem, 1, NULL); } return 0; }

Kaitstud juurdepääs muutujatele

On mitmeid funktsioone, mis võimaldavad teil töötada kõigi lõimede globaalsete muutujatega, ilma sünkroonimise pärast muretsemata, sest. need funktsioonid hoolitsevad selle eest ise – nende täitmine on aatomiline. Need on funktsioonid InterlockedIncrement, InterlockedDecrement, InterlockedExchange, InterlockedExchangeAdd ja InterlockedCompareExchange. Näiteks suurendab funktsioon InterlockedIncrement 32-bitise muutuja väärtust ühe võrra, mis on kasulik erinevate loendurite puhul.

Täieliku teabe saamiseks kõigi WIN32 API funktsioonide eesmärgi, kasutamise ja süntaksi kohta peate kasutama MS SDK abisüsteemi, mis on osa Borland Delphi või CBuilder programmeerimiskeskkondadest, samuti MSDN-i, mida tarnitakse osana Visual C programmeerimissüsteem.


Programmide puhul, mis kasutavad mitut lõime või protsesse, peavad need kõik täitma neile määratud funktsioone soovitud järjekorras. Windows 9x keskkonnas on sel eesmärgil soovitatav kasutada mitmeid mehhanisme, mis tagavad lõimede sujuva töö. Neid mehhanisme nimetatakse sünkroniseerimismehhanismid. Oletame, et töötate välja programmi, milles kaks lõime töötavad paralleelselt. Iga lõim pääseb juurde ühele jagatud globaalsele muutujale. Üks lõim, iga kord, kui sellele muutujale juurde pääseb, suurendab seda ja teine ​​lõim vähendab seda. Lõimede samaaegsel asünkroonsel tööl tekib paratamatult järgmine olukord: - esimene lõim on lugenud globaalse muutuja väärtuse lokaalseks; - OS katkestab selle, kuna sellele eraldatud protsessori ajakvant on lõppenud, ja annab juhtimise üle teisele lõimele; - teine ​​lõime luges ka globaalse muutuja väärtuse lokaalseks, vähendas seda ja kirjutas uue väärtuse tagasi; - OS annab taas juhtimise üle esimesele lõimele, mis teise lõime toimingute kohta midagi teadmata suurendab oma kohalikku muutujat ja kirjutab selle väärtuse globaalsesse. Ilmselgelt lähevad teise lõime tehtud muudatused kaotsi. Selliste olukordade vältimiseks on vaja jagatud andmete kasutamine õigeaegselt eraldada. Sellistel juhtudel kasutatakse sünkroniseerimismehhanisme, mis tagavad mitme lõime õige töö. OS-i sünkroonimistööriistadWindows: 1) kriitiline lõik (Kriitilinejaotis) on objekt, mis kuulub protsessi, mitte kerneli. See tähendab, et see ei saa sünkroonida erinevate protsesside lõime. Samuti on olemas funktsioonid lähtestamiseks (loomiseks) ja kustutamiseks, kriitilisest jaotisest sisenemiseks ja sealt väljumiseks: loomine - InitializeCriticalSection(...), kustutamine - DeleteCriticalSection(...), sisestamine - EnterCriticalSection(...), väljumine - LeaveCriticalSection (...). Piirangud: kuna tegemist ei ole kerneli objektiga, ei ole see teistele protsessidele nähtav, st kaitsta saab ainult enda protsessi lõime. Kriitiline jaotis parsib spetsiaalse protsessimuutuja väärtust, mida kasutatakse lipuna, et vältida mitme lõime samaaegset koodiosa käivitamist. Sünkroonitavate objektide hulgas on kriitilised lõigud kõige lihtsamad. 2) mutexmuutuvvälistada. See on kerneli objekt, sellel on nimi, mis tähendab, et nende abil saab sünkroonida juurdepääsu mitme protsessi jagatud andmetele, täpsemalt erinevate protsesside lõimedest. Ükski teine ​​niit ei saa omandada mutexi, mis juba ühele lõimele kuulub. Kui mutex kaitseb mõningaid jagatud andmeid, saab see oma funktsiooni täita ainult siis, kui iga lõim kontrollib enne nendele andmetele juurde pääsemist selle mutexi olekut. Windows käsitleb mutexit jagatud objektina, millest saab signaali anda või lähtestada. Mutexi signaali olek näitab, et see on hõivatud. Lõimed peavad iseseisvalt analüüsima mutexide hetkeseisu. Kui soovite, et mutexile pääseksid juurde teiste protsesside lõimed, peate sellele andma nime. Funktsioonid: CreateMutex(name) – loomine, hnd=OpenMutex(name) – avamine, WaitForSingleObject(hnd) – ootamine ja hõivamine, ReleaseMutex(hnd) – vabastamine, CloseHandle(hnd) – sulgemine. Seda saab kasutada programmide taaskäivitamise eest kaitsmiseks. 3) semafor -semafor. Kerneli objekti "semafor" kasutatakse ressursside arvestuseks ja selle eesmärk on piirata samaaegset juurdepääsu ressursile mitme lõime kaudu. Semafoori abil saab programmi tööd korraldada nii, et ressursile pääseb korraga ligi mitu lõime, kuid nende lõimede arv on piiratud. Semafoori loomisel määratakse maksimaalne lõimede arv, mis võivad ressursiga samaaegselt töötada. Iga kord, kui programm pöördub semafoori poole, vähendatakse semafoori ressursiloendurit ühe võrra. Kui ressursi loenduri väärtus muutub nulliks, pole semafor saadaval. loo CreateSemaphore, ava OpenSemaphore, võta WaitForSingleObject, vabasta ReleaseSemaphore 4 ) sündmus -sündmus. Sündmused annavad tavaliselt lihtsalt teada mõne toimingu lõpust, need on ka kerneli objektid. Saate mitte ainult selgesõnaliselt vabastada, vaid on olemas ka sündmuste seadistamise toiming. Sündmused võivad olla käsitsi (käsitsi) ja üksikud (üksikud). Üksiküritus on pigem üldine lipp. Sündmus on signaalitud olekus, kui see on määratud mõne lõimega. Kui programm nõuab, et sündmuse korral reageeriks sellele ainult üks lõimedest, samal ajal kui kõik teised lõimed ootavad, siis kasutatakse ühte sündmust. Käsitsi toimuv sündmus ei ole lihtsalt mitme lõime ühine lipp. See täidab mõnevõrra keerukamaid funktsioone. Iga lõim saab selle sündmuse määrata või selle lähtestada (tühjendada). Kui sündmus on määratud, jääb see sellesse olekusse meelevaldselt pikaks ajaks, olenemata sellest, mitu lõime sündmuse määramist ootavad. Kui kõik seda sündmust ootavad lõimed saavad teate sündmuse toimumise kohta, lähtestatakse see automaatselt. Funktsioonid: SetEvent, ClearEvent, WaitForEvent. Sündmuste tüübid: 1) automaatse lähtestamise sündmus: WaitForSingleEvent. 2) käsitsi lähtestamisega sündmus (manuaalne), siis tuleb sündmus lähtestada: ReleaseEvent. Mõned teoreetikud tõstavad esile veel ühe sünkroonimisobjekti: WaitAbleTimer on OS-i kerneli objekt, mis lülitub iseseisvalt pärast määratud ajavahemikku vabasse olekusse (äratuskell).

Mõnikord muutub see vajalikuks mitme lõime või protsessiga töötades sünkroonida täitmist kaks või enam neist. Selle põhjuseks on enamasti see, et kaks või enam lõime võivad vajada juurdepääsu jagatud ressursile, mis tõesti ei saa pakkuda mitmele lõimele korraga. Jagatud ressurss on ressurss, millele pääseb juurde korraga mitu töötavat ülesannet.

Mehhanismi, mis tagab sünkroonimisprotsessi, nimetatakse juurdepääsupiirang. Vajadus selle järele tekib ka juhtudel, kui üks lõim ootab teise lõime genereeritud sündmust. Loomulikult peab olema mingi viis, kuidas esimene lõime peatada kuni sündmuse toimumiseni. Pärast seda peaks lõime täitmist jätkama.

Ülesanne võib olla kahes üldises olekus. Esiteks saab ülesanne läbi viia(või olge valmis käivitama niipea, kui sellel on juurdepääs protsessori ressurssidele). Teiseks võib ülesanne olla blokeeritud. Sel juhul peatatakse selle täitmine kuni vajaliku ressursi vabastamiseni või teatud sündmuse toimumiseni.

Windowsil on eriteenused, mis võimaldavad teatud viisil piirata juurdepääsu jagatud ressurssidele, sest ilma operatsioonisüsteemi abita ei saa eraldi protsess või lõim ise kindlaks teha, kas tal on ressursile ainujuurdepääs. Windowsi operatsioonisüsteem sisaldab protseduuri, mis ühe pideva toiminguga kontrollib ja võimalusel määrab ressursi juurdepääsu lipu. Operatsioonisüsteemi arendajate keeles nimetatakse sellist toimingut kontrollige ja installige toimimist. Kutsutakse välja lippe, mida kasutatakse sünkroonimise tagamiseks ja ressurssidele juurdepääsu kontrollimiseks semaforid(semafor). Win32 API toetab semafoore ja muid sünkroonimisobjekte. MFC teek sisaldab ka nende objektide tuge.

Sünkroonimisobjektid ja mfc klassid

Win32 liides toetab nelja tüüpi sünkroonimisobjekte, mis kõik põhinevad ühel või teisel viisil semafori kontseptsioonil.

Esimest tüüpi objektid on semafor ise või klassikaline (standardne) semafor. See võimaldab piiratud arvul protsessidel ja lõimedel juurdepääsu ühele ressursile. Sellisel juhul on juurdepääs ressursile kas täielikult piiratud (üks ja ainult üks lõim või protsess pääseb ressursile teatud aja jooksul) või saavad samaaegse juurdepääsu vaid vähesed lõimed ja protsessid. Semafore rakendatakse loenduriga, mis kahandab, kui ülesandele eraldatakse semafor, ja suurendab, kui ülesanne vabastab semafori.

Teist tüüpi sünkroonimisobjektid on eksklusiivne (mutex) semafor. Selle eesmärk on piirata täielikult juurdepääsu ressursile, nii et ainult üks protsess või lõim pääseb ressursile igal ajahetkel juurde. Tegelikult on see eriline semafor.

Kolmas sünkroonimisobjektide tüüp on sündmus, või sündmuse objekt. Seda kasutatakse ressursile juurdepääsu blokeerimiseks, kuni mõni muu protsess või lõim teatab, et ressurssi saab kasutada. Seega annab see objekt märku vajaliku sündmuse täitmisest.

Kasutades neljandat tüüpi sünkroonimisobjekti, on võimalik keelata programmi koodi teatud osade täitmine mitme lõimega samaaegselt. Selleks tuleb need maatükid deklareerida kui kriitiline lõik. Kui üks lõime siseneb sellesse sektsiooni, on teistel lõimedel keelatud seda teha, kuni esimene lõim sellest jaotisest väljub.

Erinevalt teist tüüpi sünkroonimisobjektidest kasutatakse kriitilisi sektsioone ainult lõimede sünkroonimiseks ühes protsessis. Protsessi lõimede sünkroonimiseks või protsesside sünkroonimiseks saab kasutada teist tüüpi objekte.

MFC-s toetatakse Win32 liidese pakutavat sünkroonimismehhanismi järgmiste klasside kaudu, mis on tuletatud klassist CSyncObject:

    CCriticalSection- rakendab kriitilist lõiku.

    CEüritus- rakendab sündmuse objekti

    CMutex- rakendab eksklusiivset semafori.

    CSemaphore- rakendab klassikalist semafori.

Lisaks nendele klassidele määratleb MFC ka kaks täiendavat sünkroonimisklassi: CSingleLock ja CMultiLock. Need kontrollivad juurdepääsu sünkroonimisobjektile ja sisaldavad meetodeid, mida kasutatakse selliste objektide lubamiseks ja vabastamiseks. Klass CSingleLock juhib juurdepääsu ühele sünkroonimisobjektile ja klassile CMultiLock- mitmele objektile. Järgnevalt käsitleme ainult klassi CSingleLock.

Kui luuakse mis tahes sünkroonimisobjekt, saab sellele juurdepääsu juhtida klassi abil CSingleLock. Selleks peate esmalt looma tüüpi objekti CSingleLock konstruktorit kasutades:

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

Esimene parameeter on kursor sünkroonimisobjektile, näiteks semaforile. Teise parameetri väärtus määrab, kas konstruktor peaks proovima antud objektile juurde pääseda. Kui see parameeter on nullist erinev, siis juurdepääs antakse, vastasel juhul juurdepääsu ei üritata. Kui juurdepääs on antud, siis lõime, mis lõi klassiobjekti CSingleLock, peatatakse seni, kuni meetod vabastab vastava sünkroonimisobjekti Avage lukustus klass CSingleLock.

Kui CSingleLock-tüüpi objekt on loodud, saab juurdepääsu parameetriga pObject osutatavale objektile juhtida kahe funktsiooni abil. lukk ja Avage lukustus klass CSingleLock.

meetod lukk on mõeldud juurdepääsuks objektile sünkroonimisobjektile. Seda kutsunud lõim peatatakse kuni meetodi lõpuleviimiseni, st kuni ressursi juurde pääsemiseni. Parameetri väärtus määrab, kui kaua funktsioon ootab, et saada juurdepääs vajalikule objektile. Iga kord, kui meetod edukalt lõpule jõuab, vähendatakse sünkroonimisobjektiga seotud loenduri väärtust ühe võrra.

meetod Avage lukustus vabastab sünkroonimisobjekti, võimaldades teistel lõimedel ressurssi kasutada. Meetodi esimeses variandis suurendatakse antud objektiga seotud loenduri väärtust ühe võrra. Teises valikus määrab esimene parameeter, kui palju seda väärtust tuleks suurendada. Teine parameeter osutab muutujale, millesse kirjutatakse loenduri eelmine väärtus.

Klassiga töötades CSingleLock Ressursile juurdepääsu kontrollimise üldine protseduur on järgmine:

    luua CSyncObj tüüpi objekt (näiteks semafor), mida kasutatakse ressursile juurdepääsu juhtimiseks;

    kasutades loodud sünkroonimisobjekti loo CSingleLock tüüpi objekt;

    ressursile juurdepääsu saamiseks helistage lukustusmeetodile;

    helistage ressursile;

    helistage ressursi vabastamiseks Unlock meetodile.

Järgnevalt kirjeldatakse semafooride ja sündmuseobjektide loomist ja kasutamist. Kui olete nendest mõistetest aru saanud, saate hõlpsasti õppida ja kasutada kahte teist tüüpi sünkroonimisobjekte: kriitilisi sektsioone ja mutexe.