Meny
Är gratis
registrering
Hem  /  Firmware/ Flertrådiga applikationer. Flertrådade applikationer för .NET flertrådade program med exempel

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:

  1. 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.
  2. Hög tillförlitlighet. Onormalt avslutande av någon av processerna påverkar inte resten av processerna på något sätt.
  3. Bra portabilitet. Applikationen kommer att fungera på alla multitasking OS
  4. 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:

  1. 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.
  2. 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:

  1. Relativ enkel utveckling.
  2. 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.
  3. 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.
  4. Bra portabilitet. Applikationen kommer att fungera på de flesta multitasking-operativsystem, inklusive äldre Unix-system.
  5. 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:

  1. Denna arkitektur är inte lätt att designa och implementera för alla applikationer.
  2. 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.
  3. Ö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.
  4. 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.

  5. 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:

  1. Effektiv slumpmässig tillgång till delad data. Denna arkitektur är lämplig för implementering av databasservrar.
  2. Hög tolerans. Kan portas till alla operativsystem som stöder eller emulerar System V IPC.
  3. 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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  • Den höga kostnaden för att utveckla och felsöka applikationer på grund av klausul 1.
  • Låg tillförlitlighet. Förstörelse av datastrukturer, till exempel genom buffertspill eller pekarfel, påverkar alla trådar i en process och resulterar vanligtvis i en onormal avslutning av hela processen. Andra ödesdigra fel, som division med noll i en av trådarna, gör också att alla trådar i processen kraschar.
  • Låg säkerhet. Alla programtrådar körs i en process, det vill säga på uppdrag av samma användare och med samma åtkomsträttigheter. Det är omöjligt att implementera principen om minsta nödvändiga privilegier, processen måste utföras på uppdrag av användaren som kan utföra alla operationer som krävs av alla trådar i applikationen.
  • Skapandet av en tråd är fortfarande en ganska dyr operation. För varje tråd tilldelas nödvändigtvis en egen stack, som som standard upptar 1 megabyte RAM på 32-bitarsarkitekturer och 2 megabyte på 64-bitarsarkitekturer, och några andra resurser. Därför är denna arkitektur inte optimal för alla applikationer.
  • Oförmågan att köra applikationen på ett datorsystem med flera maskiner. Teknikerna som nämns i föregående avsnitt, som att mappa delade filer till minnet, är inte tillämpliga på ett flertrådigt program.
  • 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.

    1. 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.
    2. 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.
    3. 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.
    4. 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:

    1. Dan Appleman. Övergång till VB.NET: strategier, koncept, kod / Per. från engelska - SPb .: "Peter", 2002, - 464 s .: ill.
    2. Tom Archer. C # grunderna. De senaste teknologierna / Per. från engelska - M .: Förlags- och handelshuset "Russian Edition", 2001. - 448 s .: ill.

    Multitasking och multithreading

    Lå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