Flertrådiga applikationer. Flertrådade applikationer för .NET flertrådade program med exempel
slutet av filen. På så sätt blandas aldrig loggposter gjorda av olika processer. I mer moderna Unix-system tillhandahålls en speciell syslog-tjänst (3C) för loggning.Fördelar:
- Enkel utveckling. Faktum är att vi kör många kopior av en enda trådad applikation och de körs oberoende av varandra. Det är möjligt att inte använda någon specifik multitrådad API och medel för kommunikation mellan processer.
- Hög tillförlitlighet. Onormalt avslutande av någon av processerna påverkar inte resten av processerna på något sätt.
- Bra portabilitet. Applikationen kommer att fungera på alla multitasking OS
- Hög säkerhet. Olika ansökningsprocesser kan köras som olika användare... Således kan du implementera principen om minsta privilegium, när var och en av processerna bara har de rättigheter som är nödvändiga för att han ska fungera. Även om ett fel hittas i en av processerna som tillåter fjärrexekvering av kod, kommer en angripare endast att kunna få åtkomstnivån som denna process kördes med.
Nackdelar:
- Alla ansökningar kan inte tillhandahållas på detta sätt. Till exempel är den här arkitekturen lämplig för en server som betjänar statiska HTML-sidor, men inte alls för en databasserver och många applikationsservrar.
- Att skapa och förstöra processer är en dyr operation, så denna arkitektur är inte optimal för många uppgifter.
I Unix-system vidtas en hel rad åtgärder för att skapa en process och lansera nytt program i processen så billigt som möjligt. Du måste dock förstå att det alltid är billigare att skapa en tråd inom en befintlig process än att skapa en ny process.
Exempel: apache 1.x (HTTP-server)
Multibearbetningsapplikationer som kommunicerar över uttag, rör och System V IPC-meddelandeköer
De listade medlen för IPC (Interprocess communication) tillhör de så kallade metoderna för harmonisk interprocesskommunikation. De låter dig organisera interaktionen mellan processer och trådar utan att använda delat minne. Programmeringsteoretiker är mycket förtjusta i denna arkitektur eftersom den praktiskt taget eliminerar många av alternativen för konkurrensfel.
Fördelar:
- Relativ enkel utveckling.
- Hög tillförlitlighet. Onormal avslutning av en av processerna gör att röret eller uttaget stängs och vid meddelandeköer slutar meddelanden att komma in eller hämtas från kön. Resten av programmets processer kan enkelt upptäcka det här felet och återställa det, kanske (men inte nödvändigtvis) helt enkelt starta om den misslyckade processen.
- Många av dessa applikationer (särskilt socket-baserade sådana) är enkelt omdesignade för att köras i en distribuerad miljö, där olika komponenter i applikationen körs på olika maskiner.
- Bra portabilitet. Applikationen kommer att fungera på de flesta multitasking-operativsystem, inklusive äldre Unix-system.
- Hög säkerhet. Olika ansökningsprocesser kan köras på uppdrag av olika användare. Det är alltså möjligt att implementera principen om minsta privilegium, när var och en av processerna endast har de rättigheter som är nödvändiga för att den ska fungera.
Även om ett fel hittas i en av processerna som tillåter fjärrexekvering av kod, kommer en angripare endast att kunna få åtkomstnivån som denna process kördes med.
Nackdelar:
- Denna arkitektur är inte lätt att designa och implementera för alla applikationer.
- Alla de listade typerna av IPC-verktyg förutsätter seriell dataöverföring. Om slumpmässig åtkomst till delad data krävs är den här arkitekturen obekväm.
- Överföring av data genom en pipe, socket och meddelandekö kräver att systemanrop exekveras och data kopieras två gånger - först från den ursprungliga processens adressutrymme till kärnans adressutrymme, sedan från kärnans adressutrymme till minnet målprocess... Det är dyra operationer. Vid överföring av stora mängder data kan detta bli ett allvarligt problem.
- De flesta system har begränsningar för det totala antalet rör, uttag och IPC-anläggningar. Till exempel tillåter Solaris högst 1 024 öppna rör, sockets och filer per process som standard (detta beror på begränsningarna för det valda systemanropet). Solaris arkitektoniska gräns är 65 536 rör, uttag och filer per process.
Gränsen för det totala antalet TCP/IP-sockets är inte mer än 65536 per nätverksgränssnitt (på grund av formatet på TCP-huvuden). System V IPC-meddelandeköer finns i kärnans adressutrymme, så det finns hårda gränser för antalet köer i systemet och för mängden och antalet meddelanden som köar samtidigt.
- Att skapa och förstöra en process och växla mellan processer är dyra operationer. Denna arkitektur är inte optimal i alla fall.
Delat minne multiprocessing applikationer
Det delade minnet kan vara System V IPC delat minne och fil-till-minne-mappning. System V IPC-semaforer, mutexes och POSIX-semaforer kan användas för att synkronisera åtkomst, och när filer mappas till minnet, fånga delar av filen.
Fördelar:
- Effektiv slumpmässig tillgång till delad data. Denna arkitektur är lämplig för implementering av databasservrar.
- Hög tolerans. Kan portas till alla operativsystem som stöder eller emulerar System V IPC.
- Relativt hög säkerhet. Olika ansökningsprocesser kan köras på uppdrag av olika användare. Det är alltså möjligt att implementera principen om minsta privilegium, när var och en av processerna endast har de rättigheter som är nödvändiga för att den ska fungera. Separationen av åtkomstnivåer är dock inte lika strikt som i de tidigare diskuterade arkitekturerna.
Nackdelar:
- Utvecklingens relativa komplexitet. Åtkomstsynkroniseringsfel – så kallade konfliktfel – är mycket svåra att upptäcka under testning.
Detta kan resultera i en 3 till 5 gånger ökning av den totala utvecklingskostnaden jämfört med enkelgängade eller enklare multitasking-arkitekturer.
- Låg tillförlitlighet. Onormal avslutning av någon av programmets processer kan lämna (och ofta lämna) delat minne i ett inkonsekvent tillstånd.
Detta gör ofta att resten av programmet kraschar. Vissa applikationer, som Lotus Domino, dödar avsiktligt serveromfattande processer om någon av dem avslutas onormalt.
- Att skapa och förstöra en process och växla mellan dem är dyra operationer.
Därför är denna arkitektur inte optimal för alla applikationer.
- Under vissa omständigheter kan användningen av delat minne leda till eskalering av privilegier. Om ett fel hittas i en av processerna som leder till fjärrkörning av kod, är det mycket troligt att en angripare kommer att kunna använda det för att fjärrexekvera kod i andra processer i programmet.
Det vill säga, i ett värsta fall kan en angripare få den åtkomstnivå som motsvarar den högsta av åtkomstnivåerna för applikationsprocesserna.
- Delade minnesapplikationer måste köras på samma fysiska dator, eller åtminstone på maskiner som har delat RAM. Faktum är att denna begränsning kan kringgås, till exempel genom att använda minnesmappade delade filer, men detta introducerar betydande overhead.
Faktum är att denna arkitektur kombinerar nackdelarna med multiprocessing och multithreaded applikationer. Ett antal populära applikationer utvecklade på 80-talet och början av 90-talet, innan Unix standardiserade multithreaded API:er, använder dock denna arkitektur. Dessa är många databasservrar, både kommersiella (Oracle, DB2, Lotus Domino), såväl som gratisprogram, moderna versioner av Sendmail och några andra e-postservrar.
Egna flertrådiga applikationer
Trådar eller trådar i ett program körs inom en enda process. Hela adressutrymmet för en process delas mellan trådar. Vid första anblicken verkar det som att detta låter dig organisera interaktion mellan trådar utan några speciella API:er alls. I verkligheten är detta inte fallet - om flera trådar arbetar med en delad datastruktur, eller systemresurs, och åtminstone en av strömmarna modifierar denna struktur, då kommer data vid vissa tidpunkter att vara inkonsekventa.
Därför måste trådar använda speciella medel för att organisera interaktion. De viktigaste verktygen är primitiva för ömsesidig uteslutning (mutexes och läs-/skrivlås). Genom att använda dessa primitiver kan programmeraren säkerställa att inga trådar kommer åt delade resurser medan de är i ett inkonsekvent tillstånd (detta kallas ömsesidig uteslutning). System V IPC, endast de strukturer som finns i segmentet delat minne delas. Reguljära variabler och normalt allokerade dynamiska datastrukturer är olika för varje process). Delade dataåtkomstfel - konkurrensfel - är mycket svåra att upptäcka under testning.
Generellt kan vi säga att flertrådade applikationer har nästan samma fördelar och nackdelar som multibearbetningsapplikationer som använder delat minne.
Kostnaden för att köra en flertrådad applikation är dock lägre, och utvecklingen av en sådan applikation är i vissa avseenden lättare än en applikation baserad på delat minne. Därför har flertrådade applikationer de senaste åren blivit mer och mer populära.
Kapitel 10.
Flertrådade applikationer
Multitasking i moderna operativsystem tas för givet [ Före tillkomsten av Apple OS X fanns det inga moderna multitasking-operativsystem på Macintosh-datorer. Det är mycket svårt att korrekt designa ett operativsystem med full multitasking, så OS X måste baseras på Unix.]. Användaren förväntar sig det när han kör samtidigt textredigerare och e-postklienten kommer dessa program inte i konflikt, och när de tar emot E-post redigeraren kommer inte att sluta fungera. När flera program startas samtidigt växlar operativsystemet snabbt mellan programmen och förser dem med en processor i tur och ordning (såvida inte flera processorer är installerade på datorn förstås). Som ett resultat, illusion kör flera program samtidigt, eftersom inte ens den bästa maskinskrivaren (och den snabbaste internetanslutningen) kan hålla jämna steg med en modern processor.
Multithreading kan på sätt och vis ses som nästa nivå av multitasking: istället för att växla mellan olika programoperativ system växlar mellan olika delar av samma program. Till exempel flertrådad e-postklient låter dig ta emot nya e-postmeddelanden medan du läser eller skriver nya meddelanden. Nuförtiden tas multithreading också för givet av många användare.
VB har aldrig haft normalt stöd för multithreading. Det är sant att en av dess sorter dök upp i VB5 - kollaborativ streamingmodell(lägenhetsgängning). Som du snart kommer att se ger samarbetsmodellen programmeraren några av fördelarna med multithreading, men den drar inte full nytta av alla funktioner. Förr eller senare måste du byta från en träningsmaskin till en riktig, och VB .NET blev den första versionen av VB med stöd för en gratis multitrådad modell.
Multithreading är dock inte en av funktionerna som är lätta att implementera i programmeringsspråk och lätt bemästras av programmerare. Varför?
För i flertrådade applikationer kan det uppstå mycket knepiga fel som dyker upp och försvinner oförutsägbart (och sådana fel är svårast att felsöka).
Varning ärligt talat: multithreading är ett av de svåraste områdena inom programmering. Den minsta ouppmärksamhet leder till uppkomsten av svårfångade fel, vars korrigering tar astronomiska summor. Av denna anledning innehåller detta kapitel många dålig exempel - vi skrev dem medvetet för att demonstrera typiska fel... Detta är det säkraste sättet att lära sig flertrådsprogrammering: du måste kunna upptäcka potentiella problem när allt verkar fungera bra vid första anblicken, och veta hur du löser dem. Om du vill använda flertrådiga programmeringstekniker kan du inte klara dig utan det.
Detta kapitel kommer att lägga en solid grund för vidare självständigt arbete, men vi kommer inte att kunna beskriva flertrådsprogrammering i alla dess subtiliteter - bara den tryckta dokumentationen för klasserna i Threading-namnområdet tar mer än 100 sidor. Om du vill behärska flertrådsprogrammering på en högre nivå, se specialiserade böcker.
Men oavsett hur farlig flertrådsprogrammering är, är det oumbärligt för professionell lösning av vissa problem. Om dina program inte använder multithreading där det är lämpligt, kommer användarna att bli mycket frustrerade och föredra en annan produkt. Till exempel, bara i den fjärde versionen av det populära e-postprogrammet Eudora dök flertrådsfunktioner upp, utan vilka det är omöjligt att föreställa sig något modernt program för att arbeta med e-post. När Eudora introducerade stöd för multithreading hade många användare (inklusive en av författarna till den här boken) bytt till andra produkter.
Slutligen, i .NET existerar helt enkelt inte enkeltrådade program. Allt.NET-program är flertrådade eftersom sopsamlaren körs som en lågprioriterad bakgrundsprocess. Som visas nedan, för seriös grafisk programmering i .NET, kan korrekt trådning hjälpa till att förhindra att det grafiska gränssnittet blockeras när programmet utför långa operationer.
Vi introducerar multithreading
Varje program fungerar i en specifik sammanhang, som beskriver distributionen av kod och data i minnet. Genom att spara sammanhanget sparas faktiskt programflödets tillstånd, vilket gör att du kan återställa det i framtiden och fortsätta körningen av programmet.
Att spara sammanhang kommer med en kostnad för tid och minne. Operativsystemet kommer ihåg programtrådens tillstånd och överför kontrollen till en annan tråd. När programmet vill fortsätta att köra den suspenderade tråden måste den sparade kontexten återställas, vilket tar ännu längre tid. Därför bör multithreading endast användas när fördelarna uppväger alla kostnader. Några typiska exempel listas nedan.
- Funktionaliteten i programmet är tydligt och naturligt uppdelad i flera heterogena operationer, som i exemplet med att ta emot e-post och förbereda nya meddelanden.
- Programmet utför långa och komplexa beräkningar, och du vill inte att det grafiska gränssnittet ska blockeras under beräkningarnas varaktighet.
- Programmet körs på en dator med flera processorer med ett operativsystem som stöder användningen av flera processorer (så länge antalet aktiva trådar inte överstiger antalet processorer är parallellkörning praktiskt taget fri från kostnaderna för att byta trådar).
Innan du går vidare till mekaniken i flertrådade program är det nödvändigt att påpeka en omständighet som ofta orsakar förvirring bland nybörjare inom området flertrådsprogrammering.
En procedur, inte ett objekt, exekveras i programflödet.
Det är svårt att säga vad som menas med uttrycket "objekt springer", men en av författarna undervisar ofta i seminarier om flertrådig programmering och denna fråga ställs oftare än andra. Kanske tror någon att arbetet med programtråden börjar med ett anrop till klassens nya metod, varefter tråden bearbetar alla meddelanden som skickas till motsvarande objekt. Sådana representationer absolutär fel. Ett objekt kan innehålla flera trådar som kör olika (och ibland till och med samma) metoder, medan meddelandena från objektet sänds och tas emot av flera olika trådar (förresten, detta är en av anledningarna som komplicerar flertrådsprogrammering: för att felsöka ett program måste du ta reda på vilken tråd som finns i det här ögonblicket utför den eller den proceduren!).
Eftersom trådar skapas från metoder för objekt skapas själva objektet vanligtvis före tråden. Efter att ha skapat objektet, skapar programmet en tråd, skickar den adressen till objektets metod och först efter det ger order om att starta körningen av tråden. Proceduren för vilken tråden skapades, som alla procedurer, kan skapa nya objekt, utföra operationer på befintliga objekt och anropa andra procedurer och funktioner som ligger inom dess omfattning.
Vanliga metoder för klasser kan också köras i programtrådar. I det här fallet, tänk också på en annan viktig omständighet: tråden slutar med en utgång från proceduren för vilken den skapades. Normalt slutförande av programflödet är inte möjligt förrän proceduren avslutas.
Trådar kan avslutas inte bara naturligt utan också onormalt. Detta rekommenderas i allmänhet inte. Se Avsluta och avbryta strömmar för mer information.
De centrala .NET-funktionerna relaterade till användningen av programmatiska trådar är koncentrerade i Threading-namnområdet. Därför bör de flesta flertrådade program börja med följande rad:
Importerar System.Threading
Att importera ett namnområde gör ditt program lättare att skriva och möjliggör IntelliSense-teknik.
Den direkta kopplingen mellan flöden och procedurer tyder på att i denna bild, delegater(se kapitel 6). Specifikt inkluderar Threading-namnutrymmet ThreadStart-delegaten, som vanligtvis används vid start av programtrådar. Syntaxen för att använda denna delegat ser ut så här:
Public Delegate Sub ThreadStart ()
Kod som anropas med ThreadStart-delegaten får inte ha några parametrar eller returvärde, så trådar kan inte skapas för funktioner (som returnerar ett värde) och för procedurer med parametrar. För att överföra information från strömmen måste du också leta efter alternativa medel, eftersom de körda metoderna inte returnerar värden och inte kan använda överföring genom referens. Till exempel, om ThreadMethod är i WilluseThread-klassen, kan ThreadMethod kommunicera information genom att modifiera egenskaperna för instanser av WillUseThread-klassen.
Applikationsdomäner
.NET-trådar körs i så kallade applikationsdomäner, definierade i dokumentationen som "sandlådan där applikationen körs." En applikationsdomän kan ses som en lätt version av Win32-processer; en enda Win32-process kan innehålla flera applikationsdomäner. Den största skillnaden mellan applikationsdomäner och processer är att en Win32-process har sitt eget adressutrymme (i dokumentationen jämförs applikationsdomäner också med logiska processer som körs i en fysisk process). I NET hanteras all minneshantering av runtime, så flera applikationsdomäner kan köras i en enda Win32-process. En av fördelarna med detta schema är applikationernas förbättrade skalningsmöjligheter. Verktyg för att arbeta med applikationsdomäner finns i klassen AppDomain. Vi rekommenderar att du studerar dokumentationen för denna klass. Med dess hjälp kan du få information om miljön där ditt program körs. I synnerhet används klassen AppDomain när man utför reflektion över .NET-systemklasser. Följande program listar de laddade sammansättningarna.
Importer System.Reflektion
Modul Modul
Sub Main ()
Dimma domänen som appdomän
theDomain = AppDomain.CurrentDomain
Dimförband () As
Assemblies = theDomain.GetAssemblies
Dim anAssemblyxAs
För varje en församling i församlingar
Console.WriteLineanAssembly.Full Name) Nästa
Console.ReadLine ()
Avsluta Sub
Slutmodul
Skapar strömmar
Låt oss börja med ett rudimentärt exempel. Låt oss säga att du vill köra en procedur i en separat tråd som minskar räknarvärdet i en oändlig slinga. Proceduren definieras som en del av klassen:
Offentlig klass kommer att använda trådar
Offentlig subtrahera från räknare ()
Dim räknas som heltal
Gör medan sant räkna - = 1
Console.WriteLlne ("Är i en annan tråd och räknare ="
& räkna)
Slinga
Avsluta Sub
Slutklass
Eftersom Do loop-villkoret alltid är sant, kanske du tror att ingenting kommer att störa SubtractFromCounter-proceduren. Men i en flertrådad applikation är detta inte alltid fallet.
Följande utdrag visar Sub Main-proceduren som startar tråden och importkommandot:
Alternativ Strikt på importer System.Trådmodul Modul
Sub Main ()
1 Dim myTest As New WillUseThreads ()
2 Dim bThreadStart As New ThreadStart (AddressOf _
myTest.SubtractFromCounter)
3 Dim bTråd som ny tråd (bTrådstart)
4 "bThread.Start ()
Dim i As Integer
5 Gör medan sant
Console.WriteLine ("I huvudtråden och räkna är" & i) i + = 1
Slinga
Avsluta Sub
Slutmodul
Låt oss ta en titt på de viktigaste punkterna i följd. Först och främst fungerar Sub Man n-proceduren alltid in vanliga(huvudtråd). I .NET-program finns det alltid minst två trådar igång: huvudtråden och sopsamlingstråden. Rad 1 skapar en ny instans av testklassen. På rad 2 skapar vi en ThreadStart-delegat och skickar adressen till SubtractFromCounter-proceduren till testklassinstansen som skapats på rad 1 (denna procedur anropas utan parametrar). BraGenom att importera Threading-namnområdet kan det långa namnet utelämnas. Det nya trådobjektet skapas på rad 3. Lägg märke till att ThreadStart-delegaten passerar när du anropar Thread-klasskonstruktorn. Vissa programmerare föredrar att sammanfoga dessa två rader till en logisk rad:
Dim bThread As New Thread (New ThreadStarttAddressOf _
myTest.SubtractFromCounter))
Slutligen "startar" rad 4 tråden genom att anropa startmetoden för trådinstansen som skapats för ThreadStart-delegaten. Genom att anropa den här metoden talar vi om för operativsystemet att Subtract-proceduren ska köras i en separat tråd.
Ordet "startar" i föregående stycke är omgivet av citattecken, eftersom detta är en av många konstigheter med flertrådsprogrammering: att anropa Start startar faktiskt inte tråden! Det säger helt enkelt till operativsystemet att schemalägga den angivna tråden att köras, men det är bortom programmets kontroll att starta direkt. Du kommer inte att kunna börja köra trådar på egen hand, eftersom operativsystemet alltid kontrollerar körningen av trådar. I ett senare avsnitt kommer du att lära dig hur du använder prioritet för att få operativsystemet att starta din tråd snabbare.
I fig. 10.1 visar ett exempel på vad som kan hända efter att man startar ett program och sedan avbryter det med Ctrl + Break-tangenten. I vårt fall startade en ny tråd först efter att räknaren i huvudtråden ökat till 341!
Ris. 10.1.
Enkel flertrådad programvarukörning
Om programmet körs under en längre tid kommer resultatet att se ut ungefär som det som visas i Fig. 10.2. Vi ser att dufärdigställandet av den löpande gängan avbryts och kontrollen överförs till huvudgängan igen. I det här fallet finns det en manifestation förebyggande multitrådning genom tidsskärning. Innebörden av denna skrämmande term förklaras nedan.
Ris. 10.2.
Växla mellan trådar i ett enkelt flertrådigt program
När man avbryter trådar och överför kontroll till andra trådar använder operativsystemet principen om förebyggande multitrådning genom tidsdelning. Tidskvantisering löser också ett av de vanliga problemen som uppstod tidigare i flertrådade program - en tråd tar upp all CPU-tid och är inte sämre än kontrollen av andra trådar (som regel sker detta i intensiva cykler som den ovan). För att förhindra exklusiv CPU-kapning bör dina trådar överföra kontrollen till andra trådar då och då. Om programmet visar sig vara "omedvetet" finns det en annan, något mindre önskvärd lösning: operativsystemet föregriper alltid en pågående tråd, oavsett dess prioritetsnivå, så att åtkomst till processorn ges till varje tråd i systemet.
Eftersom kvantiseringsscheman för alla versioner av Windows som kör .NET har en minsta tidsdel tilldelad varje tråd, i .NET-programmering, är problemen med CPU exklusiva grepp inte så allvarliga. Å andra sidan, om .NET-ramverket någonsin anpassas för andra system kan detta förändras.
Om vi inkluderar följande rad i vårt program innan vi anropar Start, kommer även trådarna med lägst prioritet att få en bråkdel av CPU-tiden:
bThread.Priority = ThreadPriority.Highest
Ris. 10.3.
Tråden med högst prioritet startar vanligtvis snabbare
Ris. 10.4.
Processorn tillhandahålls också för trådar med lägre prioritet
Kommandot tilldelar maximal prioritet till den nya tråden och minskar prioritet för huvudtråden. Fikon. 10.3 kan man se att den nya tråden börjar fungera snabbare än tidigare, men som Fig. 10.4 får huvudtråden också kontrolllathet (om än under en mycket kort tid och först efter långvarigt arbete med flödet med subtraktion). När du kör programmet på dina datorer får du resultat liknande de som visas i Fig. 10.3 och 10.4, men på grund av skillnaderna mellan våra system kommer det inte att finnas någon exakt matchning.
Den uppräknade ThreadPrlority-typen inkluderar värden för fem prioritetsnivåer:
Trådprioritet.Högst
ThreadPriority.AboveNormal
ThreadPrlority.Normal
ThreadPriority.BelowNormal
Trådprioritet.Lägst
Gå med metod
Ibland måste en programtråd pausas tills en annan tråd slutar. Låt oss säga att du vill pausa tråd 1 tills tråd 2 har slutfört sin beräkning. För detta från stream 1 Join-metoden kallas för stream 2. Med andra ord kommandot
tråd2. Gå med ()
avbryter den aktuella tråden och väntar på att tråd 2 ska slutföras. Tråd 1 går till låst tillstånd.
Om du går med i stream 1 till stream 2 med hjälp av Join-metoden kommer operativsystemet automatiskt att starta stream 1 efter stream 2. Observera att startprocessen är icke-deterministisk: det är omöjligt att säga exakt hur lång tid efter slutet av tråd 2, tråd 1 kommer att börja fungera. Det finns en annan version av Join som returnerar ett booleskt värde:
tråd2. Gå med (heltal)
Den här metoden väntar antingen på att tråd 2 ska slutföras eller avblockerar tråd 1 efter att det angivna tidsintervallet har förflutit, vilket gör att schemaläggaren för operativsystemet allokerar CPU-tid till tråden igen. Metoden returnerar True om tråd 2 avslutas innan det angivna timeoutintervallet löper ut, och False annars.
Kom ihåg grundregeln: om tråd 2 har slutförts eller timeout har du ingen kontroll över när tråd 1 är aktiverad.
Trådnamn, CurrentThread och ThreadState
Egenskapen Thread.CurrentThread returnerar en referens till trådobjektet som körs för närvarande.
Även om det finns ett underbart trådfönster för felsökning av flertrådade applikationer i VB .NET, vilket beskrivs nedan, blev vi väldigt ofta hjälpta av kommandot
MsgBox (Thread.CurrentThread.Name)
Det visade sig ofta att koden exekverades i en helt annan tråd som den var tänkt att exekveras från.
Kom ihåg att termen "icke-deterministisk schemaläggning av programflöden" betyder en mycket enkel sak: programmeraren har praktiskt taget inga medel till sitt förfogande för att påverka schemaläggarens arbete. Av denna anledning använder program ofta egenskapen ThreadState, som returnerar information om det aktuella tillståndet för en tråd.
Strömmar fönster
Trådfönstret i Visual Studio .NET är ovärderligt vid felsökning av flertrådade program. Den aktiveras av undermenykommandot Debug> Windows i avbrottsläge. Låt oss säga att du tilldelade ett namn till bThread-tråden med följande kommando:
bThread.Name = "Att dra av tråden"
En ungefärlig vy av strömningsfönstret efter att ha avbrutit programmet med tangentkombinationen Ctrl + Break (eller på annat sätt) visas i Fig. 10.5.
Ris. 10.5.
Strömmar fönster
Pilen i den första kolumnen markerar den aktiva tråden som returneras av egenskapen Thread.CurrentThread. ID-kolumnen innehåller numeriska tråd-ID:n. Nästa kolumn listar strömnamnen (om tilldelad). Kolumnen Plats anger proceduren som ska köras (till exempel WriteLine-proceduren för konsolklassen i figur 10.5). De återstående kolumnerna innehåller information om prioriterade och tillfälliga trådar (se nästa avsnitt).
Trådfönstret (inte operativsystemet!) Låter dig styra trådarna i ditt program med hjälp av snabbmenyer. Du kan till exempel stoppa den aktuella tråden genom att högerklicka på motsvarande rad och välja kommandot Frys (senare kan den stoppade tråden återupptas). Stoppa trådar används ofta vid felsökning för att förhindra en felaktig tråd från att störa applikationen. Dessutom låter strömningsfönstret dig aktivera en annan (ej stoppad) ström; för att göra detta, högerklicka på önskad rad och välj in innehållsmeny kommandot Byt till tråd (eller dubbelklicka bara på trådraden). Som kommer att visas nedan är detta mycket användbart för att diagnostisera potentiella dödlägen.
Stänga av en ström
Tillfälligt oanvända strömmar kan överföras till ett passivt tillstånd med Slеer-metoden. En passiv ström anses också vara blockerad. Naturligtvis, när en tråd försätts i ett passivt tillstånd kommer resten av trådarna att ha fler processorresurser. Standardsyntaxen för Slеer-metoden är följande: Thread.Sleep (intervall_i_millisekunder)
Som ett resultat av Sleep-anropet blir den aktiva tråden passiv i åtminstone det angivna antalet millisekunder (aktivering omedelbart efter att det angivna intervallet har löpt ut är dock inte garanterat). Observera: när metoden anropas skickas inte en referens till en specifik tråd - Sleep-metoden anropas endast för den aktiva tråden.
En annan version av Sleep får den nuvarande tråden att ge upp resten av den tilldelade CPU-tiden:
Tråd.Sömn (0)
Nästa alternativ sätter den aktuella tråden i ett passivt tillstånd under en obegränsad tid (aktivering sker endast när du anropar Interrupt):
Thread.Sleer (Timeout.Infinite)
Eftersom passiva trådar (även med en obegränsad timeout) kan avbrytas av Interrupt-metoden, vilket leder till initieringen av ett ThreadlnterruptExcepti vid undantag, är Slayer-anropet alltid inneslutet i ett Try-Catch-block, som i följande utdrag:
Prova
Thread.Sleep (200)
"Trådens passiva tillstånd har avbrutits
Catch e Som undantag
"Andra undantag
Avsluta försök
Varje .NET-program körs på en programtråd, så Sleep-metoden används också för att stänga av program (om Threadipg-namnområdet inte importeras av programmet måste du använda det fullständiga namnet Threading.Thread. Sleep).
Avsluta eller avbryta programtrådar
En tråd kommer automatiskt att avslutas när metoden som anges när ThreadStart-delegaten skapas, men ibland är det nödvändigt att avsluta metoden (och därmed tråden) när vissa faktorer inträffar. I sådana fall brukar strömmar kolla villkorlig variabel, beroende på vilket tillståndbeslut fattas om nödutgång från bäcken. Vanligtvis ingår en Do-While-loop i proceduren för detta:
Sub ThreadedMethod ()
"Programmet måste tillhandahålla medel för undersökningen
"villkorlig variabel.
"Till exempel kan en villkorsvariabel utformas som en egenskap
Gör medan conditionVariable = False And MoreWorkToDo
"Huvudkoden
Loop End Sub
Det tar lite tid att polla den villkorliga variabeln. Du bör endast använda beständig polling i ett looptillstånd om du väntar på att en tråd ska avslutas i förtid.
Om villkorsvariabeln måste kontrolleras på en specifik plats, använd kommandot If-Then tillsammans med Exit Sub inuti en oändlig loop.
Tillgång till en villkorsvariabel måste synkroniseras så att exponering från andra trådar inte stör dess normala användning. Detta viktiga ämne behandlas i avsnittet "Felsökning: Synkronisering".
Tyvärr exekveras inte koden för passiva (eller på annat sätt blockerade) trådar, så alternativet med polling av en villkorsvariabel är inte lämpligt för dem. I det här fallet anropar du Interrupt-metoden på objektvariabeln som innehåller en referens till den önskade tråden.
Avbrottsmetoden kan endast anropas på trådar i tillståndet Vänta, Vila eller Gå med. Om du anropar Interrupt för en tråd som är i ett av de listade tillstånden, kommer tråden efter ett tag att börja fungera igen, och exekveringsmiljön kommer att initiera ett ThreadlnterruptedExcepti på undantag i tråden. Detta inträffar även om tråden har gjorts passiv på obestämd tid genom att anropa Thread.Sleepdimeout. Oändlig). Vi säger "efter ett tag" eftersom trådschemaläggning är icke-deterministisk. Undantaget ThreadlnterruptedExcepti on fångas av Catch-sektionen, som innehåller utgångskoden från vänteläget. Men Catch-sektionen behöver inte avsluta tråden på ett avbrottsanrop – tråden hanterar undantaget som den finner lämpligt.
I .NET kan Interrupt-metoden anropas även för oblockerade trådar. I detta fall avbryts tråden vid närmaste blockering.
Avbryta och döda trådar
Namnutrymmet Threading innehåller andra metoder som avbryter normal trådning:
- Uppskjuta;
- Avbryta.
Det är svårt att säga varför .NET inkluderade stöd för dessa metoder - att anropa Suspend and Abort kommer sannolikt att göra att programmet blir instabilt. Ingen av metoderna tillåter normal avinitialisering av strömmen. Dessutom, när du anropar Suspend eller Abort, är det omöjligt att förutsäga vilket tillstånd tråden kommer att lämna objekt i efter att ha avbrutits eller avbrutits.
Att anropa Abort ger ett ThreadAbortException. För att hjälpa dig förstå varför detta konstiga undantag inte bör hanteras i program, här är ett utdrag från .NET SDK-dokumentationen:
"... När en tråd förstörs genom att anropa Abort, kastar körtiden ett ThreadAbortException. Detta är en speciell typ av undantag som inte kan fångas upp av programmet. När detta undantag kastas, kör runtime alla Finally-block innan tråden avslutas. Eftersom alla åtgärder kan äga rum i Finally blocks, ring Join för att säkerställa att strömmen förstörs."
Moral: Abort och Suspend rekommenderas inte (och om du fortfarande inte klarar dig utan Suspend, återuppta den avbrutna tråden med hjälp av Resume-metoden). Du kan säkert avsluta en tråd endast genom att polla en synkroniserad villkorsvariabel eller genom att anropa avbrottsmetoden som diskuterats ovan.
Bakgrundstrådar (demoner)
Vissa trådar som körs i bakgrunden slutar automatiskt att köras när andra programkomponenter slutar. Särskilt sopsamlaren löper i en av bakgrundstrådarna. Bakgrundstrådar skapas vanligtvis för att ta emot data, men detta görs endast om andra trådar kör kod som kan bearbeta mottagna data. Syntax: strömnamn. IsBackGround = True
Om det bara finns bakgrundstrådar kvar i applikationen kommer applikationen att avslutas automatiskt.
Ett allvarligare exempel: extrahera data från HTML-kod
Vi rekommenderar att du endast använder strömmar när programmets funktionalitet är tydligt uppdelad i flera operationer. Ett bra exempelär programmet för att extrahera data från HTML-koden från kapitel 9. Vår klass gör två saker: hämta data från Amazons webbplats och bearbeta den. Detta är ett perfekt exempel på en situation där flertrådsprogrammering verkligen är lämplig. Vi skapar klasser för flera olika böcker och analyserar sedan data i olika strömmar. Att skapa en ny tråd för varje bok ökar effektiviteten i programmet, för medan en tråd tar emot data (vilket kan kräva att man väntar på Amazon-servern), kommer en annan tråd att vara upptagen med att behandla data som redan har tagits emot.
Den flertrådade versionen av detta program fungerar mer effektivt än den enkeltrådade versionen endast på en dator med flera processorer eller om mottagningen av ytterligare data effektivt kan kombineras med deras analys.
Som nämnts ovan kan endast procedurer som inte har några parametrar köras i trådar, så du måste göra mindre ändringar i programmet. Nedan är den grundläggande proceduren, omskriven för att utesluta parametrar:
Public Sub FindRank ()
m_Rank = ScrapeAmazon ()
Console.WriteLine ("rankingen av" & m_Name & "Is" & GetRank)
Avsluta Sub
Eftersom vi inte kommer att kunna använda det kombinerade fältet för att lagra och hämta information (skriva flertrådade program med grafiskt gränssnitt diskuteras i det sista avsnittet av detta kapitel), lagrar programmet data från de fyra böckerna i en array vars definition börjar så här:
Dim theBook (3.1) As String theBook (0.0) = "1893115992"
theBook (0.l) = "Programmering av VB .NET" "Etc.
Fyra strömmar skapas i samma cykel som AmazonRanker-objekt skapas:
För i = 0 till 3
Prova
theRanker = New AmazonRanker (theBook (i.0). theBookd.1))
aThreadStart = New ThreadStar (AddressOf theRanker.FindRan ()
aThread = Ny tråd (aThreadStart)
aThread.Name = theBook (i.l)
aThread.Start () Catch e Som undantag
Console.WriteLine (e.Message)
Avsluta försök
Nästa
Nedan följer hela programmets text:
Alternativ Strikt vid import System.IO Importer System.Net
Importerar System.Threading
Modul Modul
Sub Main ()
Dimma boken (3.1) som sträng
theBook (0.0) = "1893115992"
theBook (0.l) = "Programmera VB .NET"
theBook (l.0) = "1893115291"
theBook (l.l) = "Databasprogrammering VB .NET"
theBook (2,0) = "1893115623"
theBook (2.1) = "Programmerarens" introduktion till C #. "
theBook (3.0) = "1893115593"
theBook (3.1) = "Gland the .Net Platform"
Dim i As Integer
Dim theRanker As = AmazonRanker
Dim aThreadStart As Threading.ThreadStart
Dim aThread As Threading.Thread
För i = 0 till 3
Prova
theRanker = New AmazonRankerttheBook (i.0). theBook (i.1))
aThreadStart = New ThreadStart (AddressOf theRanker. FindRank)
aThread = Ny tråd (aThreadStart)
aThread.Name = theBook (i.l)
aThread.Start ()
Catch e Som undantag
Console.WriteLlnete.Message)
Avsluta Försök nästa
Console.ReadLine ()
Avsluta Sub
Slutmodul
Offentlig klass AmazonRanker
Privat m_URL som sträng
Privat m_Rank som heltal
Privat m_namn som sträng
Public Sub New (ByVal ISBN As String. ByVal theName As String)
m_URL = "http://www.amazon.com/exec/obidos/ASIN/" & ISBN
m_Name = theName End Sub
Public Sub FindRank () m_Rank = ScrapeAmazon ()
Console.Writeline ("rankingen av" & m_Name & "är"
& GetRank) End Sub
Allmän skrivskyddad egendom GetRank () As String Get
Om m_Rank<>0 Då
Returnera CStr (m_Rank) Annars
"Problem
Avsluta If
Avsluta Get
Avsluta egendom
Allmän skrivskyddad egenskap GetName () Som String Get
Returnera m_Name
Avsluta Get
Avsluta egendom
Privat funktion ScrapeAmazon () Som heltal Försök
Dimma webbadressen som ny Uri (m_URL)
Dimma theRequest As WebRequest
theRequest = WebRequest.Create (webbadressen)
Dimma svaret som webbsvar
theResponse = theRequest.GetResponse
Dim aReader As New StreamReader (theResponse.GetResponseStream ())
Dimma data som sträng
theData = aReader.ReadToEnd
Returanalys (theData)
Fånga E som undantag
Console.WriteLine (E.Message)
Console.WriteLine (E.StackTrace)
Trösta. Läslinje ()
Avsluta Försök Avsluta funktion
Privat funktionsanalys (ByVal theData As String) Som heltal
Dim plats As.Integer Location = theData.IndexOf (" Amazon.com
Försäljningsrankning:") _
+ "Amazon.com försäljningsrankning:".Längd
Dim temp As String
Gör tills theData.Substring (Location.l) = "<" temp = temp
& theData.Substring (Location.l) Plats + = 1 slinga
Retur Clnt (temp)
Avsluta funktion
Slutklass
Flertrådsoperationer används ofta i .NET- och I/O-namnområden, så .NET Framework-biblioteket tillhandahåller speciella asynkrona metoder för dem. För mer information om hur du använder asynkrona metoder när du skriver flertrådade program, se metoderna BeginGetResponse och EndGetResponse i klassen HTTPWebRequest.
Huvudfara (allmänna uppgifter)
Hittills har det enda säkra användningsfallet för trådar övervägts - våra strömmar ändrade inte den allmänna informationen. Om du tillåter ändringen i den allmänna informationen börjar potentiella fel att multiplicera exponentiellt och det blir mycket svårare att bli av med dem för programmet. Å andra sidan, om du förbjuder modifiering av delad data av olika trådar, kommer multithreading .NET-programmering knappast att skilja sig från de begränsade kapaciteterna hos VB6.
Vi erbjuder dig ett litet program som visar de problem som uppstår utan att gå in på onödiga detaljer. Detta program simulerar ett hus med en termostat i varje rum. Om temperaturen är 5 grader Fahrenheit eller mer (cirka 2,77 grader Celsius) lägre än måltemperaturen, beordrar vi värmesystemet att öka temperaturen med 5 grader; annars stiger temperaturen med endast 1 grad. Om den aktuella temperaturen är högre än eller lika med den inställda, görs ingen förändring. Temperaturreglering i varje rum utförs med separat flöde med 200 millisekunders fördröjning. Huvudarbetet görs med följande utdrag:
Om mHouse.HouseTemp< mHouse.MAX_TEMP = 5 Then Try
Thread.Sleep (200)
Catch tie As ThreadlnterruptedException
"Passiv väntan har avbrutits
Catch e Som undantag
"Undantag för andra försök
mHouse.HouseTemp + - 5 "Etc.
Nedan finns hela källkoden för programmet. Resultatet visas i fig. 10.6: Temperaturen i huset har nått 105 grader Fahrenheit (40,5 grader Celsius)!
1 Alternativ Strikt På
2 Importerar System.Trådning
3 Modul Modul
4 Sub Main ()
5 Dim myHouse As New House (l0)
6 Konsol. Läslinje ()
7 End Sub
8 Slutmodul
9 Public Class House
10 Public Const MAX_TEMP Som heltal = 75
11 Privat mCurTemp Som heltal = 55
12 privata mRum () Som rum
13 Public Sub New (ByVal numOfRooms Som heltal)
14 ReDim mRooms (antalOfRooms = 1)
15 Dim i As Integer
16 Dim aThreadStart As Threading.ThreadStart
17 Dim en tråd som tråd
18 För i = 0 Till numOfRooms -1
19 Försök
20 mRooms (i) = NewRoom (Me, mCurTemp, CStr (i) & "throom")
21 aThreadStart - New ThreadStart (AddressOf _
mRooms (i) .CheckTempInRoom)
22 aThread = Ny tråd (aThreadStart)
23 aThread.Start ()
24 Catch E som undantag
25 Console.WriteLine (E.StackTrace)
26 Avsluta försök
27 Nästa
28 Slut Sub
29 Offentlig egendom HusTemp () Som heltal
trettio . Skaffa sig
31 Returnera mCurTemp
32 Slut Get
33 Ange (ByVal Value As Heltal)
34 mCurTemp = Värde 35 Slutuppsättning
36 Slutfastighet
37 Slutklass
38 Public Class Room
39 Privat mCurTemp Som heltal
40 Privat mName As String
41 Privat mHouse As House
42 Public Sub New (ByVal theHouse As House,
ByVal temp As Integer, ByVal roomName As String)
43 mHuset = huset
44 mCurTemp = temp
45 mNamn = rumsnamn
46 End Sub
47 Public Sub CheckTempInRoom ()
48 Ändra temperatur ()
49 End Sub
50 Privat Sub ChangeTemperature ()
51 Försök
52 Om mHouse.HouseTemp< mHouse.MAX_TEMP - 5 Then
53 Thread.Sleep (200)
54 mHouse.HouseTemp + - 5
55 Console.WriteLine ("Am in" & Me.mName & _
56 ".Aktuell temperatur är" & mHouse.HouseTemp)
57. Elself mHouse.HouseTemp< mHouse.MAX_TEMP Then
58 Thread.Sleep (200)
59 mHouse.HouseTemp + = 1
60 Console.WriteLine ("Am in" & Me.mName & _
61 ".Aktuell temperatur är" & mHouse.HouseTemp)
62 Annat
63 Console.WriteLine ("Am in" & Me.mName & _
64 ".Aktuell temperatur är" & mHouse.HouseTemp)
65 "Gör ingenting, temperaturen är normal
66 Avsluta If
67 Catch tae As ThreadlnterruptedException
68 "Passiv väntan har avbrutits
69 Catch e Som undantag
70 "Andra undantag
71 Avsluta försök
72 End Sub
73 Slutklass
Ris. 10.6.
Flertrådsproblem
Sub Main-proceduren (raderna 4-7) skapar ett "hus" med tio "rum". Husklassen anger en maximal temperatur på 75 grader Fahrenheit (cirka 24 grader Celsius). Raderna 13-28 definierar en ganska komplex huskonstruktör. Raderna 18-27 är nyckeln till att förstå programmet. Rad 20 skapar ytterligare ett rumsobjekt, och en referens till husobjektet skickas till konstruktören så att rumsobjektet kan referera till det vid behov. Rad 21-23 startar tio strömmar för att justera temperaturen i varje rum. Rumsklassen definieras på rad 38-73. House coxpa referenslagras i variabeln mHouse i rumsklasskonstruktorn (rad 43). Koden för kontroll och justering av temperaturen (rad 50-66) ser enkel och naturlig ut, men som du snart kommer att se är detta intryck bedrägeri! Observera att den här koden är inslagen i ett Try-Catch-block eftersom programmet använder Sleep-metoden.
Knappast någon skulle gå med på att leva i temperaturer på 105 grader Fahrenheit (40,5 till 24 grader Celsius). Vad hände? Problemet är relaterat till följande rad:
Om mHouse.HouseTemp< mHouse.MAX_TEMP - 5 Then
Och följande händer: först kontrolleras temperaturen av flöde 1. Han ser att temperaturen är för låg och höjer den med 5 grader. Tyvärr, innan temperaturen stiger, avbryts ström 1 och kontrollen överförs till ström 2. Ström 2 kontrollerar samma variabel som har inte ändrats ännu flöde 1. Flöde 2 förbereder sig alltså också för att höja temperaturen med 5 grader, men hinner inte göra detta och går även i vänteläge. Processen fortsätter tills ström 1 aktiveras och går vidare till nästa kommando - höjning av temperaturen med 5 grader. Höjningen upprepas när alla 10 bäckar är aktiverade, och de boende i huset kommer att få det dåligt.
Lösning på problemet: synkronisering
I det föregående programmet uppstår en situation när programmets utdata beror på exekveringsordningen för trådarna. För att bli av med det måste du se till att kommandon gillar
Om mHouse.HouseTemp< mHouse.MAX_TEMP - 5 Then...
bearbetas helt av den aktiva tråden innan den avbryts. Denna egenskap kallas atomär skam - ett kodblock måste exekveras av varje tråd utan avbrott, som en atomenhet. En grupp av kommandon, kombinerade till ett atomblock, kan inte avbrytas av trådschemaläggaren förrän den är klar. Alla flertrådiga programmeringsspråk har sina egna sätt att säkerställa atomicitet. I VB .NET är det enklaste sättet att använda kommandot SyncLock att skicka in en objektvariabel när den anropas. Gör små ändringar i ChangeTemperature-proceduren från föregående exempel, så kommer programmet att fungera bra:
Private Sub ChangeTemperature () SyncLock (mHouse)
Prova
Om mHouse.HouseTemp< mHouse.MAXJTEMP -5 Then
Thread.Sleep (200)
mHouse.HouseTemp + = 5
Console.WriteLine ("Am in" & Me.mName & _
".Aktuell temperatur är" & mHouse.HouseTemp)
Själv
mHouse.HouseTemp< mHouse. MAX_TEMP Then
Thread.Sleep (200) mHouse.HouseTemp + = 1
Console.WriteLine ("Am in" & Me.mName & _ ".Current temperature is" & mHouse.HomeTemp) Else
Console.WriteLineC "Am in" & Me.mName & _ ".Nuvarande temperatur är" & mHouse.HouseTemp)
"Gör ingenting, temperaturen är normal
End If Catch tie As ThreadlnterruptedException
"Passiv väntan avbröts av Catch e As Exception
"Andra undantag
Avsluta försök
Avsluta SyncLock
Avsluta Sub
SyncLock-blockkoden exekveras atomiskt. Åtkomst till den från alla andra trådar kommer att stängas tills den första tråden släpper låset med kommandot End SyncLock. Om en tråd i ett synkroniserat block går in i ett passivt vänteläge förblir låset tills tråden avbryts eller återupptas.
Korrekt användning av SyncLock-kommandot håller din programtråd säker. Tyvärr har överanvändning av SyncLock en negativ inverkan på prestandan. Synkronisering av kod i ett flertrådigt program minskar hastigheten på dess arbete med flera gånger. Synkronisera endast den mest nödvändiga koden och släpp låset så snart som möjligt.
Basinsamlingsklasserna är osäkra i flertrådade applikationer, men .NET Framework innehåller trådsäkra versioner av de flesta samlingsklasserna. I dessa klasser är koden för potentiellt farliga metoder innesluten i SyncLock-block. Trådsäkra versioner av samlingsklasser bör användas i flertrådade program där dataintegriteten äventyras.
Det återstår att nämna att villkorsvariabler enkelt implementeras med hjälp av kommandot SyncLock. För att göra detta behöver du bara synkronisera skrivningen till den gemensamma booleska egenskapen, tillgänglig för läsning och skrivning, eftersom det görs i följande fragment:
Public Class ConditionVariable
Privat delat skåp som objekt = nytt objekt ()
Privat delad mOK Som boolesk delad
Egenskapen TheConditionVariable () Som Boolean
Skaffa sig
Återgå mOK
Avsluta Get
Ställ in (ByVal Value As Boolean) SyncLock (skåp)
mOK = Värde
Avsluta SyncLock
Slutuppsättning
Avsluta egendom
Slutklass
SyncLock Command and Monitor Class
Användningen av kommandot SyncLock involverar några finesser som inte visades i de enkla exemplen ovan. Så, valet av synkroniseringsobjektet spelar en mycket viktig roll. Prova att köra det föregående programmet med kommandot SyncLock (Me) istället för SyncLock (mHouse). Temperaturen stiger över tröskeln igen!
Kom ihåg att kommandot SyncLock synkroniserar med objekt, skickas som en parameter, inte av kodavsnittet. SyncLock-parametern fungerar som en dörr för åtkomst till det synkroniserade fragmentet från andra trådar. Kommandot SyncLock (Me) öppnar faktiskt flera olika "dörrar", vilket är precis vad du försökte undvika med synkronisering. Moral:
För att skydda delad data i en flertrådad applikation måste kommandot SyncLock synkronisera ett objekt åt gången.
Eftersom synkronisering är associerad med ett specifikt objekt är det i vissa situationer möjligt att oavsiktligt låsa andra fragment. Låt oss säga att du har två synkroniserade metoder, första och andra, och båda metoderna är synkroniserade på bigLock-objektet. När tråd 1 går in i metod först och fångar bigLock, kommer ingen tråd att kunna gå in i metod tvåa eftersom åtkomsten till den redan är begränsad till tråd 1!
Funktionaliteten för kommandot SyncLock kan ses som en delmängd av funktionaliteten för klassen Monitor. Monitor-klassen är mycket anpassningsbar och kan användas för att lösa icke-triviala synkroniseringsuppgifter. SyncLock-kommandot är en ungefärlig analog till Enter- och Exi t-metoderna i Monitor-klassen:
Prova
Monitor.Enter (theObject) Slutligen
Monitor.Exit (theObject)
Avsluta försök
För vissa standardoperationer (öka/minska en variabel, utbyta innehållet i två variabler) tillhandahåller .NET Framework klassen Interlocked, vars metoder utför dessa operationer på atomnivå. Med klassen Interlocked är dessa operationer mycket snabbare än att använda kommandot SyncLock.
Förregling
Under synkronisering ställs låset på objekt, inte trådar, så vid användning annorlunda föremål att blockera annorlunda kodbitar i program ibland uppstår ganska icke-triviala fel. Tyvärr är synkronisering på ett enda objekt i många fall helt enkelt oacceptabelt, eftersom det kommer att leda till att trådar blockeras för ofta.
Tänk på situationen förregling(deadlock) i sin enklaste form. Föreställ dig två programmerare vid middagsbordet. Tyvärr har de bara en kniv och en gaffel för två. Förutsatt att du behöver både en kniv och en gaffel för att äta, är två situationer möjliga:
- En programmerare lyckas ta en kniv och gaffel och börjar äta. När han är mätt lägger han middagen åt sidan, och sedan kan en annan programmerare ta dem.
- En programmerare tar kniven och den andra tar gaffeln. Ingen av dem kan börja äta om den andra inte ger upp sin apparat.
I ett flertrådigt program kallas denna situation ömsesidig blockering. De två metoderna synkroniseras på olika objekt. Tråd A fångar objekt 1 och går in i programdelen som skyddas av detta objekt. Tyvärr, för att det ska fungera, behöver det tillgång till kod som skyddas av ett annat synkroniseringslås med ett annat synkroniseringsobjekt. Men innan den hinner mata in ett fragment som är synkroniserat av ett annat objekt går ström B in i det och fångar det här objektet. Nu kan tråd A inte komma in i det andra fragmentet, tråd B kan inte komma in i det första fragmentet, och båda trådarna är dömda att vänta på obestämd tid. Ingen tråd kan fortsätta att köras eftersom det önskade objektet aldrig kommer att frigöras.
Diagnos av dödlägen kompliceras av det faktum att de kan uppstå i relativt sällsynta fall. Allt beror på i vilken ordning schemaläggaren allokerar CPU-tid till dem. Det är möjligt att i de flesta fall kommer synkroniseringsobjekt att fångas i en ordning utan dödläge.
Följande är en implementering av den dödläge som just beskrivits. Efter en kort diskussion av de mest grundläggande punkterna kommer vi att visa hur man identifierar en dödlägessituation i trådfönstret:
1 Alternativ Strikt På
2 Importerar System.Trådning
3 Modul Modul
4 Sub Main ()
5 Dim Tom som ny programmerare ("Tom")
6 Dim Bob som ny programmerare ("Bob")
7 Dim aThreadStart As New ThreadStart (AddressOf Tom.Eat)
8 Dim en tråd som ny tråd (a trådstart)
9 aThread.Name = "Tom"
10 Dim bThreadStart As New ThreadStarttAddressOf Bob.Eat)
11 Dim bTråd som ny tråd (bTrådstart)
12 bThread.Name = "Bob"
13 aThread.Start ()
14 bThread.Start ()
15 End Sub
16 Slutmodul
17 Public Class Gaffel
18 Privat delad mForkAvaiTable As Boolean = True
19 Privat delad mowner As String = "Ingen"
20 Private Readonly Property OwnsUtensil () Som sträng
21 Få
22 Återgå mÄgare
23 Slut Get
24 Slutfastighet
25 Public Sub GrabForktByVal a As Programmer)
26 Console.Writel_ine (Thread.CurrentThread.Name & _
"försöker ta tag i gaffeln.")
27 Console.WriteLine (Me.OwnsUtensil & "har gaffeln"). ...
28 Monitor.Ange (Me) "SyncLock (aFork)"
29 Om mForkAvailable Då
30 a.HasFork = Sant
31 mÄgare = a.Mitt Namn
32 mForkAvailable =
Falsk
33 Console.WriteLine (a.MyName & "just got the fork.waiting")
34 Försök
Thread.Sleep (100) Catch e As Exception Console.WriteLine (e.StackTrace)
Avsluta försök
35 Avsluta If
36 Monitor.Exit (Me)
Avsluta SyncLock
37 Slut Sub
38 Slutklass
39 Public Class Kniv
40 Private Shared mKnifeAvailable As Boolean = True
41 Privat delad mowner As String = "Ingen"
42 Privat skrivskyddad egendom OwnsUtensi1 () Som sträng
43 Få
44 Återgå mÄgare
45 Slut Get
46 Slutfastighet
47 Public Sub GrabKnifetByVal a As Programmer)
48 Console.WriteLine (Thread.CurrentThread.Name & _
"försöker greppa kniven.")
49 Console.WriteLine (Me.OwnsUtensil & "har kniven.")
50 Monitor.Enter (Me) "SyncLock (aKnife)"
51 Om mKnifeAvailable Då
52 mKnifeAvailable = False
53 a.HasKnife = Sant
54 mÄgare = a.Mitt Namn
55 Console.WriteLine (a.MyName & "just got the knife.waiting")
56 Försök
Tråd.Sömn (100)
Catch e Som undantag
Console.WriteLine (e.StackTrace)
Avsluta försök
57 Avsluta If
58 Monitor.Exit (Me)
59 End Sub
60 Slutklass
61 Public Class Programmerare
62 Privat mName As String
63 Privat delad mFork As Fork
64 Privat delad mKnife As Knife
65 Privat mHasKnife As Boolean
66 Privat mHasFork As Boolean
67 Shared Sub New ()
68 mFork = New Fork ()
69 mKniv = Ny kniv ()
70 End Sub
71 Public Sub New (ByVal theName As String)
72 mName = theName
73 End Sub
74 Allmän skrivskyddad egenskap MyName () Som sträng
75 Få
76 Returnera mName
77 Slut Get
78 Slutfastighet
79 Offentlig egendom HasKnife () As Boolean
80 Få
81 Returnera mHasKnife
82 Slut Get
83 Set (ByVal Value As Boolean)
84 mHasKnife = Värde
85 Slutsats
86 Slutfastighet
87 Offentlig egendom HasFork () As Boolean
88 Skaffa
89 Återgå mHasFork
90 Slut Get
91 Set (ByVal Value As Boolean)
92 mHasFork = Värde
93 Slutsats
94 Slutfastighet
95 Public Sub Eat ()
96 Gör tills Me.HasKnife Och Me.HasFork
97 Console.Writeline (Thread.CurrentThread.Name & "finns i tråden.")
98 Om Rnd ()<
0.5 Then
99 mFork.GrabFork (Me)
100 annat
101 mKnife.GrabKnife (Me)
102 Avsluta If
103 Slinga
104 MsgBox (Me.MyName & "kan äta!")
105 mKniv = Ny kniv ()
106 mFork = New Fork ()
107 End Sub
108 Slutklass
Huvudproceduren Main (raderna 4-16) skapar två instanser av Programmer-klassen och startar sedan två trådar för att exekvera den kritiska Eat-metoden för Programmer-klassen (raderna 95-108), som beskrivs nedan. Huvudproceduren ställer in namnen på trådarna och ställer in dem; förmodligen är allt som händer förståeligt och utan kommentarer.
Koden för Fork-klassen ser mer intressant ut (raderna 17-38) (en liknande Knife-klass definieras på raderna 39-60). Raderna 18 och 19 anger värdena för de vanliga fälten, genom vilka du kan ta reda på om kontakten är tillgänglig för närvarande, och om inte, vem som använder den. ReadOnly-egenskapen OwnUtensi1 (rad 20-24) är avsedd för den enklaste överföringen av information. Centralt i Fork-klassen är GrabFork "grab the fork"-metoden, definierad på rad 25-27.
- Raderna 26 och 27 skriver helt enkelt ut felsökningsinformation till konsolen. I huvudkoden för metoden (raderna 28-36) synkroniseras åtkomst till gaffeln efter objektbälte mig. Eftersom vårt program bara använder en gaffel, säkerställer Me sync att inga två trådar kan ta tag i den samtidigt. Kommandot Slee "p (i blocket som börjar på rad 34) simulerar fördröjningen mellan att greppa en gaffel/kniv och börja äta. Observera att Sleep-kommandot inte låser upp objekt och bara accelererar blockerat låsläge!
Det mest intressanta är dock koden för programmeringsklassen (rad 61-108). Raderna 67-70 definierar en generisk konstruktör för att säkerställa att det bara finns en gaffel och en kniv i programmet. Fastighetskoden (rad 74-94) är enkel och kräver inga kommentarer. Det viktigaste händer i Eat-metoden, som exekveras av två separata trådar. Processen fortsätter i en slinga tills någon ström fångar gaffeln tillsammans med kniven. På rad 98-102 tar objektet slumpmässigt tag i gaffeln/kniven med Rnd-anropet, vilket är det som orsakar dödläget. Följande händer:
Tråden som kör Eat-metoden för Toten anropas och går in i loopen. Han tar tag i kniven och går in i ett väntande tillstånd. - Tråden som kör Bob's Eat-metoden anropas och går in i loopen. Den kan inte ta tag i kniven, men den tar tag i gaffeln och går in i ett väntande tillstånd.
- Tråden som kör Eat-metoden för Toten anropas och går in i loopen. Han försöker ta tag i gaffeln, men Bob har redan tagit tag i gaffeln; tråden går in i ett väntande tillstånd.
- Tråden som kör Bob's Eat-metoden anropas och går in i loopen. Han försöker greppa kniven, men kniven är redan fångad av föremålet Thoth; tråden går in i ett väntande tillstånd.
Allt detta fortsätter i det oändliga - vi står inför en typisk situation med dödläge (försök att köra programmet så ser du att ingen kan äta på det här sättet).
Du kan också se om ett dödläge har inträffat i trådfönstret. Kör programmet och avbryt det med Ctrl + Break-tangenterna. Inkludera Me-variabeln i viewporten och öppna strömningsfönstret. Resultatet ser ungefär ut som det som visas i fig. 10.7. Från figuren kan du se att Bobs tråd har tagit tag i en kniv, men den har ingen gaffel. Högerklicka i fönstret Trådar på raden Tot och välj kommandot Växla till tråd från snabbmenyn. Utsiktsporten visar att Thoth-strömmen har en gaffel, men ingen kniv. Detta är förstås inget hundraprocentigt bevis, men ett sådant beteende får dig åtminstone att misstänka att något var fel.
Om alternativet med synkronisering av ett objekt (som i programmet med att öka -temperaturen i huset) inte är möjligt, för att förhindra ömsesidiga låsningar, kan du numrera synkroniseringsobjekten och alltid fånga dem i en konstant ordning. Låt oss fortsätta analogin med middagsprogrammeraren: om tråden alltid tar kniven först och sedan gaffeln kommer det inte att finnas några problem med låsning. Den första bäcken som tar tag i kniven kommer att kunna äta normalt. Översatt till språket för programströmmar betyder detta att infångning av objekt 2 endast är möjlig om objekt 1 först fångas.
Ris. 10.7.
Analys av låsningar i trådfönstret
Därför, om vi tar bort anropet till Rnd på linje 98 och ersätter det med kodavsnittet
mFork.GrabFork (Jag)
mKnife.GrabKnife (Jag)
dödläget försvinner!
Samarbeta om data när den skapas
I flertrådade applikationer finns det ofta en situation där trådar inte bara fungerar med delad data, utan också väntar på att den ska visas (det vill säga tråd 1 måste skapa data innan tråd 2 kan använda den). Eftersom data delas måste åtkomsten till den synkroniseras. Det är också nödvändigt att tillhandahålla medel för att meddela de väntande trådarna om uppkomsten av färdiga data.
Denna situation brukar kallas leverantör/konsumentproblemet. Tråden försöker komma åt data som ännu inte existerar, så den måste överföra kontrollen till en annan tråd som skapar den nödvändiga informationen. Problemet löses med följande kod:
- Tråd 1 (konsument) vaknar, anger en synkroniserad metod, letar efter data, hittar den inte och går in i ett vänteläge. Preliminärtfysiskt måste han ta bort blockeringen för att inte störa arbetet med tillförselgängan.
- Tråd 2 (leverantör) anger en synkroniserad metod som frigörs av tråd 1, skapar data för ström 1 och på något sätt meddelar ström 1 om förekomsten av data. Den släpper sedan låset så att tråd 1 kan bearbeta den nya datan.
Försök inte lösa detta problem genom att ständigt anropa tråd 1 och kontrollera villkoret för en villkorsvariabel vars värde är> satt av tråd 2. Detta beslut kommer att allvarligt påverka prestandan för ditt program, eftersom tråd 1 i de flesta fall kommer att anropa utan anledning; och tråd 2 kommer att vänta så ofta att det tar slut på tid för att skapa data.
Leverantör/konsumentrelationer är mycket vanliga, så speciella primitiver skapas för sådana situationer i flertrådade programmeringsklassbibliotek. I NET kallas dessa primitiver Wait och Pulse-PulseAl 1 och är en del av Monitor-klassen. Figur 10.8 illustrerar den situation vi är på väg att programmera. Programmet organiserar tre trådköer: en väntekö, en blockeringskö och en exekveringskö. Trådschemaläggaren tilldelar inte CPU-tid till trådar som står i väntekön. För att en tråd ska tilldelas tid måste den flyttas till exekveringskön. Som ett resultat är applikationens arbete organiserat mycket mer effektivt än med den vanliga polling av en villkorsvariabel.
I pseudokod formuleras datakonsumentspråket enligt följande:
"Inträde i ett synkroniserat block av följande typ
Medan inga data
Gå till väntekön
Slinga
Om det finns data, bearbeta det.
Lämna synkroniserat block
Omedelbart efter att Vänta-kommandot har utförts avbryts tråden, låset släpps och tråden går in i väntekön. När låset släpps får tråden i exekveringskön köras. Med tiden kommer en eller flera blockerade trådar att skapa den data som behövs för driften av tråden som står i väntekön. Eftersom datavalidering utförs i en loop, sker övergången till att använda data (efter loopen) endast när det finns data redo för bearbetning.
I pseudokod ser dataleverantörens formspråk ut så här:
"Kommer in i ett synkroniserat vyblock
Medan data INTE behövs
Gå till väntekön
Else Producera Data
När data är klar, ring Pulse-PulseAll.
för att flytta en eller flera trådar från blockeringskön till exekveringskön. Lämna det synkroniserade blocket (och återgå till körningskön)
Anta att vårt program simulerar en familj med en förälder som tjänar pengar och ett barn som spenderar de pengarna. När pengarna är slutdet visar sig att barnet måste vänta på att ett nytt belopp kommer. Mjukvaruimplementeringen av denna modell ser ut så här:
1 Alternativ Strikt På
2 Importerar System.Trådning
3 Modul Modul
4 Sub Main ()
5 Dim theFamily As New Family ()
6 theFamily.StartltsLife ()
7 End Sub
8 Avsluta fjodule
9
10 Public Class Family
11 Privata mMoney Som heltal
12 Privat mvecka som heltal = 1
13 Public Sub StartltsLife ()
14 Dim aThreadStart As New ThreadStarUAddressOf Me.Produce)
15 Dim bThreadStart As New ThreadStarUAddressOf Me.Consume)
16 Dim aThread As New Thread (aThreadStart)
17 Dim bTråd som ny tråd (bTrådstart)
18 aThread.Name = "Producera"
19 aThread.Start ()
20 bThread.Name = "Konsumera"
21 bTråd. Start ()
22 End Sub
23 Offentlig egendom TheWeek () Som heltal
24 Få
25 Retur mvecka
26 Slut Get
27 Set (ByVal Value As Heltal)
28 mvecka - Värde
29 Slutsats
30 Slutfastighet
31 Offentlig egendom OurMoney () Som heltal
32 Få
33 Returnera mMoney
34 Slut Get
35 Set (ByVal Value As Heltal)
36 mPengar = Värde
37 Slutsats
38 Slutfastighet
39 Offentlig underproduktion ()
40 trådar. sömn (500)
41 Gör
42 Monitor.Enter (Me)
43 Do While Me.OurMoney> 0
44 Monitor.Wait (Me)
45 Slinga
46 Me.OurMoney = 1000
47 Monitor.PulseAll (Me)
48 Monitor.Exit (Me)
49 Slinga
50 End Sub
51 Offentlig underkonsumtion ()
52 MsgBox ("Är i konsumtionstråden")
53 Gör
54 Monitor.Enter (Me)
55 Do While Me.OurMoney = 0
56 Monitor.Wait (Me)
57 Slinga
58 Console.WriteLine ("Kära förälder jag har precis spenderat alla dina" & _
pengar i veckan "& TheWeek)
59 Veckan + = 1
60 Om TheWeek = 21 * 52 Då System.Environment.Exit (0)
61 Me.OurMoney = 0
62 Monitor.PulseAll (Mig)
63 Monitor.Exit (Me)
64 Slinga
65 End Sub
66 Slutklass
StartltsLife-metoden (rad 13-22) förbereder för att starta Producera och Konsumera strömmar. Det viktigaste händer i strömmarna Producera (rad 39-50) och Konsum (rad 51-65). Sub Produce-proceduren kontrollerar tillgången på pengar, och om det finns pengar går de till väntekön. Annars genererar föräldern pengar (rad 46) och meddelar objekten i väntekön om en förändring i situationen. Observera att anropet till Pulse-Pulse All träder i kraft först när låset släpps med kommandot Monitor.Exit. Omvänt kontrollerar Sub Consume-proceduren tillgången på pengar, och om det inte finns några pengar, meddelar den blivande föräldern om det. Linje 60 avslutar helt enkelt programmet efter 21 villkorliga år; ringer System. Environment.Exit (0) är .NET-analogen för End-kommandot (end-kommandot stöds också, men till skillnad från System. Environment. Exit returnerar det ingen exit-kod till operativsystemet).
Trådar som läggs i väntekö måste frigöras av andra delar av ditt program. Det är av denna anledning som vi föredrar att använda PulseAll över Pulse. Eftersom det inte är känt i förväg vilken tråd som kommer att aktiveras när Pulse 1 anropas, om det är relativt få trådar i kön, kan du ringa PulseAll lika bra.
Multithreading i grafikprogram
Vår diskussion om multithreading i GUI-applikationer börjar med ett exempel som förklarar vad multithreading i GUI-applikationer är till för. Skapa ett formulär med två knappar Start (btnStart) och Avbryt (btnCancel), som visas i Fig. 10.9. Genom att klicka på Start-knappen genereras en klass som innehåller en slumpmässig sträng på 10 miljoner tecken och en metod för att räkna förekomsterna av bokstaven "E" i den långa strängen. Notera användningen av StringBuilder-klassen för effektivare skapande av långa strängar.
Steg 1
Tråd 1 märker att det inte finns några data för det. Den anropar Vänta, släpper låset och går till väntekön.
Steg 2
När låset släpps lämnar tråd 2 eller tråd 3 blockkön och går in i ett synkroniserat block och hämtar låset
Steg 3
Låt oss säga att tråd 3 går in i ett synkroniserat block, skapar data och anropar Pulse-Pulse All.
Omedelbart efter att den lämnar blocket och släpper låset, flyttas tråd 1 till exekveringskön. Om tråd 3 anropar Pluse kommer endast en in i exekveringsköntråd, när Pluse All anropas går alla trådar till exekveringskön.
Ris. 10.8.
Leverantör/konsumentproblem
Ris. 10.9.
Multithreading i en enkel GUI-applikation
Importerar System.Text
Offentlig klass slumpmässiga tecken
Privat m_Data Som StringBuilder
Privat mjength, m_count Som heltal
Public Sub New (ByVal n Som heltal)
m_Längd = n -1
m_Data = New StringBuilder (m_length) MakeString ()
Avsluta Sub
Private Sub MakeString ()
Dim i As Integer
Dim myRnd As New Random ()
För i = 0 Till m_längd
"Generera ett slumpmässigt tal mellan 65 och 90,
"konvertera det till versaler
"och bifoga till StringBuilder-objektet
m_Data.Append (Chr (myRnd.Next (65.90)))
Nästa
Avsluta Sub
Public Sub StartCount ()
GetEes ()
Avsluta Sub
Private Sub GetEes ()
Dim i As Integer
För i = 0 Till m_längd
Om m_Data.Chars (i) = CChar ("E") Då
m_count + = 1
Avsluta om nästa
m_CountDone = Sant
Avsluta Sub
Allmän skrivskyddad
Property GetCount () Som heltal Get
Om inte (m_CountDone) Då
Returnera m_count
Avsluta If
Slut Get End Property
Allmän skrivskyddad
Property IsDone () As Boolean Get
Lämna tillbaka
m_CountDone
Avsluta Get
Avsluta egendom
Slutklass
Det är mycket enkel kod kopplad till de två knapparna på formuläret. Proceduren btn-Start_Click instansierar ovanstående RandomCharacters-klass, som kapslar in en sträng med 10 miljoner tecken:
Privat Sub btnStart_Click (ByVal avsändare Som System.Object.
ByVal e As System.EventArgs) Hanterar btnSTart.Click
Dim RC som nya slumpmässiga tecken (10000000)
RC.StartCount ()
MsgBox ("Antalet es är" & RC.GetCount)
Avsluta Sub
Knappen Avbryt visar en meddelanderuta:
Privat Sub btnCancel_Click (ByVal avsändare Som System.Object._
ByVal e As System.EventArgs) Hanterar btnCancel.Click
MsgBox ("Räkna avbruten!")
Avsluta Sub
När programmet körs och Start-knappen trycks ned visar det sig att Avbryt-knappen inte svarar på användarinmatning eftersom den kontinuerliga slingan hindrar knappen från att hantera händelsen den tar emot. Detta är oacceptabelt i moderna program!
Det finns två möjliga lösningar. Det första alternativet, välkänt från tidigare VB-versioner, undviker multithreading: DoEvents-anropet ingår i slingan. I NET ser det här kommandot ut så här:
Application.DoEvents ()
I vårt exempel är detta definitivt inte önskvärt - vem vill bromsa ett program med tio miljoner DoEvents-samtal! Om du istället allokerar slingan till en separat tråd kommer operativsystemet att växla mellan trådar och knappen Avbryt förblir fungerande. Implementeringen med en separat tråd visas nedan. För att tydligt visa att Avbryt-knappen fungerar, när vi klickar på den avslutar vi helt enkelt programmet.
Nästa steg: Visa räkneknapp
Låt oss säga att du bestämde dig för att visa din kreativa fantasi och ge formen det utseende som visas i fig. 10.9. Observera: knappen Visa antal är inte tillgänglig ännu.
Ris. 10.10.
Låst knappform
En separat tråd förväntas göra räkningen och låsa upp den otillgängliga knappen. Detta kan givetvis göras; dessutom uppstår en sådan uppgift ganska ofta. Tyvärr kommer du inte att kunna agera på det mest uppenbara sättet - länka den sekundära tråden till GUI-tråden genom att hålla en länk till ShowCount-knappen i konstruktorn, eller till och med använda en standarddelegat. Med andra ord, aldrig använd inte alternativet nedan (grundläggande felaktig linjerna är i fetstil).
Offentlig klass slumpmässiga tecken
Privat m_0ata Som StringBuilder
Privat m_CountDone As Boolean
Privat mjength. m_count Som heltal
Privat m_Button Som Windows.Forms.Button
Public Sub New (ByVa1 n Som heltal, _
ByVal b As Windows.Forms.Button)
m_längd = n - 1
m_Data = New StringBuilder (mJength)
m_Button = b MakeString ()
Avsluta Sub
Private Sub MakeString ()
Dim I som heltal
Dim myRnd As New Random ()
För I = 0 Till m_längd
m_Data.Append (Chr (myRnd.Next (65.90)))
Nästa
Avsluta Sub
Public Sub StartCount ()
GetEes ()
Avsluta Sub
Private Sub GetEes ()
Dim I som heltal
För I = 0 Till mjength
Om m_Data.Chars (I) = CChar ("E") Då
m_count + = 1
Avsluta om nästa
m_CountDone = Sant
m_Button.Enabled = Sant
Avsluta Sub
Allmän skrivskyddad
Egenskap GetCount () Som heltal
Skaffa sig
Om inte (m_CountDone) Då
Kasta nytt undantag ("Räkna ännu inte gjort") Annars
Returnera m_count
Avsluta If
Avsluta Get
Avsluta egendom
Allmän skrivskyddad egendom är klar () som boolesk
Skaffa sig
Returnera m_CountDone
Avsluta Get
Avsluta egendom
Slutklass
Det är troligt att den här koden kommer att fungera i vissa fall. Ändå:
- Interaktionen mellan den sekundära tråden och tråden som skapar GUI kan inte organiseras uppenbar innebär att.
- Aldrig modifiera inte element i grafikprogram från andra programströmmar. Alla ändringar bör endast ske i tråden som skapade GUI.
Om du bryter mot dessa regler, vi vi garanterar att subtila, subtila buggar kommer att uppstå i dina flertrådade grafikprogram.
Det kommer också att misslyckas med att organisera interaktionen mellan objekt med hjälp av händelser. 06-event-arbetaren kör på samma tråd som RaiseEvent kallades så händelser kommer inte att hjälpa dig.
Fortfarande säger sunt förnuft att grafiska applikationer måste ha ett sätt att modifiera element från en annan tråd. I NET Framework finns det ett trådsäkert sätt att anropa metoder för GUI-applikationer från en annan tråd. En speciell typ av Method Invoker-delegat från System.Windows-namnrymden används för detta ändamål. Blanketter. Följande utdrag visar en ny version av GetEes-metoden (ändrade rader i fetstil):
Private Sub GetEes ()
Dim I som heltal
För I = 0 Till m_längd
Om m_Data.Chars (I) = CChar ("E") Då
m_count + = 1
Avsluta om nästa
m_CountDone = Sant försök
Dim mylnvoker som ny metodlnvoker (AddressOf UpDateButton)
myInvoker.Invoke () Catch e As ThreadlnterruptedException
"Fel
Avsluta försök
Avsluta Sub
Public Sub UpDateButton ()
m_Button.Enabled = Sant
Avsluta Sub
Inter-thread-anrop till knappen görs inte direkt, utan genom Method Invoker. .NET Framework garanterar att det här alternativet är trådsäkert.
Varför finns det så många problem med flertrådsprogrammering?
Nu när du har en viss förståelse för multithreading och de potentiella problem som är förknippade med det, beslutade vi att det skulle vara lämpligt att besvara frågan i rubriken till detta underavsnitt i slutet av detta kapitel.
En av anledningarna är att multithreading är en icke-linjär process, och vi är vana vid en linjär programmeringsmodell. Till en början är det svårt att vänja sig vid själva tanken att programexekveringen kan avbrytas slumpmässigt, och kontrollen kommer att överföras till annan kod.
Det finns dock en annan, mer grundläggande anledning: nuförtiden programmerar programmerare alltför sällan i assembler, eller åtminstone tittar på kompilatorns demonterade utdata. Annars skulle det vara mycket lättare för dem att vänja sig vid tanken att dussintals monteringsinstruktioner kan motsvara ett kommando på ett högnivåspråk (som VB .NET). Tråden kan avbrytas efter någon av dessa instruktioner, och därför mitt i ett kommando på hög nivå.
Men det är inte allt: moderna kompilatorer optimerar programprestanda och datorhårdvara kan störa minneshanteringen. Som ett resultat kan kompilatorn eller hårdvaran ändra ordningen på kommandon som anges i programmets källkod utan din vetskap [ Många kompilatorer optimerar cyklisk kopiering av arrayer som för i = 0 till n: b (i) = a (i): ncxt. Kompilatorn (eller till och med en specialiserad minneshanterare) kan helt enkelt skapa en array och sedan fylla den med en enda kopieringsoperation istället för att kopiera enskilda element många gånger!].
Förhoppningsvis kommer dessa förklaringar att hjälpa dig att bättre förstå varför flertrådsprogrammering orsakar så många problem - eller åtminstone mindre överraskning över det konstiga beteendet hos dina flertrådade program!
Andrey Kolesov
Börja överväga principerna för att skapa flertrådade applikationer för Microsoft .NET Framework, låt oss bara göra en reservation: även om alla exempel ges i Visual Basic .NET, är metoden för att skapa sådana program i allmänhet densamma för alla programmeringsspråk som stöder .NET, inklusive C #. VB valdes för att demonstrera tekniken för att skapa flertrådade applikationer främst för att tidigare versioner av detta verktyg inte gav en sådan möjlighet.
Se upp: Visual Basic .NET kan också göra DETTA!
Som ni vet har Visual Basic (till och med version 6.0) aldrig tidigare tillåtit att skapa flertrådade programvarukomponenter (EXE, ActiveX DLL och OCX). Kom ihåg att COM-arkitekturen inkluderar tre olika gängningsmodeller: enkelgängad, enkelgängad lägenhet (STA) och flertrådig lägenhet. VB 6.0 låter dig skapa program av de två första typerna. STA-versionen tillhandahåller ett pseudo-flertrådigt läge - flera trådar fungerar verkligen parallellt, men samtidigt är programkoden för var och en av dem skyddad från åtkomst till den från utsidan (särskilt trådar kan inte använda delade resurser).
Visual Basic .NET kan nu implementera gratis trådning i sin ursprungliga form. Mer exakt, i .NET, stöds detta läge på nivån för de gemensamma klassbiblioteken Class Library och Common Language Runtime. Som ett resultat fick VB.NET, tillsammans med andra programmeringsspråk .NET, tillgång till dessa funktioner.
Vid ett tillfälle reagerade VB-utvecklargemenskapen, som uttryckte missnöje med många framtida innovationer av detta språk, med stort godkännande på nyheten att det med hjälp av den nya versionen av verktyget kommer att vara möjligt att skapa flertrådade program (se "Väntar på Visual Studio .NET", "BYTE / Ryssland "Nr 1/2001). Men många experter uttryckte mer återhållsamma bedömningar av denna innovation. Här är till exempel åsikten från Dan Appleman, en välkänd utvecklare och författare till många böcker för VB-programmerare: på grund av mänskliga snarare än tekniska faktorer ... Jag är rädd för multithreading i VB.NET eftersom VB-programmerare vanligtvis inte gör det. har erfarenhet av att designa och felsöka flertrådade applikationer."
Faktum är att, precis som andra programmeringsverktyg på låg nivå (till exempel samma Win API), ger fri multithreading å ena sidan fler möjligheter att skapa skalbara lösningar med hög prestanda, och å andra sidan ställer det högre krav på utvecklarnas kvalifikationer. Dessutom förvärras problemet här av det faktum att sökningen efter fel i en flertrådad applikation är mycket svår, eftersom de oftast uppträder slumpmässigt, som ett resultat av en specifik korsning av parallella beräkningsprocesser (det är ofta helt enkelt omöjligt att reproducera sådana en situation igen). Det är därför som metoderna för traditionell felsökning av program i form av deras upprepade körning vanligtvis inte hjälper i det här fallet. Och det enda sättet att säkert använda multithreading är att designa din applikation väl och följa alla klassiska principer för "bra programmering".
Problemet med VB-programmerare är att även om många av dem är ganska erfarna proffs och är väl medvetna om fallgroparna med multithreading, kan användningen av VB6 dämpa deras vaksamhet. När allt kommer omkring, när vi anklagar VB för begränsningar, glömmer vi ibland att många av begränsningarna bestämdes av de förbättrade säkerhetsfunktionerna i detta verktyg, som förhindrar eller eliminerar utvecklarfel. Till exempel skapar VB6 automatiskt en separat kopia av alla globala variabler för varje tråd, vilket förhindrar eventuella konflikter mellan dem. I VB.NET flyttas sådana problem helt över på programmerarens axlar. Man bör också komma ihåg att användningen av en flertrådig modell istället för en enkeltrådad inte alltid leder till en ökning av programprestanda, prestanda kan till och med minska (även i flerprocessorsystem!).
Allt ovanstående bör dock inte ses som ett råd att inte bråka med multithreading. Du behöver bara ha en god uppfattning om när sådana lägen är värda att använda, och förstå att ett kraftfullare utvecklingsverktyg alltid ställer högre krav på programmerarens kvalifikationer.
Parallell bearbetning i VB6
Naturligtvis var det möjligt att organisera pseudo-parallell databehandling med VB6, men dessa möjligheter var mycket begränsade. Till exempel behövde jag för flera år sedan skriva en procedur som pausar exekveringen av ett program under ett visst antal sekunder (motsvarande SLEEP-sats var lätt tillgänglig i Microsoft Basic / DOS). Det är lätt att implementera det själv som följande enkla subrutin:
Dess prestanda kan enkelt verifieras, till exempel genom att använda följande kod för att hantera ett knappklick på ett formulär:
För att lösa detta problem i VB6, inuti Do ... Loop i SleepVB-proceduren, måste du avkommentera anropet till DoEvents-funktionen, som överför kontrollen till operativsystemet och returnerar antalet öppna formulär i denna VB-applikation. Men observera att om du visar ett fönster med meddelandet "En annan hej!", blockerar i sin tur exekveringen av hela applikationen, inklusive SleepVB-proceduren.
Genom att använda globala variabler som flaggor är det också möjligt att säkerställa att SleepVB-proceduren körs onormalt. Det är i sin tur det enklaste exemplet på en beräkningsprocess som helt tar upp processorresurser. Men om du ska göra några användbara beräkningar (och inte snurra i en tom slinga), måste du komma ihåg att anropet till DoEvent-funktionen tar ganska lång tid, så detta bör göras med ganska stora intervaller.
För att se det begränsade stödet för parallell beräkning i VB6, ersätt anropet till DoEvents-funktionen med en etikettutgång:
Label1.Caption = Timer
I det här fallet kommer inte bara Command2-knappen inte att utlösas, utan även inom 5 sekunder kommer innehållet på etiketten inte att ändras.
För ett annat experiment, lägg till ett vänteanrop till koden för Command2 (detta kan göras eftersom SleepVB-proceduren är återinträdande):
Private Sub Command2_Click () Ring SleepVB (5) MsgBox "En annan hej!" Avsluta Sub |
Starta sedan programmet och klicka på Kommando1, och efter 2-3 sekunder - Kommando2. Det första meddelandet som visas är "Ännu ett hej!" Även om motsvarande process startade senare. Anledningen till detta är att DoEvents-funktionen bara söker efter händelser på det visuella, men inte efter närvaron av andra beräkningstrådar. Dessutom körs VB-applikationen faktiskt i en tråd, så kontrollen återgick till händelseproceduren som senast startade.
.NET trådkontroll
Att bygga flertrådiga .NET-applikationer bygger på en grupp .NET Framework-basklasser som beskrivs i System.Threading-namnområdet. I det här fallet tillhör nyckelrollen Thread-klassen, med hjälp av vilken nästan alla trådhanteringsoperationer utförs. Från och med nu gäller allt som har sagts om att arbeta med trådar för alla programmeringsverktyg i .NET, inklusive C #.
För en första bekantskap med skapandet av parallella strömmar kommer vi att skapa en Windows-applikation med ett formulär där vi kommer att placera knapparna ButtonStart och ButtonAbort och skriva följande kod:
Jag vill genast uppmärksamma er på tre punkter. Först används nyckelorden Imports för att referera till de förkortade namnen på klasserna som beskrivs här av namnutrymmet. Jag citerade specifikt en annan användning av Imports för att beskriva den förkortade motsvarigheten till ett långt namnområdesnamn (VB = Microsoft.VisualBasic) som kan appliceras på programtext. I det här fallet kan du direkt se vilken namnrymd Timer-objektet tillhör.
För det andra använde jag booleska parenteser #Region för att visuellt separera koden jag skrev från koden som genereras automatiskt av formulärdesignern (den senare ingår inte här).
För det tredje har beskrivningarna av ingångsparametrarna för händelseprocedurer tagits bort speciellt (detta kommer att göras ibland i framtiden), för att inte distraheras av saker som inte är viktiga i det här fallet.
Starta programmet och klicka på knappen ButtonStart. Processen att vänta i en loop under ett angivet tidsintervall har börjat, och i det här fallet (till skillnad från exemplet med VB6) - i en oberoende tråd. Detta är lätt att se - alla visuella element i formuläret är tillgängliga. Genom att till exempel klicka på knappen Avbryt knappen kan du avbryta processen med metoden Avbryt (men att stänga formuläret med knappen Stäng systemet avbryter inte proceduren!). För klarhet i processens dynamik kan du placera en etikett på formuläret och lägga till utdata från den aktuella tiden till väntslingan för SleepVBNET-proceduren:
Label1.Text = _ "Aktuell tid =" & VB.TimeOfDay
Exekveringen av SleepVBNET-proceduren (som i det här fallet redan är en metod för det nya objektet) kommer att fortsätta även om du lägger till en meddelanderuta i ButtonStart-koden för att visa ett meddelande om starten av beräkningarna efter att tråden har startat (Fig. 1).
Ett mer komplext alternativ är en ström som en klass
För ytterligare experiment med trådar, låt oss skapa en ny VB-applikation av typen Console, bestående av en vanlig kodmodul med en Main-procedur (som börjar köras när applikationen startar) och en modul av WorkerThreadClass-klassen:
Låt oss starta den skapade applikationen. Ett konsolfönster kommer att visas, där du kommer att se en rullande rad med tecken som visar modellen för den pågående beräkningsprocessen (WorkerThread). Sedan kommer en meddelanderuta att visas, utfärdad av anropsprocessen (Main), och slutligen kommer vi att se bilden som visas i Fig. 2 (om du inte är nöjd med exekveringshastigheten för den modellerade processen, ta bort eller lägg till några aritmetiska operationer med variabeln "a" i WorkerThread-proceduren).
Observera: meddelanderutan "Första tråden startade" visades med en märkbar fördröjning efter starten av WorkerThread-processen (i fallet med formuläret som beskrivs i föregående stycke, skulle ett sådant meddelande visas nästan omedelbart efter att man tryckt på knappen ButtonStart) . Detta beror med största sannolikhet på att, när du arbetar med formuläret, har händelseprocedurer företräde framför processen som startas. När det gäller en konsolapplikation har alla procedurer samma prioritet. Vi kommer att diskutera frågan om prioriteringar senare, men för nu kommer vi att sätta anropstråden (Main) till högsta prioritet:
Thread.CurrentThread.Priority = _ ThreadPriority.Highest Thread1.Start () |
Nu visas fönstret nästan omedelbart. Som du kan se finns det två sätt att skapa instanser av Thread-objektet. Först använde vi den första av dem - vi skapade ett nytt objekt (tråd) Thread1 och arbetade med det. Det andra alternativet är att hämta Thread-objektet för den aktuella tråden med den statiska CurrentThread-metoden. Så här satte huvudproceduren en högre prioritet åt sig själv, men den kan göra det för vilken annan tråd som helst, till exempel:
Tråd1.Prioritet = Trådprioritet.Lägsta tråd1.Start ()
För att visa möjligheterna att hantera en pågående process, lägg till följande kodrader i slutet av huvudproceduren:
Starta nu applikationen samtidigt som du gör några musoperationer (förhoppningsvis har du valt önskad latensnivå i WorkerThread så att processen inte är särskilt snabb, men inte heller för långsam).
Först startar "Process 1" i konsolfönstret, och meddelandet "Första tråden har startat" visas. "Process 1" körs och du trycker snabbt på OK-knappen i meddelanderutan.
Vidare - "Process 1" fortsätter, men efter två sekunder visas meddelandet "Tråden pausad". Process 1 frös. Klicka på OK i meddelanderutan: Process 1 fortsatte och slutfördes framgångsrikt.
I det här utdraget har vi använt Sleep-metoden för att avbryta den aktuella processen. Obs: Sleep är en statisk metod och kan endast tillämpas på den aktuella processen, inte någon instans av Thread-objekt. Språksyntaxen låter dig skriva Thread1.Sleep eller Thread.Sleep, men i det här fallet används CurrentThread-objektet fortfarande.
Sleep-metoden kan också använda argumentet 0. I det här fallet kommer den aktuella tråden att släppa den oanvända återstoden av sin tilldelade tidsdel.
Ett annat intressant användningsfall för Sleep är med Timeout.Infinite. I det här fallet kommer tråden att avbrytas på obestämd tid tills tillståndet avbryts av en annan tråd med metoden Thread.Interrupt.
För att stänga av en extern tråd från en annan tråd utan att stoppa den senare måste du använda ett anrop till Thread.Suspend-metoden. Då kommer det att vara möjligt att fortsätta dess exekvering med Thread.Resume-metoden, vilket vi gjorde i ovanstående kod.
Lite om trådsynkronisering
Synkronisering av trådar är ett av huvudproblemen när man skriver flertrådade applikationer, och System.Threading-utrymmet tillhandahåller ett brett utbud av verktyg för att åstadkomma detta. Men nu ska vi bara bekanta oss med Thread.Join-metoden, som låter dig spåra slutet av en tråds exekvering. För att se hur det fungerar, ersätt de sista raderna i Main med denna kod:
Process Priority Management
Tilldelningen av processortidsskivor mellan trådar utförs med hjälp av prioriteringar, som ställs in i form av egenskapen Thread.Priority. Strömmar som skapas under körning kan ställas in på fem värden: Högst, Ovannormal, Normal (standard), Under Normal och Lägst. För att se hur prioriteringar påverkar exekveringshastigheten för trådar, låt oss skriva följande kod för huvudproceduren:
Sub Main () "beskrivning av den första processen Dim Thread1 As Thread Dim oWorker1 As New WorkerThreadClass () Thread1 = New Thread (AddressOf _ oWorker1.WorkerThread)" Thread1.Priority = _ "ThreadPriority.BelowNormal" överför initial data: oWorker1. Start = 1 oWorker1.Finish = 10 oWorker1.ThreadName = "Nedräkning 1" oWorker1.SymThread = "." "beskrivning av den andra processen Dim Thread2 As Thread Dim oWorker2 As New WorkerThreadClass () Thread2 = New Thread (AddressOf _ oWorker2.WorkerThread)" överför initial data: oWorker2.Start = 11 oWorker2.Finish = 20 oWorker2.ThreadName 2" oWorker .SymThread = "*" "" startar ett lopp Thread.CurrentThread.Priority = _ ThreadPriority.Highest Thread1.Start () Thread2.Start () "Väntar på att processerna ska avsluta Thread1.Join () Thread2.Join () ) MsgBox (" Båda processerna avslutade ") End Sub |
Observera att detta använder en enda klass för att skapa flera trådar. Låt oss starta applikationen och titta på dynamiken i exekveringen av de två trådarna (Fig. 3). Här kan du se att de i allmänhet körs i samma hastighet, den första ligger något före på grund av den tidigare lanseringen.
Nu, innan du startar den första tråden, sätt dess prioritet en nivå lägre:
Thread1.Priority = _ ThreadPriority.BelowNormal
Bilden förändrades dramatiskt: den andra strömmen tog nästan hela tiden bort från den första (fig. 4).
Observera också användningen av Join-metoden. Med dess hjälp utför vi en ganska vanlig variant av trådsynkronisering, där huvudprogrammet väntar på slutförandet av flera parallella beräkningsprocesser.
Slutsats
Vi har precis berört grunderna för att utveckla flertrådade .NET-applikationer. En av de svåraste och i praktiken aktuella frågorna är trådsynkronisering. Förutom att använda Thread-objektet som beskrivs i den här artikeln (det har många metoder och egenskaper som vi inte övervägde här), Monitor- och Mutex-klasserna, såväl som lock (C #) och SyncLock (VB.NET) uttalanden, spelar en mycket viktig roll i trådhantering. ...
En mer detaljerad beskrivning av denna teknik ges i separata kapitel i böckerna och från vilken jag skulle vilja citera några citat (som jag håller helt med om) som en mycket kort sammanfattning av ämnet "Multithreading i .NET".
"Om du är nybörjare, kanske du blir förvånad över att upptäcka att överkostnaderna för att skapa och skicka trådar kan få en enkeltrådad applikation att köras snabbare ... Försök därför alltid att testa både enkeltrådiga och flertrådiga prototyper. "
"Du måste vara försiktig med din multithreading-design och noggrant kontrollera åtkomsten till delade objekt och variabler."
"Tänk inte på att multitråda är standardmetoden."
"Jag frågade en publik med erfarna VB-programmerare om de skulle få gratis trådning i en framtida version av VB. Nästan alla räckte upp händerna. Sedan frågade jag vem som vet vad han höll på med. Den här gången räckte bara ett fåtal upp händerna. och det fanns medvetna leenden på deras läppar."
"Om du inte skräms av utmaningarna med att designa flertrådade applikationer, när den används på rätt sätt, kan multitrådning dramatiskt förbättra applikationsprestanda."
På egen hand vill jag tillägga att tekniken för att skapa flertrådade .NET-applikationer (som många andra .NET-teknologier) som helhet är praktiskt taget oberoende av vilket språk som används. Därför råder jag utvecklare att studera olika böcker och artiklar, oavsett vilket programmeringsspråk de väljer för att demonstrera en viss teknik.
Litteratur:
- Dan Appleman. Övergång till VB.NET: strategier, koncept, kod / Per. från engelska - SPb .: "Peter", 2002, - 464 s .: ill.
- Tom Archer. C # grunderna. De senaste teknologierna / Per. från engelska - M .: Förlags- och handelshuset "Russian Edition", 2001. - 448 s .: ill.
Multitasking och multithreadingLåt oss börja med detta enkla uttalande: 32-bitars Windows-operativsystem stöder multitasking (multiprocessing) och multithreading-lägen för databehandling. Det går att diskutera hur bra de gör det, men det är en annan fråga. Multitasking är ett arbetssätt när en dator kan utföra flera uppgifter samtidigt, parallellt. Det är tydligt att om en dator har en processor, så pratar vi om pseudo-parallellism, när OS, enligt vissa regler, snabbt kan växla mellan olika uppgifter. En uppgift är ett program eller en del av ett program (applikation) som utför någon logisk åtgärd och är en enhet för vilken operativsystemet allokerar resurser. I en något förenklad form kan vi anta att i Windows är en uppgift varje programvarukomponent implementerad som en separat körbar modul (EXE, DLL). För Windows har begreppet "uppgift" samma betydelse som "process", vilket i synnerhet betyder exekvering av programkod strikt i adressutrymmet som är tilldelat för det. Det finns två huvudtyper av multitasking - kooperativ och förebyggande. Det första alternativet, implementerat i tidigare versioner av Windows, ger möjlighet att växla mellan uppgifter endast i det ögonblick som den aktiva uppgiften kommer åt OS (till exempel för I/O). I det här fallet är varje tråd ansvarig för att återföra kontrollen till operativsystemet. Om uppgiften glömde att göra en sådan operation (till exempel fastnade den i en slinga) ledde det ganska ofta till att hela datorn frös. Förebyggande multitasking är ett läge när operativsystemet självt ansvarar för att ge varje tråd sin rätta tidsdel, varefter den (om det finns förfrågningar från andra uppgifter) automatiskt avbryter denna tråd och bestämmer vad som ska börja härnäst. Tidigare kallades detta läge "tidsdelning". Vad är en ström? En tråd är en autonom beräkningsprocess, men inte allokerad på OS-nivå, utan inom en uppgift. Den grundläggande skillnaden mellan en tråd och en "process-task" är att alla trådar i en uppgift exekveras i ett enda adressutrymme, det vill säga de kan arbeta med delade minnesresurser. Det är här deras fördelar (parallell databehandling) och nackdelar (hot mot programmets tillförlitlighet) ligger. Man bör komma ihåg att i fallet med multitasking är operativsystemet primärt ansvarigt för att skydda applikationer, och när man använder multithreading, utvecklaren själv. Observera att användningen av multitasking-läge i system med en processor gör det möjligt att öka den övergripande prestandan för multitasking-systemet som helhet (även om inte alltid, eftersom antalet växlar ökar, ökar andelen resurser som ockuperas av operativsystemet). Men exekveringstiden för en specifik uppgift ökar alltid, även om det bara är något, på grund av operativsystemets extra arbete. Om processorn är tungt belastad med uppgifter (med minimal stilleståndstid för I/O, till exempel när det gäller att lösa rent matematiska problem), uppnås en verklig total prestandaökning endast när man använder multiprocessorsystem. Sådana system tillåter olika parallelliseringsmodeller - på uppgiftsnivå (varje uppgift kan endast uppta en processor, trådar exekveras endast i pseudo-parallellism) eller på trådnivå (när en uppgift kan uppta flera processorer med sina trådar). Här kan du också komma ihåg att när man körde kraftfulla delade datorsystem, vars förfader var IBM System / 360-familjen i slutet av 60-talet, var en av de mest brådskande uppgifterna att välja det optimala styralternativet för multitasking - inklusive i ett dynamiskt läge, med hänsyn till olika parametrar. Multitasking-hantering är i princip en funktion av operativsystemet. Men effektiviteten av genomförandet av det här eller det alternativet är direkt relaterat till särdragen hos datorns arkitektur som helhet, och särskilt processorn. Till exempel fungerade samma högpresterande IBM System / 360 bra i delade system för affärsuppgifter, men samtidigt var det helt olämpligt för att lösa problem av "realtids"-klassen. På den tiden låg betydligt billigare och enklare minidatorer som DEC PDP 11/20 klart i täten på detta område. |
Vilket ämne väcker flest frågor och svårigheter för nybörjare? När jag frågade min lärare och Java-programmerare Alexander Pryakhin om detta, svarade han omedelbart: "Multithreading". Tack till honom för idén och hjälpen med att förbereda den här artikeln!
Vi kommer att undersöka applikationens inre värld och dess processer, ta reda på vad essensen av multithreading är, när det är användbart och hur man implementerar det - med Java som exempel. Om du lär dig ett annat OOP-språk, oroa dig inte: de grundläggande principerna är desamma.
Om bäckar och deras ursprung
För att förstå multithreading, låt oss först förstå vad en process är. En process är en bit virtuellt minne och resurser som operativsystemet allokerar för att köra ett program. Om du öppnar flera instanser av samma applikation kommer systemet att tilldela en process för varje. I moderna webbläsare kan en separat process ansvara för varje flik.
Du har förmodligen stött på Windows "Task Manager" (i Linux är det "System Monitor") och du vet att onödiga processer som körs belastar systemet, och de mest "tunga" av dem fryser ofta, så de måste avslutas med tvång .
Men användare älskar multitasking: mata dem inte med bröd – låt dem öppna ett dussin fönster och hoppa fram och tillbaka. Det finns ett dilemma: du måste säkerställa samtidig drift av applikationer och samtidigt minska belastningen på systemet så att det inte saktar ner. Låt oss säga att hårdvaran inte kan hålla jämna steg med ägarnas behov - du måste lösa problemet på mjukvarunivå.
Vi vill att processorn ska utföra fler instruktioner och bearbeta mer data per tidsenhet. Det vill säga, vi måste passa in mer av den körda koden i varje tidssegment. Tänk på en enhet för kodexekvering som ett objekt - det är en tråd.
Ett komplext fall är lättare att närma sig om man delar upp det i flera enkla. Så när man arbetar med minne: en "tung" process delas upp i trådar som tar upp färre resurser och som är mer benägna att leverera koden till kalkylatorn (hur exakt - se nedan).
Varje applikation har minst en process, och varje process har minst en tråd, som kallas huvudtråden och från vilken, vid behov, nya lanseras.
Skillnad mellan trådar och processer
Trådar använder minnet som tilldelats för processen, och processerna kräver sitt eget minnesutrymme. Därför skapas och slutförs trådar snabbare: systemet behöver inte allokera ett nytt adressutrymme till dem varje gång och sedan släppa det.
Processer arbetar var och en med sin egen data - de kan utbyta något endast genom mekanismen för kommunikation mellan processer. Trådar får direkt tillgång till varandras data och resurser: det man har ändrat är omedelbart tillgängligt för alla. Tråden kan styra "karlen" i processen, medan processen uteslutande styr sina "döttrar". Därför går det snabbare att växla mellan strömmar och kommunikationen mellan dem är lättare.
Vad är slutsatsen av detta? Om du behöver bearbeta en stor mängd data så snabbt som möjligt, dela upp den i bitar som kan bearbetas av separata trådar och pussla sedan ihop resultatet. Det är bättre än att skapa resurskrävande processer.
Men varför går en populär applikation som Firefox vägen att skapa flera processer? Eftersom det är för webbläsaren som isolerade flikar fungerar är pålitligt och flexibelt. Om något är fel med en process är det inte nödvändigt att avsluta hela programmet - det är möjligt att spara åtminstone en del av datan.
Vad är multithreading
Så vi kommer till huvudpunkten. Multithreading är när ansökningsprocessen delas upp i trådar som bearbetas parallellt - vid en tidsenhet - av processorn.
Beräkningsbelastningen är fördelad mellan två eller flera kärnor, så att gränssnittet och andra programkomponenter inte bromsar varandras arbete.
Flertrådiga applikationer kan köras på enkärniga processorer, men sedan körs trådarna i tur och ordning: den första fungerade, dess tillstånd sparades - den andra fick fungera, sparades - återvände till den första eller startade den tredje, etc.
Upptagna människor klagar över att de bara har två händer. Processer och program kan ha så många händer som behövs för att slutföra uppgiften så snabbt som möjligt.
Vänta på en signal: synkronisering i flertrådade applikationer
Föreställ dig att flera trådar försöker ändra samma dataområde samtidigt. Vems ändringar kommer så småningom att accepteras och vems ändringar kommer att annulleras? För att undvika förvirring med delade resurser måste trådar samordna sina åtgärder. För att göra detta utbyter de information med hjälp av signaler. Varje tråd berättar för de andra vad den gör och vilka förändringar som kan förväntas. Så data i alla trådar om det aktuella tillståndet för resurser synkroniseras.
Grundläggande synkroniseringsverktyg
Ömsesidig uteslutning (ömsesidig uteslutning, förkortat - mutex) - en "flagga" som går till tråden som för närvarande tillåts arbeta med delade resurser. Eliminerar åtkomst av andra trådar till det upptagna minnesområdet. Det kan finnas flera mutex i en applikation, och de kan delas mellan processer. Det finns en hake: mutex tvingar applikationen att komma åt operativsystemets kärna varje gång, vilket är dyrt.
Semafor - låter dig begränsa antalet trådar som kan komma åt en resurs vid ett givet tillfälle. Detta kommer att minska belastningen på processorn vid exekvering av kod där det finns flaskhalsar. Problemet är att det optimala antalet trådar beror på användarens maskin.
Händelse - du definierar ett villkor vid uppkomsten av vilket kontroll överförs till den önskade tråden. Strömmar utbyter händelsedata för att utveckla och logiskt fortsätta varandras handlingar. En fick data, den andra kontrollerade deras korrekthet, den tredje sparade den på hårddisken. Evenemang skiljer sig åt i sättet de ställs in. Om du behöver meddela flera trådar om en händelse måste du manuellt ställa in avbrytningsfunktionen för att stoppa signalen. Om det bara finns en måltråd kan du skapa en automatisk återställningshändelse. Den kommer att stoppa signalen själv efter att den når strömmen. Händelser kan ställas i kö för flexibel flödeskontroll.
Kritiskt avsnitt - en mer komplex mekanism som kombinerar en loopräknare och en semafor. Räknaren låter dig skjuta upp starten av semaforen under önskad tid. Fördelen är att kärnan bara aktiveras om sektionen är upptagen och semaforen behöver slås på. Resten av tiden körs tråden i användarläge. Tyvärr kan en sektion bara användas inom en process.
Hur man implementerar multithreading i Java
Trådklassen ansvarar för att arbeta med trådar i Java. Att skapa en ny tråd för att utföra en uppgift innebär att skapa en instans av klassen Thread och associera den med den kod du vill ha. Detta kan göras på två sätt:
underklass Tråd;
implementera Runnable-gränssnittet i din klass och skicka sedan klassinstanserna till trådkonstruktorn.
Även om vi inte kommer att beröra ämnet dödlägen, när trådar blockerar varandras arbete och fryser, lämnar vi det till nästa artikel.
Java multithreading exempel: ping-pong med mutexes
Om du tror att något hemskt är på väg att hända, andas ut. Vi kommer att överväga att arbeta med synkroniseringsobjekt nästan på ett lekfullt sätt: två trådar kommer att kastas av en mutex.Men i själva verket kommer du att se en riktig applikation där bara en tråd kan behandla allmänt tillgänglig data åt gången.
Låt oss först skapa en klass som ärver egenskaperna hos den tråd vi redan känner till, och skriv en kickBall-metod:
Public class PingPongThread utökar tråden (PingPongThread (String name) (this.setName (name); // åsidosätt trådens namn) @Override public void run () (Ball ball = Ball.getBall (); while (ball.isInGame () ) (kickBall (boll);)) privat void kickBall (Ball ball) (om (! ball.getSide (). är lika med (getName ())) (ball.kick (getName ());)))
Låt oss nu ta hand om bollen. Han kommer inte att vara enkel med oss, utan minnesvärd: så att han kan berätta vem som slog honom, från vilken sida och hur många gånger. För att göra detta använder vi en mutex: den samlar in information om arbetet i var och en av trådarna - detta gör att isolerade trådar kan kommunicera med varandra. Efter den 15:e träffen tar vi bollen ur spelet för att inte skada den allvarligt.
Offentlig klass Boll (privat int kicks = 0; privat statisk bollinstans = ny boll (); privat strängsida = ""; privat boll () () statisk boll getBall () (återvänd instans;) synkroniserad void kick (strängspelares namn) (spark ++; sida = spelarnamn; System.out.println (sparkar + "" + sida);) String getSide () (retursida;) boolean isInGame () (return (kicks)< 15); } }
Och nu kommer två spelartrådar in på scenen. Låt oss kalla dem, utan vidare, Ping and Pong:
Offentlig klass PingPongGame (PingPongThread player1 = new PingPongThread ("Ping"); PingPongThread player2 = new PingPongThread ("Pong"); Ball ball; PingPongGame () (ball = Ball.getBall ();) void startGame () kastar InterruptedException (spelare1) .start (); player2.start ();))
"Full stadion med folk - dags att börja matchen." Vi kommer officiellt att tillkännage öppnandet av mötet - i huvudklassen för ansökan:
Public class PingPong (public static void main (String args) kastar InterruptedException (PingPongGame-spel = nytt PingPongGame (); game.startGame ();))
Som ni ser är det inget rasande här. Detta är bara en introduktion till multithreading för tillfället, men du vet redan hur det fungerar, och du kan experimentera - begränsa spelets varaktighet inte av antalet slag, utan till exempel efter tid. Vi kommer tillbaka till ämnet multithreading senare - vi ska titta på java.util.concurrent-paketet, Akka-biblioteket och den flyktiga mekanismen. Låt oss också prata om att implementera multithreading i Python.
Clay Breshears
Introduktion
Intels multithreading-implementeringsmetoder inkluderar fyra huvudfaser: analys, design och implementering, felsökning och prestandajustering. Detta är metoden som används för att skapa en flertrådad applikation från sekventiell kod. Att arbeta med mjukvara under det första, tredje och fjärde steget täcks ganska brett, medan informationen om implementeringen av det andra steget är helt klart otillräcklig.
Många böcker har publicerats om parallella algoritmer och parallell beräkning. Dessa publikationer täcker dock huvudsakligen meddelandeförmedling, distribuerade minnessystem eller teoretiska parallella datormodeller som ibland är otillämpliga på riktiga flerkärniga plattformar. Om du är redo att göra allvar med flertrådsprogrammering behöver du förmodligen veta hur du designar algoritmer för dessa modeller. Naturligtvis är användningen av dessa modeller ganska begränsad, så många mjukvaruutvecklare kan behöva implementera dem i praktiken.
Det är ingen överdrift att säga att utvecklingen av flertrådade applikationer först och främst är en kreativ aktivitet, och först sedan en vetenskaplig aktivitet. I den här artikeln kommer du att lära dig om åtta enkla regler som hjälper dig att utöka din bas av samtidiga programmeringsmetoder och förbättra effektiviteten i att tråda dina applikationer.
Regel 1. Välj de operationer som utförs i programkoden oberoende av varandra
Parallell bearbetning gäller endast de operationer i sekventiell kod som utförs oberoende av varandra. Ett bra exempel på hur självständiga handlingar leder till ett verkligt enskilt resultat är att bygga ett hus. Det involverar arbetare av många specialiteter: snickare, elektriker, putsare, rörmokare, takläggare, målare, murare, trädgårdsmästare och så vidare. Vissa av dem kan naturligtvis inte börja jobba innan andra har avslutat sin verksamhet (t.ex. kommer takläggare inte att börja arbeta förrän väggarna är byggda, och målare kommer inte att måla dessa väggar om de inte är putsade). Men generellt kan vi säga att alla som är inblandade i bygget agerar oberoende av varandra.
Tänk på ett annat exempel - arbetscykeln för en DVD-uthyrningsbutik som tar emot beställningar på vissa filmer. Beställningar fördelas bland de anställda på punkten som letar efter dessa filmer i lagret. Naturligtvis, om en av arbetarna tar från lagret en skiva på vilken en film med Audrey Hepburn spelades in, kommer detta inte på något sätt att påverka en annan arbetare som letar efter en annan actionfilm med Arnold Schwarzenegger, och ännu mer så kommer det inte att påverka deras kollega som är på jakt efter skivor med ny säsong av serien "Vänner". I vårt exempel tror vi att alla problem i samband med bristen på filmer i lager var lösta innan beställningarna anlände till uthyrningsstället, och paketeringen och frakten av en beställning kommer inte att påverka behandlingen av andra.
I ditt arbete kommer du förmodligen att stöta på beräkningar som bara kan bearbetas i en specifik sekvens, och inte parallellt, eftersom olika iterationer eller steg i slingan beror på varandra och måste utföras i strikt ordning. Låt oss ta ett levande exempel från det vilda. Föreställ dig en dräktig hjort. Eftersom att föda ett foster varar i genomsnitt åtta månader, så kommer det, vad man än kan säga, inte en fawn att dyka upp på en månad, även om åtta renar blir dräktiga samtidigt. Men åtta renar samtidigt skulle göra sitt jobb perfekt om de spändes till alla i tomtens släde.
Regel 2. Tillämpa parallellitet med låg detaljnivå
Det finns två tillvägagångssätt för parallell partitionering av sekventiell programkod: bottom-up och top-down. Först, i steget för kodanalys, bestäms kodsegment (så kallade "hot spots"), som tar en betydande del av programexekveringstiden. Att separera dessa kodsegment parallellt (om möjligt) kommer att ge maximal prestandavinst.
Bottom-up-metoden implementerar multithreading av kod hot spots. Om det inte är möjligt att dela de hittade punkterna parallellt, bör du undersöka applikationsanropsstacken för att fastställa andra segment som är tillgängliga för parallelldelning och som tar lång tid att slutföra. Låt oss säga att du arbetar med ett program för att komprimera grafik. Komprimering kan implementeras med hjälp av flera oberoende parallella strömmar som bearbetar enskilda segment av bilden. Men även om du har lyckats implementera multithreading av "hot spots", försumma inte analysen av anropsstacken, som ett resultat av vilket du kan hitta segment tillgängliga för parallell delning på en högre nivå av programkoden. På så sätt kan du öka granulariteten i den parallella bearbetningen.
I top-down-metoden analyseras programkodens arbete och dess individuella segment markeras, vars utförande leder till att hela uppgiften slutförs. Om det inte finns något tydligt oberoende av huvudkodsegmenten, analysera deras beståndsdelar för att hitta oberoende beräkningar. Genom att analysera programkoden kan du identifiera de kodmoduler som förbrukar mest CPU-tid. Låt oss titta på hur man implementerar trådning i en videokodningsapplikation. Parallell bearbetning kan implementeras på den lägsta nivån - för oberoende pixlar i en bildruta, eller på en högre nivå - för grupper av bildrutor som kan bearbetas oberoende av andra grupper. Om en applikation skrivs för att bearbeta flera videofiler samtidigt, kan parallelldelning på denna nivå vara ännu enklare, och detaljerna blir de lägsta.
Granulariteten för parallell beräkning hänvisar till mängden beräkning som måste utföras innan synkronisering mellan trådar. Med andra ord, ju mindre frekvent synkronisering sker, desto lägre granularitet. Finkorniga gängningsberäkningar kan göra att systemets overhead för gängning uppväger de användbara beräkningarna som utförs av dessa gängor. Ökningen av antalet trådar med samma mängd beräkning komplicerar bearbetningsprocessen. Multithreading med låg granularitet introducerar mindre systemlatens och har större potential för skalbarhet, vilket kan uppnås med ytterligare trådar. För att implementera parallell bearbetning med låg granularitet, rekommenderas att använda ett uppifrån och ned-tillvägagångssätt och tråd på en hög nivå i anropsstacken.
Regel 3. Bygg in skalbarhet i din kod för att förbättra prestandan när antalet kärnor växer.
För inte så länge sedan, förutom dual-core-processorer, dök fyrkärniga upp på marknaden. Dessutom har Intel redan annonserat en processor med 80 kärnor, som kan utföra en biljon flyttalsoperationer per sekund. Eftersom antalet kärnor i processorer bara kommer att växa över tiden, måste din kod ha tillräcklig potential för skalbarhet. Skalbarhet är en parameter genom vilken man kan bedöma en applikations förmåga att adekvat reagera på förändringar såsom en ökning av systemresurser (antal kärnor, minnesstorlek, bussfrekvens, etc.) eller en ökning av mängden data. När antalet kärnor i framtida processorer ökar, skriv skalbar kod som kommer att öka i prestanda genom att öka systemresurserna.
För att parafrasera en av lagarna i Northcote Parkinson (C. Northecote Parkinson), kan vi säga att "databehandling tar upp alla tillgängliga systemresurser." Detta innebär att när beräkningsresurserna ökar (till exempel antalet kärnor), kommer alla troligen att användas för att bearbeta data. Låt oss gå tillbaka till videokomprimeringsapplikationen som diskuterades ovan. Tillägget av ytterligare kärnor till processorn kommer sannolikt inte att påverka storleken på de bearbetade ramarna - istället kommer antalet trådar som bearbetar ramen att öka, vilket kommer att leda till en minskning av antalet pixlar per tråd. Som ett resultat, på grund av organiseringen av ytterligare strömmar, kommer mängden overhead att öka, och graden av parallellitetsgranularitet kommer att minska. Ett annat mer troligt scenario är en ökning av storleken eller antalet videofiler som behöver kodas. I det här fallet kommer organisationen av ytterligare strömmar som kommer att bearbeta större (eller ytterligare) videofiler att göra det möjligt att dela upp hela arbetsvolymen direkt i det skede där ökningen ägde rum. I sin tur kommer en applikation med sådana möjligheter att ha en hög potential för skalbarhet.
Att designa och implementera parallell bearbetning med datanedbrytning ger ökad skalbarhet jämfört med att använda funktionell nedbrytning. Antalet oberoende funktioner i programkoden är oftast begränsat och ändras inte under exekveringen av applikationen. Eftersom varje oberoende funktion tilldelas en separat tråd (och följaktligen en processorkärna), med en ökning av antalet kärnor, kommer ytterligare organiserade trådar inte att orsaka en ökning av prestanda. Så, parallella partitioneringsmodeller med dataupplösning kommer att ge ökad potential för skalbarhet av applikationen på grund av att mängden bearbetad data kommer att öka med antalet processorkärnor.
Även om programkoden trådar oberoende funktioner, är det troligt att ytterligare trådar kan användas för att starta när ingångsbelastningen ökar. Låt oss gå tillbaka till husbyggnadsexemplet som diskuterades ovan. Byggnadens säregna syfte är att utföra ett begränsat antal självständiga uppgifter. Men om du får i uppdrag att bygga dubbelt så många våningar kommer du förmodligen att vilja anställa ytterligare arbetare inom vissa specialiteter (målare, takläggare, rörmokare, etc.). Därför måste du utveckla applikationer som kan anpassa sig till den dataupplösning som blir resultatet av ökad arbetsbelastning. Om din kod implementerar funktionell nedbrytning, överväg att organisera ytterligare trådar när antalet processorkärnor ökar.
Regel 4. Använd trådsäkra bibliotek
Om du kanske behöver ett bibliotek för att hantera data hot spots i din kod, se till att överväga att använda out-of-the-box-funktioner istället för din egen kod. Kort sagt, försök inte uppfinna hjulet på nytt genom att utveckla kodsegment vars funktioner redan tillhandahålls i optimerade biblioteksrutiner. Många bibliotek, inklusive Intel® Math Kernel Library (Intel® MKL) och Intel® Integrated Performance Primitives (Intel® IPP), innehåller redan flertrådsfunktioner optimerade för flerkärniga processorer.
Det bör noteras att när du använder procedurer från flertrådade bibliotek måste du se till att anrop av ett eller annat bibliotek inte påverkar den normala driften av trådar. Det vill säga, om proceduranrop görs från två olika trådar, bör korrekta resultat returneras från varje anrop. Om procedurer hänvisar till delade biblioteksvariabler och uppdaterar dem, kan ett datarace inträffa, vilket kommer att påverka tillförlitligheten av beräkningsresultaten negativt. För att fungera korrekt med trådar läggs biblioteksproceduren till som ny (det vill säga uppdaterar inget annat än lokala variabler) eller synkroniseras för att skydda åtkomst till delade resurser. Slutsats: innan du använder något tredjepartsbibliotek i din programkod, läs den bifogade dokumentationen för att se till att det fungerar korrekt med strömmar.
Regel 5. Använd en lämplig flertrådsmodell
Anta att funktioner från de flertrådade biblioteken uppenbarligen inte räcker till för parallell uppdelning av alla lämpliga kodsegment, och du var tvungen att tänka på hur trådarna organiseras. Ha inte bråttom att skapa din egen (krångliga) trådstruktur om OpenMP-biblioteket redan innehåller all funktionalitet du behöver.
Nackdelen med explicit multithreading är omöjligheten av exakt trådkontroll.
Om du bara behöver parallell separation av resurskrävande slingor, eller om den extra flexibiliteten som explicita trådar ger är sekundär för dig, så är det i det här fallet ingen mening att göra onödigt arbete. Ju mer komplex implementeringen av multithreading är, desto större är sannolikheten för fel i koden och desto svårare är dess efterföljande förfining.
OpenMP-biblioteket är fokuserat på datanedbrytning och är särskilt väl lämpat för trådning av loopar som arbetar med stora mängder information. Trots det faktum att endast datauppdelning är tillämplig på vissa applikationer, är det nödvändigt att ta hänsyn till ytterligare krav (till exempel arbetsgivaren eller kunden), enligt vilka användningen av OpenMP är oacceptabel och det återstår att implementera multithreading med explicit metoder. I det här fallet kan OpenMP användas för preliminär trådning för att uppskatta de potentiella prestandavinsterna, skalbarheten och den ungefärliga ansträngningen som skulle krävas för att senare dela upp koden genom explicit multitrådning.
Regel 6. Resultatet av programkoden bör inte bero på sekvensen av exekvering av parallella trådar
För sekventiell programkod räcker det att helt enkelt definiera ett uttryck som kommer att exekveras efter vilket annat uttryck som helst. I flertrådad kod är ordningen för exekvering av trådar inte definierad och beror på instruktionerna från operativsystemets schemaläggare. Strängt taget är det nästan omöjligt att förutsäga sekvensen av trådar som kommer att startas för att utföra en operation, eller att bestämma vilken tråd som kommer att startas av schemaläggaren vid ett senare tillfälle. Förutsägelse används främst för att minska fördröjningen av en applikation, särskilt när den körs på en plattform med en processor med färre kärnor än antalet organiserade trådar. Om en tråd blockeras för att den behöver åtkomst till ett område som inte är skrivet till cachen, eller för att den behöver utföra en I/O-begäran, kommer schemaläggaren att avbryta den och starta tråden redo att starta.
Dataracesituationer är ett omedelbart resultat av osäkerhet i schemaläggning av trådexekvering. Det kan vara fel att anta att någon tråd kommer att ändra värdet på en delad variabel innan en annan tråd läser det värdet. Med lycka till, kommer ordningen för exekvering av trådar för en viss plattform att förbli densamma över alla lanseringar av applikationen. De minsta förändringarna i systemets tillstånd (till exempel platsen för data på hårddisken, minneshastigheten eller till och med en avvikelse från den nominella frekvensen för nätaggregatet) kan provocera fram en annan ordning av utförande av trådar. För programkod som bara fungerar korrekt med en viss sekvens av trådar, är således problem associerade med "datarace"-situationer och dödlägen troliga.
Ur prestationsvinstsynpunkt är det att föredra att inte begränsa ordningsföljden för exekvering av trådar. En strikt sekvens av exekvering av strömmar tillåts endast om det är absolut nödvändigt, bestämt av ett förutbestämt kriterium. I händelse av en sådan omständighet kommer trådarna att startas i den ordning som anges av de tillhandahållna synkroniseringsmekanismerna. Tänk dig till exempel två vänner som läser en tidning utspridda på ett bord. För det första kan de läsa med olika hastigheter och för det andra kan de läsa olika artiklar. Och här spelar det ingen roll vem som läser tidningens uppslag först – han får i alla fall vänta på sin vän innan han vänder blad. Samtidigt finns det inga begränsningar för tid och ordning för att läsa artiklar - vänner läser med vilken hastighet som helst, och synkronisering mellan dem sker omedelbart när du vänder sidan.
Regel 7. Använd lokal strömlagring. Tilldela lås till specifika dataområden efter behov
Synkronisering ökar oundvikligen belastningen på systemet, vilket inte på något sätt påskyndar processen för att erhålla resultaten av parallella beräkningar, utan säkerställer deras korrekthet. Ja, synkronisering är nödvändig, men den bör inte överanvändas. För att minimera synkronisering används lokal lagring av strömmar eller allokerade minnesområden (till exempel arrayelement markerade med identifierarna för motsvarande strömmar).
Behovet av att dela temporära variabler genom olika trådar är sällsynt. Sådana variabler måste deklareras eller allokeras lokalt till varje tråd. Variabler vars värden är mellanresultat av exekveringen av trådar måste också deklareras lokala för motsvarande trådar. Synkronisering krävs för att summera dessa mellanresultat i ett delat minnesområde. För att minimera eventuell stress på systemet är det att föredra att uppdatera detta gemensamma område så sällan som möjligt. För explicita flertrådsmetoder tillhandahålls API:er för lokal lagring av trådar för att säkerställa integriteten hos lokal data från början av exekvering av ett flertrådssegment av kod till början av nästa segment (eller under bearbetningen av ett anrop av en flertrådad funktion tills nästa körning av samma funktion).
Om det inte går att lagra strömmar lokalt synkroniseras åtkomsten till delade resurser med hjälp av olika objekt, till exempel lås. I det här fallet är det viktigt att korrekt tilldela lås till specifika datablock, vilket är lättast att göra om antalet lås är lika med antalet datablock. En enda låsmekanism som synkroniserar åtkomst till flera minnesområden används endast när alla dessa områden konstant är i samma kritiska sektion av programkoden.
Vad ska du göra om du behöver synkronisera åtkomst till en stor mängd data, till exempel till en array med 10 000 element? Att sätta upp ett enda lås för hela arrayen är definitivt en flaskhals i din applikation. Måste man verkligen organisera låsning för varje element separat? Sedan, även om 32 eller 64 parallella trådar kommer åt data, måste du förhindra åtkomstkonflikter till ett ganska stort minnesområde, och sannolikheten för sådana konflikter är 1%. Lyckligtvis finns det en sorts gyllene medelväg, den så kallade "moduloblockeringen". Om N modulolås används kommer var och en att synkronisera åtkomst till den N:e delen av det delade dataområdet. Till exempel, om två sådana lås är organiserade, kommer ett av dem att förhindra åtkomst till jämna element i arrayen, och det andra - till udda. I det här fallet bestämmer trådar, med hänvisning till det nödvändiga elementet, dess paritet och ställer in lämpligt lås. Antalet lås modulo väljs med hänsyn till antalet trådar och sannolikheten för samtidig åtkomst av flera trådar till samma minnesområde.
Observera att samtidig användning av flera låsmekanismer inte är tillåten för att synkronisera åtkomst till ett minnesområde. Låt oss komma ihåg Segals lag: ”En person som bara har en klocka vet med säkerhet vad klockan är. En person som har några timmar på sig är inte säker på någonting." Låt oss anta att två olika lås styr åtkomst till en variabel. I detta fall kan det första låset användas av ett segment av koden och det andra av ett annat segment. Då kommer trådarna som kör dessa segment att hamna i en rassituation för de delade data som de kommer åt samtidigt.
Regel 8. Ändra mjukvarualgoritmen om det behövs för att implementera multithreading
Kriteriet för att utvärdera prestandan för applikationer, både sekventiellt och parallellt, är exekveringstiden. Som en uppskattning av algoritmen är en asymptotisk ordning lämplig. Detta teoretiska mått är nästan alltid användbart för att utvärdera prestandan för en applikation. Det vill säga, allt annat lika kommer en applikation med tillväxthastigheten O (n log n) (quicksort) att köras snabbare än en applikation med tillväxthastigheten O (n2) (selektiv sortering), även om resultaten av dessa applikationer är desamma.
Ju bättre den asymptotiska exekveringsordningen är, desto snabbare körs den parallella applikationen. Men även den mest effektiva sekventiella algoritmen kan inte alltid delas upp i parallella strömmar. Om ett program hotspot är för svårt att dela, och det inte finns något sätt att flertråda på en högre nivå av hotspot-anropsstacken, bör du först överväga att använda en annan sekventiell algoritm som är lättare att dela än den ursprungliga. Naturligtvis finns det andra sätt att förbereda din kod för trådning.
Som en illustration av det sista påståendet, betrakta multiplikationen av två kvadratiska matriser. Strassens algoritm har en av de bästa asymptotiska exekveringsordern: O (n2.81), vilket är mycket bättre än O (n3)-ordningen för den vanliga triple nested loop-algoritmen. Enligt Strassens algoritm är varje matris uppdelad i fyra submatriser, varefter sju rekursiva anrop görs för att multiplicera n / 2 × n / 2 submatriser. För att parallellisera rekursiva anrop kan du skapa en ny tråd som sekventiellt kommer att utföra sju oberoende multiplikationer av submatriserna tills de når den angivna storleken. I det här fallet kommer antalet trådar att växa exponentiellt, och granulariteten hos beräkningarna som utförs av varje nybildad tråd kommer att öka med minskande storlek på submatriserna. Överväg ett annat alternativ - organisera en pool med sju trådar som arbetar samtidigt och utföra en multiplikation av submatriser. När trådpoolen är klar anropas Strassen-metoden rekursivt för att multiplicera submatriserna (som i den sekventiella versionen av programkoden). Om systemet som kör ett sådant program har fler än åtta processorkärnor, kommer några av dem att vara inaktiva.
Matrismultiplikationsalgoritmen är mycket lättare att parallellisera med hjälp av en trippel kapslad loop. I det här fallet tillämpas datauppdelning, där matriser är uppdelade i rader, kolumner eller submatriser, och var och en av trådarna utför vissa beräkningar. Implementeringen av en sådan algoritm utförs med hjälp av OpenMP-pragmor som infogas på någon nivå av slingan, eller genom att uttryckligen organisera trådar som utför matrisdelning. Implementeringen av denna enklare sekventiella algoritm kommer att kräva mycket mindre modifieringar av programkoden, jämfört med implementeringen av den flertrådade Strassen-algoritmen.
Så nu känner du till åtta enkla regler för att effektivt konvertera sekventiell kod till parallell. Genom att följa dessa riktlinjer kommer du att kunna skapa flertrådslösningar betydligt snabbare, med ökad tillförlitlighet, optimal prestanda och färre flaskhalsar.
För att återgå till webbsidan för flertrådsprogrammering, gå till