Tråden står. Kritiska delar av att slutföra synkronisering i Windows

Detta synkroniseringsobjekt kan endast användas lokalt inom processen som skapade det. De återstående objekten kan användas för att synkronisera trådar av olika processer. Namnet på objektet "kritisk sektion" är associerat med ett abstrakt urval av en del av programkoden (sektionen) som utför vissa operationer, vars ordning inte kan överträdas. Det vill säga, ett försök av två olika trådar att samtidigt exekvera koden för detta avsnitt kommer att resultera i ett fel.

Det kan till exempel vara lämpligt att skydda skrivfunktioner med en sådan sektion, eftersom samtidig åtkomst av flera skribenter bör förhindras.

För det kritiska avsnittet introduceras två operationer:

gå in i avsnittet; Så länge som någon tråd är i den kritiska delen kommer alla andra trådar som försöker komma in i den automatiskt att stoppa och vänta. En tråd som redan har gått in i det här avsnittet kan gå in i den flera gånger utan att vänta på att den ska släppas.

lämna avsnittet; När en tråd lämnar en sektion minskas antalet gånger som tråden går in i sektionen, så att sektionen kommer att frigöras för andra trådar endast om tråden lämnar sektionen lika många gånger som den kom in i den. När ett kritiskt avsnitt släpps kommer bara en tråd att väckas och väntar på tillåtelse att komma in i det avsnittet.

Generellt sett, i andra icke-Win32 API:er (till exempel OS/2), behandlas den kritiska sektionen inte som ett synkroniseringsobjekt, utan som ett stycke programkod som bara kan exekveras av en applikationstråd. Det vill säga att inträde i en kritisk sektion betraktas som en tillfällig avstängning av gängomkopplingsmekanismen tills den lämnar denna sektion. I Win32 API behandlas kritiska sektioner som objekt, vilket leder till viss förvirring - de är mycket lika i sina egenskaper som icke namngivna exklusiva objekt ( mutex, se nedan).

När du använder kritiska sektioner måste du se till att för stora fragment av kod inte allokeras till sektionen, eftersom detta kan leda till betydande förseningar i exekveringen av andra trådar.

Till exempel, i förhållande till de redan diskuterade högarna, är det inte meningsfullt att skydda alla funktioner för att arbeta med högen med en kritisk sektion, eftersom läsarfunktioner kan utföras parallellt. Dessutom verkar användningen av en kritisk sektion även för att synkronisera skribenter faktiskt obekväm - eftersom för att synkronisera en skribent med läsare måste den senare fortfarande gå in i denna sektion, vilket praktiskt taget leder till att alla funktioner skyddas av en enda sektion.

Det finns flera fall av effektiv användning av kritiska avsnitt:

läsare kommer inte i konflikt med författare (endast författare behöver skyddas);

alla trådar har ungefär lika åtkomsträttigheter (det är till exempel omöjligt att särskilja rena författare och läsare);

vid konstruktion av sammansatta synkroniseringsobjekt, bestående av flera standardobjekt, för att skydda sekventiella operationer på ett sammansatt objekt.

I de tidigare delarna av artikeln pratade jag om generella principer och specifika metoder för att bygga flertrådade applikationer. Olika trådar kräver nästan alltid periodvis interaktion med varandra, och behovet av synkronisering uppstår oundvikligen. Idag ska vi titta på det viktigaste, mest kraftfulla och mest mångsidiga Windows-synkroniseringsverktyget: kärnsynkroniseringsobjekt.

WaitForMultipleObjects och andra väntefunktioner

Som du kommer ihåg, för att synkronisera trådar, behöver du vanligtvis tillfälligt avbryta exekveringen av en av trådarna. I detta fall måste det överföras med hjälp av medel operativ system till ett standbyläge där det inte tar upp någon CPU-tid. Vi känner redan till två funktioner som kan göra detta: SuspendThread och ResumeThread. Men som jag sa i föregående del av artikeln, på grund av vissa funktioner är dessa funktioner olämpliga för synkronisering.

Idag ska vi titta på en annan funktion, som också försätter tråden i ett vänteläge, men till skillnad från SuspendThread/ResumeThread är den speciellt utformad för att organisera synkronisering. Det här är WaitForMultipleObjects. Eftersom den här funktionen är mycket viktig kommer jag att avvika något från min regel om att inte gå in på API-detaljer och kommer att prata om det mer i detalj, även tillhandahålla dess prototyp:

DWORD WaitForMultipleObjects (

DWORD nCount , // antal objekt i lpHandles-arrayen

KONST HANDTAG * lpHandtag , // pekare till en array av kärnobjektsbeskrivningar

BOOL bWaitAll , // flagga som indikerar om det är nödvändigt att vänta på alla objekt eller bara ett räcker

DWORD dwMillisekunder // Paus

Huvudparametern för denna funktion är en pekare till en array av kärnobjekthandtag. Vi kommer att prata om vad dessa objekt är nedan. För nu är det viktigt för oss att veta att vilket som helst av dessa objekt kan vara i ett av två tillstånd: neutralt eller "signalerande" (signalerat tillstånd). Om bWaitAll-flaggan är FALSE kommer funktionen att returnera kontroll så snart minst ett av objekten ger en signal. Och om flaggan är TRUE kommer detta bara att hända när alla objekt börjar signalera på en gång (som vi kommer att se är detta den viktigaste egenskapen för denna funktion). I det första fallet kan du genom returvärdet ta reda på vilket av objekten som skickade signalen. Du måste subtrahera konstanten WAIT_OBJECT_0 från den, och du får ett index i lpHandles-arrayen. Om väntetiden överskrider den timeout som anges i den sista parametern, kommer funktionen att sluta vänta och returnera värdet WAIT_TIMEOUT . Du kan ange konstanten INFINITE som en timeout, och sedan väntar funktionen "hela vägen", eller vice versa, 0, och då kommer tråden inte att avbrytas alls. I det senare fallet kommer funktionen att returnera kontroll omedelbart, men från dess resultat kommer det att vara möjligt att ta reda på objektens tillstånd. Den sista tekniken används mycket ofta. Som du kan se har den här funktionen rika möjligheter. Det finns flera fler WaitForXXX-funktioner, men de är alla varianter på huvudtemat. Speciellt WaitForSingleObject är bara en förenklad version av det. Resten har var och en sin egen extra funktionalitet, men används i allmänhet mindre ofta. Till exempel gör de det möjligt att svara inte bara på signaler från kärnobjekt, utan även på att nya fönstermeddelanden kommer in i trådens kö. Som vanligt hittar du deras beskrivning, samt detaljerad information om WaitForMultipleObjects, på MSDN.

Låt oss nu prata om vad dessa mystiska "kärnobjekt" är. Låt oss börja med att dessa inkluderar själva trådarna och processerna. De går in i ett signaltillstånd omedelbart efter slutförandet. Detta är en mycket viktig funktion eftersom det ofta är nödvändigt att spåra när en tråd eller process avslutas. Låt till exempel vår serverapplikation med en uppsättning arbetartrådar avslutas. I det här fallet måste kontrolltråden på något sätt informera arbetartrådarna om att det är dags att avsluta arbetet (till exempel genom att sätta en global flagga), och sedan vänta tills alla trådar är klara, göra alla nödvändiga åtgärder för korrekt slutförande: frigöra resurser , informera kunder om att stänga av, stänga nätverksanslutningar, etc.

Det faktum att trådar slår på en signal när de avslutar arbetet gör det extremt enkelt att lösa problemet med synkronisering med trådavslutning:

// För enkelhetens skull, låt oss bara ha en arbetstråd. Låt oss lansera det:

HANTERA hWorkerThread = :: Skapa tråd (...);

// Innan vi avslutar arbetet måste vi på något sätt informera arbetartråden om att det är dags att ladda ner.

// Vänta tills tråden är klar:

DWORD dwWaitResult = :: WaitForSingleObject ( hWorkerThread , OÄNDLIG );

om( dwWaitResult != WAIT_OBJECT_0 ) { /* felhantering */ }

// Trådhandtaget kan stängas:

KONTROLLERA (:: Stäng Handtag ( hWorkerThread );

/* Om CloseHandle misslyckas och returnerar FALSE, gör jag inget undantag. För det första, även om detta hände på grund av ett systemfel skulle det inte få direkta konsekvenser för vårt program, för eftersom vi stänger handtaget betyder det att inget vidare arbete med det förväntas. I verkligheten kan ett misslyckande med CloseHandle bara betyda ett fel i ditt program. Därför kommer vi att infoga VERIFY-makrot här för att inte missa det i skedet av felsökning av applikationen. */

Koden som väntar på att processen ska slutföras kommer att se liknande ut.

Om det inte fanns någon sådan funktion inbyggd i systemet, skulle arbetartråden själv på något sätt behöva överföra information om dess slutförande till den huvudsakliga. Även om den gjorde detta som det sista, kunde huvudtråden inte vara säker på att arbetaren inte hade åtminstone ett par monteringsanvisningar kvar att utföra. I individuella situationer(till exempel om trådens kod finns i en DLL som måste laddas ur när den avslutas) kan detta få ödesdigra konsekvenser.

Jag skulle vilja påminna dig om att även efter att en tråd (eller process) har avslutats, förblir dess handtag fortfarande aktiva tills de explicit stängs med CloseHandle-funktionen. (Glöm förresten inte att göra detta!) Detta görs bara för att du ska kunna kontrollera trådens tillstånd när som helst.

Så, WaitForMultipleObjects-funktionen (och dess analoger) låter dig synkronisera exekveringen av en tråd med tillståndet för synkroniseringsobjekt, särskilt andra trådar och processer.

Kärna specialobjekt

Låt oss gå vidare och överväga kärnobjekt som är designade specifikt för synkronisering. Dessa är händelser, semaforer och mutexer. Låt oss kort titta på var och en av dem:

Händelse

Kanske det enklaste och mest grundläggande synkroniseringsobjektet. Detta är bara en flagga som kan ställas in med SetEvent/ResetEvent-funktionerna: signalering eller neutral. En händelse är det bekvämaste sättet att skicka en signal till en väntande tråd om att någon händelse har inträffat (det är därför det kallas så), och att det kan fortsätta att fungera. Med hjälp av en händelse kan vi enkelt lösa synkroniseringsproblemet när vi initierar en arbetstråd:

// För enkelhetens skull, låt händelsehandtaget lagras i en global variabel:

HANDLE g_hEventInitComplete = NULL ; // lämna aldrig en variabel oinitierad!

{ // kod i huvudtråden

// skapa en händelse

g_hEventInitComplete = :: Skapa händelse ( NULL,

FALSK , // vi kommer att prata om denna parameter senare

FALSK , // initialtillstånd - neutral

om(! g_hEventInitComplete ) { /* glöm inte felhantering */ }

// skapa en arbetstråd

DWORD idWorkerThread = 0 ;

HANTERA hWorkerThread = :: Skapa tråd ( NULL , 0 , & WorkerThreadProc , NULL , 0 , & idWorkerThread );

om(! hWorkerThread ) { /* felhantering */ }

// vänta på en signal från arbetartråden

DWORD dwWaitResult = :: WaitForSingleObject ( g_hEventInitComplete , OÄNDLIG );

om( dwWaitResult != WAIT_OBJECT_0 ) { /* fel */ }

// nu kan du vara säker på att arbetstråden har slutfört initieringen.

KONTROLLERA (:: Stäng Handtag ( g_hEventInitComplete )); // glöm inte att stänga onödiga föremål

g_hEventInitComplete = NULL ;

// arbetstrådsfunktion

DWORD WINAPI WorkerThreadProc ( LPVOID_parameter )

InitializeWorker (); // initiering

// signalerar att initieringen är klar

BOOL är ok = :: SetEvent ( g_hEventInitComplete );

om(! är OK ) { /* fel */ }

Det bör noteras att det finns två markant olika typer av händelser. Vi kan välja en av dem med den andra parametern i CreateEvent-funktionen. Om det är TRUE skapas en händelse vars tillstånd endast styrs manuellt, det vill säga av SetEvent/ResetEvent-funktionerna. Om den är FALSK kommer en automatisk återställningshändelse att genereras. Detta innebär att så snart en tråd som väntar på en given händelse släpps av en signal från den händelsen, kommer den automatiskt att återställas till ett neutralt tillstånd. Deras skillnad är mest uttalad i en situation där flera trådar väntar på en händelse samtidigt. En manuell händelse är som en startpistol. När den väl är inställd på signaleringsläget kommer alla trådar att släppas på en gång. En automatisk återställningshändelse liknar en vändkors för tunnelbanan: den släpper bara ett flöde och återgår till ett neutralt tillstånd.

Mutex

Jämfört med en händelse är det ett mer specialiserat objekt. Det används vanligtvis för att lösa ett vanligt synkroniseringsproblem som att komma åt en resurs som delas av flera trådar. På många sätt liknar det en automatisk återställningshändelse. Den största skillnaden är att den har en speciell bindning till en specifik tråd. Om en mutex är i ett signalerat tillstånd betyder det att det är gratis och inte tillhör någon tråd. Så fort en viss tråd har väntat på denna mutex, återställs den senare till ett neutralt tillstånd (här är det precis som en auto-reset-händelse), och tråden blir dess ägare tills den explicit släpper mutexen med ReleaseMutex-funktionen, eller så upphör det. För att vara säker på att endast en tråd arbetar med delad data åt gången, bör du omge alla platser där sådant arbete sker med paret: WaitFor - ReleaseMutex :

HANDTAG g_hMutex ;

// Låt mutex-handtaget lagras i en global variabel. Naturligtvis måste det skapas i förväg, innan du startar arbetartrådar. Låt oss anta att detta redan har gjorts.

int jag väntar = :: WaitForSingleObject ( g_hMutex , OÄNDLIG );

växla( jag väntar ) {

fall WAIT_OBJECT_0 : // Allt är bra

ha sönder;

fall WAIT_ABANDONED : /* Någon tråd avslutades, glömde att ringa ReleaseMutex. Troligtvis betyder detta att det finns en bugg i ditt program! Därför, för säkerhets skull, kommer vi att infoga ASSERT här, men i den slutliga versionen (release) kommer vi att betrakta denna kod som framgångsrik. */

HÄVDA ( falsk );

ha sönder;

standard:

// Det borde finnas felhantering här.

// Sektion av kod skyddad av en mutex.

ProcessCommonData ();

KONTROLLERA (:: ReleaseMutex ( g_hMutex ));

Varför är en mutex bättre än en automatisk återställningshändelse? I exemplet ovan kan det också användas, bara ReleaseMutex skulle behöva ersättas med SetEvent . Följande svårighet kan dock uppstå. Oftast måste man arbeta med delad data på flera ställen. Vad händer om ProcessCommonData i vårt exempel anropar en funktion som fungerar med samma data och som redan har sitt eget WaitFor - ReleaseMutex-par (i praktiken händer det ganska ofta)? Om vi ​​använde händelsen skulle programmet uppenbarligen hänga, eftersom inuti det skyddade blocket händelsen är i neutralt tillstånd. En mutex är smartare designad. För värdtråden förblir den alltid i signaleringsläge, trots att den för alla andra trådar är i neutral. Därför, om en tråd har fått ett mutex, kommer det inte att blockeras att anropa WaitFor-funktionen igen. Dessutom har mutex också en räknare inbyggd i sig, så ReleaseMutex måste anropas lika många gånger som WaitFor anropades. På så sätt kan vi säkert skydda varje kod som fungerar på delad data med ett WaitFor - ReleaseMute x-par, utan att oroa oss för att koden anropas rekursivt. Detta gör mutex till ett mycket enkelt verktyg att använda.

Semafor

Ett ännu mer specifikt synkroniseringsobjekt. Jag måste erkänna att det i min praktik aldrig har funnits ett fall där det skulle ha varit användbart. En semafor är utformad för att begränsa det maximala antalet trådar som samtidigt kan arbeta med en viss resurs. I huvudsak är en semafor en händelse med en räknare. Så länge som denna räknare är större än noll är semaforen i signaleringstillstånd. Varje anrop till WaitFor minskar dock denna räknare med ett tills den blir noll och semaforen går in i ett neutralt tillstånd. Liksom en mutex har en semafor en ReleaseSemaphor-funktion som ökar räknaren. Men till skillnad från en mutex, är en semafor inte bunden till en tråd, och att anropa WaitFor/ReleaseSemaphor igen kommer att minska/öka räknaren igen.

Hur kan du använda en semafor? Den kan till exempel användas för att på konstgjord väg begränsa flertrådning. Som jag redan diskuterat kan för många samtidigt aktiva trådar avsevärt försämra prestandan för hela systemet på grund av frekventa kontextväxlingar. Och om vi var tvungna att skapa för många arbetstrådar, kan vi begränsa antalet samtidigt aktiva trådar till ett antal i storleksordningen av antalet processorer.

Vad mer kan du säga om kärnsynkroniseringsobjekt? Det är väldigt bekvämt att kunna ge dem namn. Alla funktioner som skapar synkroniseringsobjekt har en motsvarande parameter: CreateEvent, CreateMutex, CreateSemaphore. Om du till exempel anropar CreateEvent två gånger, båda gångerna anger samma icke-tomma namn, kommer funktionen andra gången, istället för att skapa ett nytt objekt, att returnera handtaget för ett befintligt. Detta kommer att hända även om det andra samtalet gjordes från en annan process. Det senare är mycket praktiskt i de fall du behöver synkronisera trådar som tillhör olika processer.

När du inte längre behöver synkroniseringsobjektet, glöm inte att anropa CloseHandle-funktionen som jag redan nämnde när jag pratade om trådar. Faktum är att det inte nödvändigtvis tar bort objektet direkt. Faktum är att ett objekt kan ha flera handtag, då kommer det att raderas först när det sista stängs.

Jag vill påminna dig om det Det bästa sättet För att säkerställa att CloseHandle eller en liknande "cleanup"-funktion anropas, även i händelse av en nödsituation, är att placera den i en destruktor. Förresten, detta beskrevs en gång ganska bra och i detalj i Kirill Pleshivtsevs artikel "Smart Destructor". I exemplen ovan använde jag inte denna teknik enbart i utbildningssyfte, så att funktionen för API-funktionerna blev tydligare. I riktig kod bör du alltid använda omslagsklasser med smarta förstörare för rensning.

Förresten, samma problem uppstår alltid med ReleaseMutex-funktionen och liknande som med CloseHandle . Det måste påkallas efter avslutat arbete med allmänna data, oavsett hur framgångsrikt detta arbete har slutförts (trots allt kunde ett undantag ha kastats). Konsekvenserna av "glömska" är allvarligare här. Om att inte anropa CloseHandle bara kommer att resultera i en resursläcka (vilket också är dåligt!), så kommer en outgiven mutex inte att tillåta andra trådar att arbeta med den delade resursen förrän den misslyckade tråden avslutas, vilket med största sannolikhet inte kommer att tillåta applikationen att fungera i vanliga fall. För att undvika detta kommer en specialutbildad klass med en smart destruktor återigen att hjälpa oss.

Som avslutning på granskningen av synkroniseringsobjekt skulle jag vilja nämna ett objekt som inte finns i Win32 API. Många av mina kollegor uttrycker förvirring över varför Win32 inte har ett specialiserat objekt av typen "man skriver, många läser". Detta är en sorts "avancerad mutex" som säkerställer att endast en tråd kan komma åt gemensamma data för skrivning åt gången, och flera trådar kan komma åt den endast för läsning. Ett liknande objekt kan hittas i UNIX. Vissa bibliotek, till exempel från Borland, erbjuder att emulera det baserat på standardsynkroniseringsobjekt. Den verkliga fördelen med sådana emuleringar är dock mycket tveksam. Ett sådant objekt kan effektivt implementeras endast vid drift systemkärnnivå. Men i Windows tillhandahåller inte kärnan ett sådant objekt.

Varför tog inte Windows NT kärnutvecklarna hand om detta? Varför är vi sämre än UNIX? Enligt min mening är svaret att det helt enkelt inte har funnits ett verkligt behov av ett sådant objekt på Windows ännu. På en vanlig maskin med en processor, där trådar fysiskt inte kan fungera samtidigt hur som helst, blir det nästan likvärdigt med en mutex. På en multiprocessormaskin kan den ge fördelar på grund av att den låter lästrådar fungera parallellt. Samtidigt kommer denna vinst faktiskt att bli märkbar endast när sannolikheten för en "kollision" av lästrådar är hög. Det råder ingen tvekan om att, till exempel, på en 1024-processormaskin kommer ett sådant kärnobjekt att vara avgörande. Liknande maskiner finns, men dessa är specialiserade system som körs under specialiserade OS. Ofta är sådana operativsystem byggda på basis av UNIX, troligen därifrån ett objekt av typen "man skriver, många läser" hittat sin väg in i mer vanliga versioner av detta system. Men på de x86-maskiner som vi är vana vid installeras som regel bara en och bara ibland två processorer. Och bara de mest avancerade modellerna av processorer som Intel Xeon stödjer 4 eller ännu fler processorkonfigurationer, men sådana system är fortfarande exotiska. Men även på ett sådant "avancerat" system kan en "avancerad mutex" ge en märkbar prestandavinst endast i mycket specifika situationer.

Att implementera en "avancerad" mutex är helt enkelt inte värt besväret. På en "lågprocessor" maskin kan den vara ännu mindre effektiv på grund av komplexiteten i objektets logik jämfört med en standardmutex. Observera att implementeringen av ett sådant objekt inte är så enkelt som det kan verka vid första anblicken. Om implementeringen misslyckas, om det finns för många lästrådar, kommer skrivtråden helt enkelt "inte komma igenom" till data. Av dessa skäl rekommenderar jag inte heller att du försöker efterlikna ett sådant objekt. I verkliga applikationer på riktiga maskiner kommer en vanlig mutex eller kritisk sektion (som kommer att diskuteras i nästa del av artikeln) att göra ett utmärkt jobb med att synkronisera åtkomst till delad data. Även om jag tror att med utvecklingen av Windows OS kommer kärnobjektet "man skriver många lästa" att dyka upp förr eller senare.

Notera. Faktum är att objektet "man skriver, många läser" finns fortfarande i Windows NT. Jag visste bara inte om det när jag skrev den här artikeln. Detta objekt kallas "kärnresurser" och är inte tillgängligt för program i användarläge, vilket förmodligen är anledningen till att det inte är särskilt känt. Liknande information om det finns i DDK. Tack till Konstantin Manurin för att du påpekade detta för mig.

Dödläge

Låt oss nu återgå till WaitForMultipleObjects-funktionen, mer exakt till dess tredje parameter, bWaitAll. Jag lovade att berätta varför möjligheten att vänta på flera objekt samtidigt är så viktig.

Det är tydligt varför det behövs en funktion som låter dig vänta på ett av flera objekt. I avsaknad av en speciell funktion kunde detta endast göras genom att sekventiellt kontrollera objektens tillstånd i en tom slinga, vilket naturligtvis är oacceptabelt. Men behovet av en speciell funktion som låter dig vänta till det ögonblick när flera objekt går in i signaltillståndet på en gång är inte så uppenbart. Låt oss faktiskt föreställa oss följande typiska situation: vår tråd behöver vid ett visst tillfälle tillgång till två uppsättningar gemensamma data samtidigt, som var och en är ansvarig för sin egen mutex, låt oss kalla dem A och B. Det verkar som att tråden kan vänta först tills mutex A är ledig och ta tag i det, vänta sedan på att mutex B ska släppas... Det verkar som att vi klarar oss med ett par samtal till WaitForSingleObject . Detta kommer faktiskt att fungera, men bara så länge som alla andra trådar skaffar mutexer i samma ordning: först A, sedan B. Vad händer om en tråd försöker göra det motsatta: först förvärva B, sedan A? Förr eller senare kommer en situation att uppstå när en tråd har fångat mutex A, en annan B, den första väntar på att B ska bli ledig och den andra A. Det är klart att de aldrig kommer att vänta på detta och programmet kommer att frysa.

Denna typ av dödläge är ett mycket vanligt fel. Liksom alla fel i samband med synkronisering, dyker det bara upp då och då och kan förstöra en programmerares nerver. Samtidigt är nästan alla scheman som involverar flera synkroniseringsobjekt fyllda av dödläge. Därför bör detta problem ges Särskild uppmärksamhet vid designstadiet av ett sådant system.

I det enkla exemplet ovan är det ganska enkelt att undvika blockering. Det är nödvändigt att kräva att alla trådar skaffar mutexer i en viss ordning: först A, sedan B. Men i ett komplext program där det finns många objekt kopplade till varandra på olika sätt är detta vanligtvis inte så lätt att uppnå. Inte två, men många föremål och trådar kan vara inblandade i ett lås. Därför mest pålitligt sätt För att undvika dödläge i en situation där en tråd behöver flera synkroniseringsobjekt samtidigt är att ta tag i dem alla med ett anrop till WaitForMultipleObjects-funktionen med parametern bWaitAll=TRUE. I sanning, i det här fallet flyttar vi bara problemet med dödlägen till operativsystemets kärna, men huvudsaken är att detta inte längre kommer att vara vårt bekymmer. Men i ett komplext program med många objekt, när det inte alltid är möjligt att omedelbart säga vilka av dem som kommer att behövas för att utföra en viss operation, är det ofta inte lätt att samla alla WaitFor-samtal på ett ställe och kombinera dem.

Det finns alltså två sätt att undvika dödläge. Du måste antingen se till att synkroniseringsobjekt alltid förvärvas av trådar i exakt samma ordning, eller att de förvärvas med ett enda anrop till WaitForMultipleObjects . Den senare metoden är enklare och att föredra. Men i praktiken uppstår ständigt svårigheter att uppfylla båda kraven, och det är nödvändigt att kombinera båda dessa tillvägagångssätt. Att designa komplexa tidskretsar är ofta en mycket utmanande uppgift.

Exempel på synkroniseringsorganisation

I de flesta typiska situationer, som de jag beskrev ovan, är det inte svårt att organisera synkronisering, det räcker med en händelse eller en mutex. Men då och då finns det mer komplexa fall där lösningen på problemet inte är så uppenbar. Jag skulle vilja illustrera detta med ett specifikt exempel från min praktik. Som du kommer att se var lösningen förvånansvärt enkel, men jag var tvungen att prova flera misslyckade alternativ innan jag hittade den.

Alltså uppgiften. Nästan alla moderna nedladdningshanterare, eller enkelt uttryckt "gungstolar", har förmågan att begränsa trafiken så att "gungstolen" som körs i bakgrunden inte stör användarens surfande på Internet. Jag utvecklade ett liknande program, och jag fick i uppdrag att implementera just en sådan "funktion". Min nedladdare fungerade enligt det klassiska multithreading-schemat, när varje uppgift, i detta fall att ladda ner en specifik fil, hanteras av en separat tråd. Trafikgränsen borde ha varit kumulativ för alla flöden. Det vill säga, det var nödvändigt att säkerställa att alla trådar under ett givet tidsintervall inte läste mer än ett visst antal byte från sina uttag. Att helt enkelt dela denna gräns lika mellan strömmar kommer uppenbarligen att vara ineffektivt, eftersom nedladdning av filer kan vara väldigt ojämn, den ena laddas ner snabbt, den andra långsamt. Därför behöver vi en gemensam räknare för alla trådar, hur många byte som har lästs och hur många fler som kan läsas. Det är här synkronisering är oumbärlig. Kravet på att vilken som helst av arbetartrådarna skulle kunna stoppas när som helst gjorde uppgiften mer komplex.

Låt oss formulera problemet mer detaljerat. Jag bestämde mig för att bifoga synkroniseringssystemet i en specialklass. Här är dess gränssnitt:

klass CQuota {

offentlig: // metoder

tomhet Uppsättning ( osignerad int _nKvot );

osignerad int Begäran ( osignerad int _nBytesToRead , HANDLE_hStopEvent );

tomhet Släpp ( osignerad int _nBytesRevert , HANDLE_hStopEvent );

Med jämna mellanrum, säg en gång per sekund, anropar kontrolltråden Set-metoden och ställer in nedladdningskvoten. Innan en arbetstråd läser data som tas emot från nätverket anropar den Request-metoden, som kontrollerar att den aktuella kvoten inte är noll, och i så fall returnerar antalet byte som kan läsas inom den aktuella kvoten. Kvoten minskas på motsvarande sätt med detta antal. Om kvoten är noll när Request anropas måste anropstråden vänta tills den dyker upp. Ibland händer det att färre byte faktiskt tas emot än vad som begärdes, i vilket fall tråden returnerar en del av den kvot som tilldelats den med hjälp av Releasemetoden. Och, som jag redan sa, kan användaren när som helst ge kommandot att sluta ladda ner. I detta fall måste väntan avbrytas oavsett förekomsten av en kvot. En speciell händelse används för detta: _hStopEvent. Eftersom uppgifter kan startas och stoppas oberoende av varandra, har varje arbetstråd sin egen stopphändelse. Dess handtag skickas till metoderna Request and Release.

I ett av de misslyckade alternativen försökte jag använda en kombination av en mutex som synkroniserar åtkomst till klassen CQuota och en händelse som signalerar närvaron av en kvot. Stopphändelsen passar dock inte in i detta schema. Om en tråd vill erhålla en kvot, måste dess vänteläge kontrolleras av ett komplext booleskt uttryck: ((mutex OCH kvothändelse) OR stop-händelse). Men WaitForMultipleObjects tillåter inte detta, du kan kombinera flera kärnobjekt antingen med en OCH- eller ELLER-operation, men inte blandade. Att försöka dela en väntetid med två på varandra följande anrop till WaitForMultipleObjects resulterar oundvikligen i ett dödläge. I allmänhet visade sig denna väg vara en återvändsgränd.

Jag kommer inte att skapa mer dimma och berätta lösningen. Som jag sa tidigare, är en mutex mycket lik en automatisk återställningshändelse. Och här har vi just det sällsynta fallet när det är bekvämare att använda det, men inte bara en, utan två på en gång:

klass CQuota {

privat: // data

osignerad int m_nQuota ;

CEvent m_eventHasQuota ;

CEvent m_eventNoQuota ;

Endast en av dessa händelser kan ställas in åt gången. Varje tråd som manipulerar en kvot måste ta upp en första händelse om den återstående kvoten inte är noll, och en andra händelse om kvoten är slut. En tråd som vill få en kvot måste vänta på den första händelsen. Tråden som ökar kvoten behöver bara vänta på någon av dessa händelser, eftersom om båda är i återställningstillstånd betyder det att en annan tråd för närvarande arbetar med kvoten. Sålunda utför två händelser två funktioner samtidigt: synkronisering av dataåtkomst och väntan. Slutligen, eftersom tråden väntar på en av två händelser, kan händelsen som signalerar att den stoppas lätt inkluderas.

Låt mig ge dig ett exempel på implementeringen av Request-metoden. Resten genomförs på liknande sätt. Jag har lite förenklat koden som används i själva projektet:

osignerad int CQuota :: Begäran ( osignerad int _nBegäran , HANDLE_hStopEvent )

om(! _nBegäran ) lämna tillbaka 0 ;

osignerad int nGe = 0 ;

HANTERA hEvents [ 2 ];

hEvents [ 0 ] = _hStopEvent ; // Stop-händelsen har högre prioritet. Låt oss sätta honom först.

hEvents [ 1 ] = m_eventHasQuota ;

int iWaitResult = :: WaitForMultipleObjects ( 2 , hEvents , FALSK , OÄNDLIG );

växla( iWaitResult ) {

fall WAIT_FAILED :

// FEL

kasta nytt CWin32Undantag ;

fall WAIT_OBJECT_0 :

// Stoppa händelse. Jag hanterade det med ett speciellt undantag, men ingenting hindrar mig från att implementera det på annat sätt.

kasta nytt CStopException ;

fall WAIT_OBJECT_0 + 1 :

// Event "kvot tillgänglig"

HÄVDA ( m_nQuota ); // Om den här händelsen gav en signal, men det finns faktiskt ingen kvot, så har vi gjort ett misstag någonstans. Vi måste leta efter en bugg!

om( _nBegäran >= m_nQuota ) {

nGe = m_nQuota ;

m_nQuota = 0 ;

m_eventNoQuota . Uppsättning ();

annan {

nGe = _nBegäran ;

m_nQuota -= _nBegäran ;

m_eventHasQuota . Uppsättning ();

ha sönder;

lämna tillbaka nGe ;

En liten notis. MFC-biblioteket användes inte i det projektet, men, som du säkert redan gissat, gjorde jag min egen CEvent-klass, en wrapper runt kärnhändelseobjektet, liknande MFC. Som sagt, sådana enkla wrapper-klasser är mycket användbara när du har en viss resurs (i det här fallet ett kärnobjekt) som måste komma ihåg att släppas när arbetet är slutfört. Annars är det ingen skillnad om man ska skriva SetEvent(m_hEvent) eller m_event.Set().

Jag hoppas att det här exemplet hjälper dig att designa din egen synkroniseringskrets om du stöter på en icke-trivial situation. Det viktigaste är att analysera ditt schema så noggrant som möjligt. Kan det finnas en situation där det kanske inte fungerar korrekt, i synnerhet kan det uppstå en blockering? Att fånga sådana fel i en debugger är vanligtvis en hopplös uppgift, bara detaljerad analys hjälper här.

Så vi har tittat på det viktigaste medlet trådsynkronisering: kärnsynkroniseringsobjekt. Detta är ett kraftfullt och mångsidigt verktyg. Med dess hjälp kan du bygga även mycket komplexa synkroniseringsscheman. Lyckligtvis inträffar sådana icke-triviala situationer sällan. Dessutom kommer mångsidighet alltid på bekostnad av prestanda. Därför är det i många fall värt att använda andra trådsynkroniseringsfunktioner som finns tillgängliga i Windows, såsom kritiska sektioner och atomoperationer. De är inte så universella, men enkla och effektiva. Vi kommer att prata om dem i nästa del.

En process är en instans av ett program som laddas in i minnet. Den här instansen kan skapa trådar, som är en sekvens av instruktioner att köra. Det är viktigt att förstå att det inte är processer som körs, utan snarare trådar.

Dessutom har varje process minst en tråd. Denna tråd kallas applikationens huvudtråd (huvudtråd).

Eftersom det nästan alltid finns många fler trådar än det finns fysiska processorer för att exekvera dem, exekveras trådarna faktiskt inte samtidigt, utan i sin tur (processortiden fördelas mellan trådarna). Men att byta mellan dem händer så ofta att de verkar gå parallellt.

Beroende på situationen kan trådar vara i tre tillstånd. För det första kan en tråd exekveras när den tilldelas CPU-tid, dvs. det kan vara i ett tillstånd av aktivitet. För det andra kan den vara inaktiv och väntar på att processorn ska tilldelas, dvs. vara i ett tillstånd av beredskap. Och det finns en tredje, också mycket viktigt tillstånd- blockerande tillstånd. När en tråd är blockerad tilldelas den ingen tid alls. Vanligtvis placeras ett block i väntan på någon händelse. När denna händelse inträffar flyttas tråden automatiskt från blockerat tillstånd. Till exempel, om en tråd utför beräkningar och den andra måste vänta på resultaten för att spara dem på disk. Den andra skulle kunna använda en loop som "while(!isCalcFinished) continue;", men det är lätt att verifiera i praktiken att under exekveringen av denna loop är processorn 100% upptagen (detta kallas aktiv väntan). Sådana cykler bör undvikas om möjligt, där låsmekanismen ger ovärderlig hjälp. Den andra tråden kan blockera sig själv tills den första tråden tar upp en händelse som indikerar att läsningen är klar.

Synkronisera trådar i Windows OS

Windows implementerar förebyggande multitasking - detta innebär att systemet när som helst kan avbryta exekveringen av en tråd och överföra kontrollen till en annan. Tidigare, i Windows 3.1, användes en organisationsmetod som kallas cooperativ multitasking: systemet väntade tills själva tråden överförde kontrollen till den, och det var därför, om en applikation frös, datorn måste startas om.

Alla trådar som hör till samma process delar några gemensamma resurser - som RAM-adressutrymme eller öppna filer. Dessa resurser tillhör hela processen och därför till var och en av dess trådar. Därför kan varje tråd arbeta med dessa resurser utan några begränsningar. Men... Om en tråd ännu inte har arbetat klart med någon delad resurs, och systemet byter till en annan tråd som använder samma resurs, så kan resultatet av arbetet med dessa trådar vara extremt annorlunda än vad som var tänkt. Sådana konflikter kan också uppstå mellan trådar som hör till olika processer. När två eller flera trådar delar någon delad resurs uppstår det här problemet.

Exempel. Trådar som inte är synkroniserade: Om du tillfälligt pausar utdatatråden (paus), kommer bakgrundsuppsättningstråden att fortsätta att köras.

#omfatta #omfatta int a; HANDTAG hThr; osignerat långt uThrID; void Thread(void* pParams) ( int i, num = 0; medan (1) ( för (i=0; i)<5; i++) a[i] = num; num++; } } int main(void) { hThr=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)Thread,NULL,0,&uThrID); while(1) printf("%d %d %d %d %d\n", a, a, a, a, a); return 0; }

Det är därför det behövs en mekanism för att tillåta trådar att samordna sitt arbete med delade resurser. Denna mekanism kallas trådsynkroniseringsmekanismen.

Denna mekanism är en uppsättning operativsystemobjekt som skapas och hanteras programmatiskt, är gemensamma för alla trådar i systemet (vissa delas av trådar som tillhör samma process) och används för att koordinera åtkomst till resurser. Resurser kan vara vad som helst som kan delas av två eller flera trådar - en diskfil, en port, en databaspost, ett GDI-objekt och till och med en global programvariabel (som kan nås av trådar som tillhör samma process).

Det finns flera synkroniseringsobjekt, av vilka de viktigaste är mutex, den kritiska sektionen, händelsen och semaforen. Vart och ett av dessa objekt implementerar sin egen synkroniseringsmetod. Dessutom kan själva processer och trådar användas som synkroniseringsobjekt (när en tråd väntar på att en annan tråd eller process ska slutföras); samt filer, kommunikationsenheter, konsolinmatning och ändringsmeddelanden.

Vilket synkroniseringsobjekt som helst kan vara i det så kallade signaltillståndet. För varje typ av objekt har detta tillstånd en annan betydelse. Trådar kan kontrollera det aktuella tillståndet för ett objekt och/eller vänta på en förändring i detta tillstånd och på så sätt koordinera sina åtgärder. Detta säkerställer att när en tråd arbetar med synkroniseringsobjekt (skapar dem, ändrar tillstånd), kommer systemet inte att avbryta dess exekvering förrän det slutför denna åtgärd. Således är alla slutliga operationer med synkroniseringsobjekt atomära (odelbara.

Arbeta med synkroniseringsobjekt

För att skapa ett eller annat synkroniseringsobjekt anropas en speciell WinAPI-funktion av typen Create... (till exempel CreateMutex). Detta anrop returnerar ett handtag till ett objekt (HANDLE) som kan användas av alla trådar som hör till denna process. Det är möjligt att komma åt ett synkroniseringsobjekt från en annan process - antingen genom att ärva ett handtag till detta objekt, eller helst genom att använda ett anrop till objektets öppningsfunktion (Öppna...). Efter detta samtal kommer processen att få ett handtag som senare kan användas för att arbeta med objektet. Ett objekt måste, om det inte är avsett att användas inom en enda process, ges ett namn. Namnen på alla objekt måste vara olika (även om de är av olika typ). Du kan till exempel inte skapa en händelse och en semafor med samma namn.

Med hjälp av den befintliga deskriptorn för ett objekt kan du bestämma dess nuvarande tillstånd. Detta görs med hjälp av den sk. väntande funktioner. Den vanligaste funktionen är WaitForSingleObject. Denna funktion tar två parametrar, den första är objekthandtaget, den andra är timeouten i ms. Funktionen returnerar WAIT_OBJECT_0 om objektet signaleras, WAIT_TIMEOUT om det tog timeout och WAIT_ABANDONED om mutex-objektet inte frigjordes innan dess ägande tråd avslutades. Om timeout anges som noll, returnerar funktionen resultatet omedelbart, annars väntar den på den angivna tiden. Om objektets tillstånd blir signal innan denna tid löper ut, kommer funktionen att returnera WAIT_OBJECT_0, annars returnerar funktionen WAIT_TIMEOUT. Om den symboliska konstanten INFINITE anges som tid, kommer funktionen att vänta på obestämd tid tills objektets tillstånd blir signal.

Ett mycket viktigt faktum är att anrop av en väntande funktion blockerar den aktuella tråden, d.v.s. Medan en tråd är i viloläge tilldelas den ingen CPU-tid.

Kritiska avsnitt

Ett kritiskt sektionsobjekt hjälper programmeraren att isolera kodavsnittet där en tråd kommer åt en delad resurs och förhindra samtidig användning av resursen. Innan du använder resursen går tråden in i den kritiska delen (anropar funktionen EnterCriticalSection). Om någon annan tråd sedan försöker komma in i samma kritiska avsnitt, kommer dess körning att pausas tills den första tråden lämnar avsnittet genom att anropa LeaveCriticalSection. Används endast för trådar i en process. Ordningen för inträde i den kritiska sektionen är inte definierad.

Det finns också en TryEnterCriticalSection-funktion som kontrollerar om den kritiska delen för närvarande är upptagen. Med sin hjälp kan tråden, medan den väntar på tillgång till en resurs, inte blockeras, men utföra några användbara åtgärder.

Exempel. Synkronisera trådar med kritiska avsnitt.

#omfatta #omfatta CRITICAL_SECTION cs; int a; HANDTAG hThr; osignerat långt uThrID; void Thread(void* pParams) ( int i, num = 0; medan (1) ( EnterCriticalSection(&cs); för (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; }

Ömsesidiga uteslutningar

Ömsesidiga exkluderingsobjekt (mutex, mutex - från MUTual EXclusion) låter dig koordinera den ömsesidiga uteslutningen av åtkomst till en delad resurs. Signaltillståndet för ett objekt (d.v.s. "inställt" tillstånd) motsvarar en tidpunkt då objektet inte tillhör någon tråd och kan "fångas". Omvänt motsvarar tillståndet "återställning" (icke-signal) det ögonblick då någon tråd redan äger detta objekt. Åtkomst till ett objekt ges när tråden som äger objektet släpper det.

Två (eller flera) trådar kan skapa en mutex med samma namn genom att anropa CreateMutex-funktionen. Den första tråden skapar faktiskt en mutex, och de nästa får ett handtag till ett redan existerande objekt. Detta gör att flera trådar kan få ett handtag till samma mutex, vilket gör att programmeraren inte behöver oroa sig för vem som faktiskt skapar mutexen. Om detta tillvägagångssätt används, är det lämpligt att ställa in bInitialOwner-flaggan till FALSE, annars kommer det att bli vissa svårigheter att avgöra vem som faktiskt skapade mutexet.

Flera trådar kan få ett handtag till samma mutex, vilket möjliggör kommunikation mellan processer. Följande mekanismer för detta tillvägagångssätt kan användas:

  • En underordnad process skapad med funktionen CreateProcess kan ärva ett mutex-handtag om parametern lpMutexAttributes angavs när mutexen skapades med CreateMutex-funktionen.
  • En tråd kan erhålla en dubblett av en befintlig mutex med DuplicateHandle-funktionen.
  • En tråd kan ange namnet på en befintlig mutex när du anropar funktionerna OpenMutex eller CreateMutex.

För att deklarera ett ömsesidigt undantag som tillhörande den aktuella tråden måste du anropa en av de väntande funktionerna. Tråden som äger objektet kan återhämta det så många gånger den vill (detta leder inte till självlåsning), men den måste släppa det lika många gånger med hjälp av ReleaseMutex-funktionen.

För att synkronisera trådarna i en process är det mer effektivt att använda kritiska avsnitt.

Exempel. Synkronisera trådar med mutexes.

#omfatta #omfatta HANDTAG hMutex; int a; HANDTAG hThr; osignerat långt uThrID; void Thread(void* pParams) ( int i, num = 0; while (1) ( WaitForSingleObject(hMutex, INFINITE); för (i=0; i<5; i++) a[i] = num; num++; ReleaseMutex(hMutex); } } int main(void) { hMutex=CreateMutex(NULL, FALSE, NULL); hThr=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)Thread,NULL,0,&uThrID); while(1) { WaitForSingleObject(hMutex, INFINITE); printf("%d %d %d %d %d\n", a, a, a, a, a); ReleaseMutex(hMutex); } return 0; }

evenemang

Händelseobjekt används för att meddela väntande trådar att en händelse har inträffat. Det finns två typer av händelser - med manuell och automatisk återställning. Manuell återställning utförs av funktionen ResetEvent. Manuell återställningshändelser används för att meddela flera trådar samtidigt. När du använder en automatisk återställningshändelse kommer endast en väntande tråd att ta emot meddelandet och fortsätta att köra, resten kommer att fortsätta att vänta.

CreateEvent-funktionen skapar ett händelseobjekt, SetEvent - ställer in händelsen till signaltillståndet, ResetEvent - återställer händelsen. PulseEvent-funktionen ställer in en händelse, och efter att trådarna som väntar på denna händelse återupptas (alla vid manuell återställning och endast en vid automatisk återställning), återställer den. Om det inte finns några väntande trådar återställer PulseEvent helt enkelt händelsen.

Exempel. Synkronisera trådar med hjälp av händelser.

#omfatta #omfatta HANTERA hEvent1, hEvent2; int a; HANDTAG hThr; osignerat långt uThrID; void Thread(void* pParams) ( int i, num = 0; while (1) ( WaitForSingleObject(hEvent2, INFINITE); för (i=0; i<5; i++) a[i] = num; num++; SetEvent(hEvent1); } } int main(void) { hEvent1=CreateEvent(NULL, FALSE, TRUE, NULL); hEvent2=CreateEvent(NULL, FALSE, FALSE, NULL); hThr=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)Thread,NULL,0,&uThrID); while(1) { WaitForSingleObject(hEvent1, INFINITE); printf("%d %d %d %d %d\n", a, a, a, a, a); SetEvent(hEvent2); } return 0; }

Semaforer

Ett semaforobjekt är egentligen ett mutex-objekt med en räknare. Detta objekt låter sig "fångas" av ett visst antal trådar. Efter detta kommer "fånga" att vara omöjligt tills en av trådarna som tidigare "fångade" semaforen släpper den. Semaforer används för att begränsa antalet trådar som samtidigt arbetar med en resurs. Det maximala antalet trådar överförs till objektet under initiering; efter varje "fångning" minskas semaforräknaren. Signaltillståndet motsvarar ett räknarvärde större än noll. När räknaren är noll anses semaforen inte vara installerad (återställ).

CreateSemaphore-funktionen skapar ett semaforobjekt som indikerar dess maximalt möjliga initiala värde, OpenSemaphore - returnerar en deskriptor av en befintlig semafor, semaforen fångas med hjälp av väntande funktioner och semaforvärdet minskas med en, ReleaseSemaphore - semaforen släpps med semaforen värde ökat med det värde som anges i parameternumret.

Exempel. Synkronisera trådar med semaforer.

#omfatta #omfatta HANDTAG hSem; int a; HANDTAG hThr; osignerat långt uThrID; void Thread(void* pParams) ( int i, num = 0; while (1) ( WaitForSingleObject(hSem, INFINITE); för (i=0; i<5; i++) a[i] = num; num++; ReleaseSemaphore(hSem, 1, NULL); } } int main(void) { hSem=CreateSemaphore(NULL, 1, 1, "MySemaphore1"); hThr=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)Thread,NULL,0,&uThrID); while(1) { WaitForSingleObject(hSem, INFINITE); printf("%d %d %d %d %d\n", a, a, a, a, a); ReleaseSemaphore(hSem, 1, NULL); } return 0; }

Skyddad tillgång till variabler

Det finns ett antal funktioner som låter dig arbeta med globala variabler från alla trådar utan att behöva oroa dig för synkronisering, eftersom dessa funktioner övervakar det själva - deras utförande är atomärt. Dessa funktioner är InterlockedIncrement, InterlockedDecrement, InterlockedExchange, InterlockedExchangeAdd och InterlockedCompareExchange. Till exempel ökar funktionen InterlockedIncrement atomiskt värdet på en 32-bitars variabel med en, vilket är bekvämt att använda för olika räknare.

För att få fullständig information om syftet, användningen och syntaxen för alla WIN32 API-funktioner måste du använda MS SDK-hjälpsystemet som ingår i Borland Delphi- eller CBuilder-programmeringsmiljöerna, samt MSDN, som tillhandahålls som en del av Visual C-programmeringssystemet.


För program som använder flera trådar eller processer är det nödvändigt att de alla utför sina tilldelade funktioner i den ordning som krävs. I Windows 9x-miljön föreslås det för detta ändamål att använda flera mekanismer för att säkerställa samordnad drift av trådar. Dessa mekanismer kallas synkroniseringsmekanismer. Anta att du utvecklar ett program där två trådar fungerar parallellt. Varje tråd har åtkomst till en delad global variabel. Varje gång man kommer åt denna variabel ökar en tråd den, och den andra tråden utför en minskning. När trådar fungerar samtidigt asynkront uppstår oundvikligen följande situation: - den första tråden läser värdet på en global variabel till en lokal; - OS avbryter det, eftersom processortidsdelen som tilldelats det har löpt ut, och överför kontrollen till den andra tråden; - den andra tråden övervägde också värdet av den globala variabeln till en lokal, minskade det och skrev tillbaka det nya värdet; - OS överför återigen kontrollen till den första tråden, som, utan att veta något om den andra trådens åtgärder, ökar sin lokala variabel och skriver dess värde till den globala. Uppenbarligen kommer ändringarna som gjorts av den andra tråden att gå förlorade. För att undvika sådana situationer är det nödvändigt att separera användningen av delad data över tid. I sådana fall används synkroniseringsmekanismer för att säkerställa korrekt drift av flera trådar. OS-synkroniseringsverktygWindows: 1) kritiskt avsnitt (KritiskSektion) är ett objekt som tillhör processen, inte kärnan. Detta innebär att den inte kan synkronisera trådar från olika processer. Det finns också funktioner för initiering (skapande) och radering, inträde och utgång från en kritisk sektion: skapande - InitializeCriticalSection(...), radering - DeleteCriticalSection(...), ingång - EnterCriticalSection(...), exit - LeaveCriticalSection (...). Begränsningar: Eftersom det inte är ett kärnobjekt är det inte synligt för andra processer, vilket innebär att du bara kan skydda trådarna i din egen process. Den kritiska sektionen analyserar värdet av en speciell processvariabel, som används som en flagga för att förhindra att flera trådar exekverar ett visst avsnitt av koden samtidigt. Bland synkroniseringsobjekt är kritiska avsnitt de enklaste. 2) mutexföränderligutesluta. Detta är ett kärnobjekt, det har ett namn, vilket betyder att de kan användas för att synkronisera åtkomst till delad data från flera processer, eller mer exakt, från trådar av olika processer. Ingen annan tråd kan förvärva en mutex som redan ägs av en av trådarna. Om en mutex skyddar vissa delade data, kommer den att kunna utföra sin funktion endast om varje tråd kontrollerar tillståndet för denna mutex innan den kommer åt dessa data. Windows behandlar en mutex som ett delat objekt som kan signaleras eller återställas. Signaleringstillståndet för mutex indikerar att det är upptaget. Trådar måste oberoende analysera det aktuella tillståndet för mutexes. Om du vill att mutex ska vara tillgängligt för trådar av andra processer måste du ge det ett namn. Funktioner: CreateMutex(name) – creation, hnd=OpenMutex(name) – öppning, WaitForSingleObject(hnd) – väntar och ockuperar, ReleaseMutex(hnd) – släpper, CloseHandle(hnd) – stängning. Den kan användas för att skydda mot att program startas om. 3) semafor -semafor. Kärnobjektet "semaphore" används för att redogöra för resurser och tjänar till att begränsa samtidig åtkomst till en resurs med flera trådar. Med hjälp av en semafor kan du organisera arbetet i ett program på ett sådant sätt att flera trådar samtidigt kan komma åt en resurs, men antalet av dessa trådar kommer att vara begränsat. När du skapar en semafor anger du det maximala antalet trådar som samtidigt kan arbeta med resursen. Varje gång ett program kommer åt en semafor, minskas semaforens resursräknare med ett. När resursräknaren blir noll är semaforen inte tillgänglig. skapa CreateSemaphore, öppna OpenSemaphore, ockupera WaitForSingleObject, släpp ReleaseSemaphore 4 ) händelse -händelse. Händelser signalerar vanligtvis helt enkelt slutförandet av någon operation; de är också kärnobjekt. Du kan inte bara uttryckligen släppa den, utan det finns också en händelseinställningsoperation. Händelser kan vara manuella eller enstaka. En enskild händelse är mer en allmän flagga. En händelse är i signalerat tillstånd om den ställs in av någon tråd. Om programmet kräver att när en händelse inträffar, reagerar endast en av trådarna på den, medan alla andra trådar fortsätter att vänta, så används en enda händelse. En manuell händelse är inte bara en gemensam flagga för flera trådar. Den utför något mer komplexa funktioner. Vilken tråd som helst kan ställa in denna händelse eller återställa (rensa) den. När en händelse väl är inställd kommer den att förbli i det tillståndet på obestämd tid, oavsett hur många trådar som väntar på att händelsen ska ställas in. När alla trådar som väntar på denna händelse får ett meddelande om att händelsen har inträffat, återställs den automatiskt. Funktioner: SetEvent, ClearEvent, WaitForEvent. Händelsetyper: 1) händelse med automatisk återställning: WaitForSingleEvent. 2) en händelse med manuell återställning (manuell), då måste händelsen återställas: ReleaseEvent. Vissa teoretiker identifierar ett annat synkroniseringsobjekt: WaitAbleTimer - ett OS-kärnobjekt som oberoende går in i ett fritt tillstånd efter ett givet tidsintervall (väckarklocka).

Ibland när man arbetar med flera trådar eller processer blir det nödvändigt synkronisera exekvering två eller flera av dem. Anledningen till detta är oftast att två eller flera trådar kan kräva tillgång till en delad resurs som verkligen kan inte tillhandahållas till flera trådar samtidigt. En delad resurs är en resurs som kan nås samtidigt av flera pågående uppgifter.

Mekanismen som säkerställer synkroniseringsprocessen kallas begränsning av tillträde. Behovet av det uppstår även i fall där en tråd väntar på en händelse genererad av en annan tråd. Naturligtvis måste det finnas något sätt på vilket den första tråden kommer att avbrytas innan händelsen inträffar. Efter detta bör tråden fortsätta sitt exekvering.

Det finns två allmänna tillstånd som en uppgift kan befinna sig i. För det första kan uppgiften genomföras(eller vara redo att köra så snart den får tillgång till CPU-resurser). För det andra kan uppgiften vara blockerad. I det här fallet avbryts dess exekvering tills resursen den behöver frigörs eller en viss händelse inträffar.

Windows har speciella tjänster som låter dig begränsa åtkomsten till delade resurser på ett visst sätt, för utan hjälp av operativsystemet kan en separat process eller tråd inte själv avgöra om den har ensam tillgång till en resurs. Operativsystemet Windows innehåller en procedur som under en kontinuerlig operation kontrollerar och om möjligt ställer in resursåtkomstflaggan. På språket för operativsystemutvecklare kallas denna operation kontroll och installationsfunktion. Flaggor som används för att tillhandahålla synkronisering och kontroll åtkomst till resurser anropas semaforer(semafor). Win32 API ger stöd för semaforer och andra synkroniseringsobjekt. MFC-biblioteket innehåller även stöd för dessa objekt.

Synkroniseringsobjekt och mfc-klasser

Win32-gränssnittet stöder fyra typer av synkroniseringsobjekt - alla är på något sätt baserade på konceptet en semafor.

Den första typen av objekt är själva semaforen, eller klassisk (standard) semafor. Det tillåter ett begränsat antal processer och trådar att komma åt en enda resurs. I det här fallet är tillgången till resursen antingen helt begränsad (en och endast en tråd eller process kan komma åt resursen under en viss tidsperiod), eller så får bara ett litet antal trådar och processer åtkomst samtidigt. Semaforer implementeras med hjälp av en räknare vars värde minskar när en semafor allokeras till en uppgift och ökar när uppgiften släpper en semafor.

Den andra typen av synkroniseringsobjekt är exklusiv (mutex) semafor. Den är utformad för att helt begränsa åtkomsten till en resurs så att endast en process eller tråd kan komma åt resursen vid varje given tidpunkt. I själva verket är detta en speciell typ av semafor.

Den tredje typen av synkroniseringsobjekt är händelse, eller händelseobjekt. Den används för att blockera åtkomst till en resurs tills någon annan process eller tråd förklarar att resursen kan användas. Således signalerar detta objekt fullbordandet av den nödvändiga händelsen.

Med hjälp av en fjärde typ av synkroniseringsobjekt kan du förbjuda exekvering av vissa delar av programkoden av flera trådar samtidigt. För att göra detta måste dessa områden deklareras som kritiskt avsnitt. När en tråd går in i det här avsnittet är andra trådar förbjudna att göra detsamma tills den första tråden lämnar avsnittet.

Kritiska sektioner, till skillnad från andra typer av synkroniseringsobjekt, används endast för att synkronisera trådar inom samma process. Andra typer av objekt kan användas för att synkronisera trådar inom en process eller för att synkronisera processer.

I MFC stöds synkroniseringsmekanismen som tillhandahålls av Win32-gränssnittet av följande klasser, som härrör från klassen CSyncObject:

    CCriticalSection- implementerar det kritiska avsnittet.

    CEvent- implementerar ett händelseobjekt

    CMutex- implementerar en exklusiv semafor.

    CSemafor- implementerar en klassisk semafor.

Utöver dessa klasser definierar MFC också två extra synkroniseringsklasser: CSingleLock Och CMultiLock. De styr åtkomsten till synkroniseringsobjektet och innehåller metoder som används för att tillhandahålla och frigöra sådana objekt. Klass CSingleLock kontrollerar åtkomst till ett enda synkroniseringsobjekt och klassen CMultiLock- till flera föremål. I det följande kommer vi bara att överväga klassen CSingleLock.

När väl ett synkroniseringsobjekt har skapats kan åtkomst till det kontrolleras med klassen CSingleLock. För att göra detta måste du först skapa ett objekt av typen CSingleLock använder konstruktorn:

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

Den första parametern skickar en pekare till ett synkroniseringsobjekt, till exempel en semafor. Värdet på den andra parametern avgör om konstruktören ska försöka komma åt detta objekt. Om denna parameter inte är noll kommer åtkomst att erhållas, annars görs inga försök att få åtkomst. Om åtkomst beviljas, då tråden som skapade klassobjektet CSingleLock, kommer att stoppas tills motsvarande synkroniseringsobjekt släpps av metoden Låsa upp klass CSingleLock.

När ett objekt av typen CSingleLock skapas kan åtkomst till objektet som pekas på av pObject styras med två funktioner: Låsa Och Låsa upp klass CSingleLock.

Metod Låsaär avsett att få tillgång till ett objekt till ett synkroniseringsobjekt. Tråden som anropade den är avstängd tills metoden är klar, det vill säga tills åtkomst till resursen erhålls. Värdet på parametern bestämmer hur länge funktionen väntar på åtkomst till det önskade objektet. Varje gång en metod slutförs framgångsrikt, minskas värdet på räknaren som är associerad med synkroniseringsobjektet med ett.

Metod Låsa upp Frigör synkroniseringsobjektet, vilket tillåter andra trådar att använda resursen. I den första versionen av metoden ökas värdet på räknaren som är associerad med detta objekt med ett. I det andra alternativet bestämmer den första parametern hur mycket detta värde ska ökas. Den andra parametern pekar på variabeln i vilken det föregående räknarvärdet kommer att skrivas in.

När du arbetar med en klass CSingleLock Den allmänna proceduren för att kontrollera åtkomst till en resurs är:

    skapa ett objekt av typen CSyncObj (till exempel en semafor) som kommer att användas för att kontrollera åtkomst till resursen;

    använd det skapade synkroniseringsobjektet, skapa ett objekt av typen CSingleLock;

    för att få tillgång till en resurs, anropa låsmetoden;

    komma åt en resurs;

    Anropa upplåsningsmetoden för att frigöra resursen.

Följande beskriver hur man skapar och använder semaforer och händelseobjekt. När du väl förstår dessa begrepp kan du enkelt lära dig och använda två andra typer av synkroniseringsobjekt: kritiska sektioner och mutexer.