Prosesser og tråder. Multitasking og multithreading

E Denne artikkelen er ikke for erfarne Python-temmere, for hvem det er en barnelek å løse dette virvaret av slanger, men snarere en overfladisk oversikt over flertrådede muligheter for de som nylig har blitt hekta på Python.

Dessverre er det ikke mye stoff på russisk om temaet multithreading i Python, og jeg begynte å komme over pythonere som ikke hadde hørt noe, for eksempel om GIL, med misunnelsesverdig regelmessighet. I denne artikkelen vil jeg prøve å beskrive de mest grunnleggende funksjonene til flertråds Python, fortelle deg hva GIL er og hvordan du kan leve med det (eller uten det), og mye mer.


Python er et fascinerende programmeringsspråk. Den kombinerer mange programmeringsparadigmer perfekt. De fleste problemene en programmerer kan støte på løses her enkelt, elegant og konsist. Men for alle disse oppgavene er det ofte tilstrekkelig med en enkelt-tråds løsning, og enkelt-tråds programmer er vanligvis forutsigbare og enkle å feilsøke. Det samme kan ikke sies om multi-threaded og multi-prosess programmer.

Flertrådede applikasjoner


Python har en modul tråding , og den har alt du trenger for flertrådsprogrammering: det finnes ulike typer låser, en semafor og en hendelsesmekanisme. Med et ord - alt du trenger for de aller fleste multitrådede programmer. Dessuten er det ganske enkelt å bruke alle disse verktøyene. La oss se på et eksempel på et program som kjører 2 tråder. En tråd skriver ti "0-er", den andre - ti "1-er", og strengt tatt etter tur.

importere tråder

def skribent

for i i xrange(10):

skriv ut x

Event_for_set.set()

# init-hendelser

e1 = threading.Event()

e2 = threading.Event()

# init-tråder

0 , e1, e2))

1 , e2, e1))

# start tråder

t1.start()

t2.start()

t1.join()

t2.join()


Ingen magi eller voodoo-kode. Koden er klar og konsistent. Dessuten, som du kan se, opprettet vi en tråd fra en funksjon. Dette er veldig praktisk for små oppgaver. Denne koden er også ganske fleksibel. La oss si at vi har en tredje prosess som skriver "2", så vil koden se slik ut:

importere tråder

def skribent (x, event_for_wait, event_for_set):

for i i xrange(10):

Event_for_wait.wait() # vent på hendelse

Event_for_wait.clear() # ren hendelse for fremtiden

skriv ut x

Event_for_set.set() # angi hendelse for nabotråd

# init-hendelser

e1 = threading.Event()

e2 = threading.Event()

e3 = threading.Event()

# init-tråder

t1 = threading.Thread(target=writer, args=( 0 , e1, e2))

t2 = threading.Thread(target=writer, args=( 1 , e2, e3))

t3 = threading.Thread(target=writer, args=( 2, e3, e1))

# start tråder

t1.start()

t2.start()

t3.start()

e1.set() # initier den første hendelsen

# koble tråder til hovedtråden

t1.join()

t2.join()

t3.join()


Vi la til en ny hendelse, en ny tråd og endret litt parameterne som
tråder starter (du kan selvfølgelig skrive en mer generell løsning ved å bruke for eksempel MapReduce, men dette er utenfor rammen av denne artikkelen).
Som du kan se, er det fortsatt ingen magi. Alt er enkelt og oversiktlig. La oss gå videre.

Global tolkelås


Det er to vanligste grunner til å bruke tråder: for det første for å øke effektiviteten ved å bruke flerkjernearkitekturen til moderne prosessorer, og dermed programytelsen;
for det andre, hvis vi trenger å dele opp logikken til programmet i parallelle, helt eller delvis asynkrone seksjoner (for eksempel for å kunne pinge flere servere samtidig).

I det første tilfellet står vi overfor en slik begrensning av Python (eller rettere sagt dens hovedimplementering CPython) som Global Interpreter Lock (eller GIL for kort). Konseptet med GIL er at bare én tråd kan kjøres av prosessoren om gangen. Dette gjøres for at det ikke skal oppstå konkurranse mellom tråder for individuelle variabler. Den utførende tråden har tilgang gjennom hele miljøet. Denne funksjonen ved implementering av tråder i Python forenkler arbeidet med tråder og gir en viss trådsikkerhet.

Men det er et subtilt poeng her: det kan virke som om en flertrådsapplikasjon vil kjøre nøyaktig like lang tid som en entrådsapplikasjon som gjør det samme, eller for summen av utførelsestiden for hver tråd på CPU-en . Men her venter en ubehagelig effekt på oss. La oss se på programmet:

med open("test1.txt" , "w") som feil:

for i i xrange(1000000):

skriv ut >> feil, 1


Dette programmet skriver ganske enkelt en million linjer med "1" til en fil og gjør det på ~0,35 sekunder på datamaskinen min.

La oss se på et annet program:

fra trådimport Tråd

def writer(filnavn, n):

med åpen(filnavn, "w") som feil:

for i i xrange(n):

skriv ut >> feil, 1

t1 = Thread(target=writer, args=("test2.txt", 500 000 ,))

t2 = Thread(target=writer, args=("test3.txt", 500 000 ,))

t1.start()

t2.start()

t1.join()

t2.join()


Dette programmet lager 2 tråder. I hver strøm skriver den en halv million linjer med "1" til en separat fil. I hovedsak er mengden arbeid den samme som det forrige programmet. Men over tid dukker det opp en interessant effekt. Programmet kan kjøre fra 0,7 sekunder til så lenge som 7 sekunder. Hvorfor skjer dette?

Dette skjer fordi når en tråd ikke trenger CPU-ressursen, frigjør den GIL, og i det øyeblikket kan både seg selv, en annen tråd, og også hovedtråden prøve å få den. Samtidig kan operativsystemet, vel vitende om at det er mange kjerner, forverre alt ved å prøve å fordele tråder mellom kjernene.

UPD: for øyeblikket er det i Python 3.2 en forbedret implementering av GIL, der dette problemet er delvis løst, spesielt på grunn av det faktum at hver tråd, etter å ha mistet kontrollen, venter en kort periode før den kan igjen ta tak i GIL (det er en tråd om dette emnet god presentasjon på engelsk)

"Det viser seg at du ikke kan skrive effektive flertrådsprogrammer i Python?" spør du. Nei, selvfølgelig, det er en vei ut, og til og med flere.

Multiprosessapplikasjoner


For å løse problemet som er beskrevet i forrige avsnitt, har Python en modul delprosess . Vi kan skrive et program som vi ønsker å kjøre i en parallell tråd (faktisk allerede en prosess). Og kjør den i en eller flere tråder i et annet program. Denne metoden ville virkelig fremskynde driften av programmet vårt, fordi trådene som er opprettet i GIL-starteren ikke tar over, men bare venter på fullføringen av den kjørende prosessen. Imidlertid er det mange problemer med denne metoden. Hovedproblemet er at det blir vanskelig å overføre data mellom prosesser. Vi måtte på en eller annen måte serialisere objekter, etablere kommunikasjon gjennom PIPE eller andre verktøy, men alt dette bærer uunngåelig overhead og koden blir vanskelig å forstå.

En annen tilnærming kan hjelpe oss her. Python har en multiprosesseringsmodul . I funksjonalitet ligner denne modulen tråding . For eksempel kan prosesser lages på samme måte fra vanlige funksjoner. Metodene for å jobbe med prosesser er nesten de samme som for tråder fra trådingsmodulen. Men for å synkronisere prosesser og utveksle data er det vanlig å bruke andre verktøy. Vi snakker om køer (Queue) og kanaler (Pipe). Imidlertid er analoger av låser, hendelser og semaforer som var i tråd også til stede her.

I tillegg har multiprosesseringsmodulen en mekanisme for å arbeide med delt minne. For dette formålet inneholder modulen variable (Value) og array (Array) klasser, som kan "deles" mellom prosesser. For å gjøre det enklere å jobbe med delte variabler, kan du bruke Manager-klasser. De er mer fleksible og enklere å bruke, men tregere. Det er verdt å merke seg den fine muligheten til å lage vanlige typer fra ctypes-modulen ved å bruke multiprocessing.sharedctypes-modulen.

Multiprosesseringsmodulen har også en mekanisme for å lage prosesspooler. Denne mekanismen er veldig praktisk å bruke for å implementere Master-Worker-mønsteret eller for å implementere et parallelt kart (som på en måte er et spesialtilfelle av Master-Worker).

Blant hovedproblemene med å jobbe med multiprosessmodulen, er det verdt å merke seg den relative plattformavhengigheten til denne modulen. Siden arbeid med prosesser er organisert ulikt i ulike operativsystemer, er det pålagt noen begrensninger for koden. For eksempel har ikke Windows en gaffelmekanisme, så prosesseparasjonspunktet må pakkes inn i:

hvis __navn__ == "__main__" :


Imidlertid er dette designet allerede i god form.

Hva annet...


Det finnes andre biblioteker og tilnærminger for å skrive parallelle applikasjoner i Python. Du kan for eksempel bruke Hadoop+Python eller ulike MPI-implementeringer i Python (pyMPI, mpi4py). Du kan til og med bruke wrappers av eksisterende C++- eller Fortran-biblioteker. Her kan vi nevne slike rammer/biblioteker som Pyro, Twisted, Tornado og mange andre. Men alt dette er utenfor rammen av denne artikkelen.

Hvis du likte stilen min, vil jeg i neste artikkel prøve å fortelle deg hvordan du skriver enkle tolker i PLY og hva de kan brukes til.

Kapittel nr. 10.

Flertrådede applikasjoner

Multitasking i moderne operativsystemer tas for gitt [ Før bruken av Apple OS X hadde ikke Macintosh-datamaskiner moderne multitasking-operativsystemer. Det er veldig vanskelig å designe et operativsystem med full multitasking på riktig måte, så OS X måtte være basert på Unix.]. Brukeren forventer at når du kjører et tekstredigeringsprogram og en e-postklient samtidig, vil disse programmene ikke komme i konflikt, og når de mottar e-post, vil ikke redaktøren slutte å fungere. Når flere programmer kjører samtidig, bytter operativsystemet raskt mellom programmer, og gir dem en prosessor etter tur (med mindre, selvfølgelig, datamaskinen har flere prosessorer installert). Som et resultat er det opprettet illusjon kjører flere programmer samtidig, siden selv den beste maskinskriveren (og den raskeste Internett-tilkoblingen) ikke vil holde tritt med en moderne prosessor.

Multithreading kan på en måte sees på som neste nivå av multitasking: i stedet for å bytte mellom forskjellige programmer, operativsystemet bytter mellom ulike deler av det samme programmet. For eksempel lar en flertråds e-postklient deg godta nye e-postmeldinger mens du leser eller skriver nye meldinger. I dag er multithreading også tatt for gitt av mange brukere.

VB har aldri hatt skikkelig multithreading-støtte. Riktignok dukket en av variantene opp i VB5 - samarbeidende strømmemodell(leilighetsgjenging). Som du snart vil se, gir samarbeidsmodellen programmereren noen av fordelene med multithreading, men den drar ikke full nytte av alle funksjonene. Før eller siden må du bytte fra en treningsmaskin til en ekte, og VB .NET var den første versjonen av VB som støttet den gratis flertrådede modellen.

Imidlertid er multithreading ikke en funksjon som lett kan implementeres i programmeringsspråk eller lett læres av programmerere. Hvorfor?

Fordi flertrådede applikasjoner kan ha veldig vanskelige feil som vises og forsvinner uforutsigbart (og disse er de vanskeligste feilene å feilsøke).

Rettferdig advarsel: multithreading er et av de vanskeligste områdene innen programmering. Den minste uoppmerksomhet fører til utseendet på subtile feil, hvis korrigering koster astronomiske summer. Av denne grunn inneholder dette kapitlet mange dårlig eksempler - vi skrev dem bevisst på en slik måte at de demonstrerte typiske feil. Dette er den sikreste tilnærmingen til å lære flertrådsprogrammering: du bør kunne oppdage potensielle problemer når alt ser ut til å fungere bra, og vite hvordan du løser dem. Hvis du vil bruke flertrådede programmeringsteknikker, kan du ikke klare deg uten det.

Dette kapittelet vil legge et solid grunnlag for videre selvstendig arbeid, men vi vil ikke være i stand til å beskrive flertrådsprogrammering i alle dens forviklinger – den trykte dokumentasjonen om klassene til Threading-navnerommet tar alene opp mer enn 100 sider. Hvis du ønsker å mestre flertrådsprogrammering på et høyere nivå, se spesialiserte bøker.

Men uansett hvor farlig flertrådsprogrammering kan være, er den uerstattelig for profesjonell løsning av visse problemer. Hvis programmene dine ikke bruker multithreading der det er hensiktsmessig, vil brukerne bli veldig skuffet og velge et annet produkt. For eksempel introduserte bare den fjerde versjonen av det populære e-postprogrammet Eudora multi-threaded-funksjoner, uten hvilke det er umulig å forestille seg et moderne program for å jobbe med e-post. Da Eudora introduserte støtte for multithreading, hadde mange brukere (inkludert en av forfatterne av denne boken) byttet til andre produkter.

Til slutt, enkelttrådede programmer eksisterer rett og slett ikke i .NET. Alle.NET-programmer er flertrådede fordi søppelsamleren kjører som en lavprioritet bakgrunnsprosess. Som vist nedenfor, når du utfører seriøs grafisk programmering i .NET, hjelper riktig trådkommunikasjon med å forhindre at GUI blokkerer når programmet utfører langvarige operasjoner.

Vi introduserer Multithreading

Hvert program fungerer i en bestemt kontekst, som beskriver distribusjonen av kode og data i minnet. Lagring av konteksten lagrer faktisk tilstanden til programtråden, slik at den kan gjenopprettes i fremtiden og programmet fortsetter å kjøre.

Å lagre kontekst innebærer en viss mengde tid og minne. Operativsystemet husker tilstanden til programtråden og overfører kontrollen til en annen tråd. Når et program vil fortsette å kjøre en suspendert tråd, må den lagrede konteksten gjenopprettes, noe som tar enda mer tid. Derfor bør multithreading kun brukes når fordelene oppveier kostnadene. Noen typiske eksempler er listet opp nedenfor.

  • Funksjonaliteten til programmet er klart og naturlig delt inn i flere heterogene operasjoner, som i eksemplet med å motta e-post og forberede nye meldinger.
  • Programmet utfører lange og komplekse beregninger, og du vil ikke at GUI-en skal blokkeres mens den gjør beregningene.
  • Programmet kjører på en multiprosessordatamaskin med et operativsystem som støtter bruk av flere prosessorer (så lenge antallet aktive tråder ikke overstiger antall prosessorer, koster parallellkjøring praktisk talt ingen kostnader forbundet med å bytte tråder).

Før du går videre til mekanikken til flertrådede programmer, er det nødvendig å påpeke en omstendighet som ofte forårsaker misforståelser blant nykommere innen flertrådsprogrammering.

Programtråden vil utføre prosedyren, ikke objektet.

Det er vanskelig å si hva vi mener med «et objekt utføres», men en av forfatterne underviser ofte på seminarer om flertrådsprogrammering, og dette spørsmålet stilles oftere enn andre. Man kan tro at en tråd starter med å kalle New-metoden til en klasse, hvoretter tråden behandler alle meldinger som sendes til det tilsvarende objektet. Slike ideer absolutt er feil. Ett objekt kan inneholde flere tråder som utfører forskjellige (og noen ganger til og med de samme) metodene, mens meldinger fra objektet sendes og mottas av flere forskjellige tråder (forresten, dette er en av årsakene som gjør flertrådsprogrammering vanskelig: i for å feilsøke et program, må du vite hvilken tråd som i et gitt øyeblikk utfører en eller annen prosedyre!).

Fordi tråder opprettes basert på objektmetoder, opprettes vanligvis selve objektet før tråden. Etter vellykket opprettelse av et objekt, oppretter programmet en tråd, gir den adressen til objektets metode, og først etter det instruerer tråden om å begynne kjøringen. Prosedyren som tråden ble opprettet for, som alle prosedyrer, kan opprette nye objekter, utføre operasjoner på eksisterende objekter og kalle andre prosedyrer og funksjoner som er innenfor dens omfang.

Tråder kan også utføre vanlige klassemetoder. I dette tilfellet, husk også en annen viktig omstendighet: tråden avsluttes ved å avslutte prosedyren den ble opprettet for. Før du avslutter prosedyren, er normal avslutning av programtråden umulig.

Tråder kan avsluttes ikke bare naturlig, men også unormalt. Dette anbefales generelt ikke. For mer informasjon, se avsnittet Avslutte og avbryte tråder.

De viktigste .NET-fasilitetene knyttet til bruk av programtråder er konsentrert i Threading-navneområdet. Derfor bør de fleste flertrådede programmer starte med følgende linje:

Importerer System.Threading

Import av et navneområde forenkler programregistrering og lar deg bruke IntelliSense-teknologi.

Den direkte sammenhengen mellom tråder og prosedyrer antyder at tråder spiller en viktig rolle i dette bildet. delegater(se kapittel 6). Spesielt inkluderer Threading-navneområdet ThreadStart-delegaten, som ofte brukes når du starter programtråder. Syntaksen for å bruke denne delegaten er:

Offentlig delegat Sub ThreadStart()

Kode som kalles ved hjelp av ThreadStart-delegaten må ikke ha noen parametere og ingen returverdi, så tråder kan ikke opprettes for funksjoner (som returnerer en verdi) eller prosedyrer med parametere. For å sende informasjon fra en strøm, må du også se etter alternative måter, siden utførelsesmetoder ikke returnerer verdier og ikke kan bruke pass-by-referanse. For eksempel, hvis ThreadMethod-prosedyren er i WilluseThread-klassen, kan ThreadMethod kommunisere informasjon ved å endre egenskapene til forekomster av WillUseThread-klassen.

Applikasjonsdomener

.NET-programtråder kjører i det som kalles applikasjonsdomener, definert i dokumentasjonen som "et isolert miljø der en applikasjon kjører." Et applikasjonsdomene kan betraktes som en lett versjon av Win32-prosesser; en enkelt Win32-prosess kan inneholde flere applikasjonsdomener. Hovedforskjellen mellom applikasjonsdomener og prosesser er at en Win32-prosess har sitt eget adresseområde (dokumentasjonen sammenligner også applikasjonsdomener med logiske prosesser som kjører i en fysisk prosess). I .NET håndteres all minneadministrasjon av kjøretiden, så flere applikasjonsdomener kan kjøres i en enkelt Win32-prosess. En av fordelene med denne ordningen er de forbedrede skaleringsmulighetene til applikasjoner. Verktøy for å jobbe med applikasjonsdomener finnes i AppDomain-klassen. Vi anbefaler at du ser gjennom dokumentasjonen for denne klassen. Den kan brukes til å få informasjon om miljøet programmet kjører i. Spesielt blir AppDomain-klassen brukt når man utfører refleksjon over .NET-systemklasser. Følgende program viser en liste over innlastede sammenstillinger.

Importer System.Refleksjon

Modul Modul

Sub Main()

Dim domenet som appdomene

theDomain = AppDomain.CurrentDomain

DimAssemblies()As

Assemblies = theDomain.GetAssemblies

Dim anAssemblyxAs

For hver forsamling i forsamlinger

Console.WriteLinetanAssembly.Full Name) Neste

Console.ReadLine()

End Sub

Sluttmodul

Opprette tråder

La oss starte med et grunnleggende eksempel. La oss si at du vil kjøre en prosedyre i en egen tråd som reduserer en teller i en uendelig sløyfe. Prosedyren er definert som en del av en klasse:

Offentlig klasse vil bruke tråder

Offentlig SubtractFromCounter()

Dim teller som heltall

Gjør mens sant teller -= 1

Console.WriteLlne("Er i en annen tråd og teller ="

&telle)

Løkke

End Sub

Slutt klasse

Siden Do loop-betingelsen forblir sann til enhver tid, tror du kanskje at ingenting vil hindre SubtractFromCounter-prosedyren fra å utføres. Dette er imidlertid ikke alltid tilfelle i en flertrådsapplikasjon.

Følgende utdrag viser Sub Main-prosedyren som starter tråden og Importer-kommandoen:

Alternativ Strict On Imports System.Threading Module Modulel

Sub Main()

1 Dim myTest As New WillUseThreads()

2 Dim bThreadStart As New ThreadStart(AddressOf _

myTest.SubtractFromCounter)

3 Dim bThread As New Thread(bThreadStart)

4" bThread.Start()

Dim i As Integer

5 Gjør mens sant

Console.WriteLine("I hovedtråden og antall er " & i) i += 1

Løkke

End Sub

Sluttmodul

La oss se på de viktigste punktene en etter en. For det første fungerer Sub Man n-prosedyren alltid hovedtråden(hovedtråd). I .NET-programmer er det alltid minst to tråder som kjører: hovedtråden og søppeltråden. Linje 1 oppretter en ny forekomst av testklassen. På linje 2 oppretter vi en ThreadStart-delegat og sender adressen til SubtractFromCounter-prosedyren til forekomsten av testklassen opprettet på linje 1 (denne prosedyren kalles uten parametere). FlinkVed å importere Threading-navneområdet kan det lange navnet utelates. Det nye trådobjektet opprettes på linje 3. Legg merke til at ThreadStart-delegaten sendes når Thread-klassekonstruktøren kalles. Noen programmerere foretrekker å kombinere disse to linjene til en logisk linje:

Dim bThread As New Thread(New ThreadStarttAddressOf _

myTest.SubtractFromCounter))

Til slutt "starter" linje 4 tråden ved å kalle opp Start-metoden til Thread-klasseforekomsten som er opprettet for ThreadStart-delegaten. Ved å kalle denne metoden indikerer vi overfor operativsystemet at Subtract-prosedyren skal kjøres i en egen tråd.

Ordet "starter" i forrige avsnitt er i anførselstegn fordi det viser en av de mange raritetene ved flertrådsprogrammering: å ringe Start starter faktisk ikke tråden! Den forteller deg ganske enkelt at operativsystemet skal planlegge at den angitte tråden skal kjøres, men at selve utførelsen er utenfor programmets kontroll. Du vil ikke kunne begynne å kjøre tråder på egen hånd fordi operativsystemet alltid kontrollerer utføringen av tråder. I en senere del vil du lære hvordan du bruker prioritet for å tvinge operativsystemet til å starte tråden raskere.

I fig. Figur 10.1 viser et eksempel på hva som kan skje etter å ha kjørt et program og deretter avbrutt det med Ctrl+Break-tasten. I vårt tilfelle startet den nye tråden først etter at telleren i hovedtråden økte til 341!

Ris. 10.1. Enkel flertråds programmatisk kjøretid

Hvis programmet kjører over lengre tid, vil resultatet se omtrent ut som det vist i fig. 10.2. Vi ser at duDen løpende tråden er suspendert og kontrollen overføres tilbake til hovedtråden. I dette tilfellet er det en manifestasjon forebyggende flertråding gjennom tidsskjæring. Betydningen av dette skremmende begrepet er forklart nedenfor.

Ris. 10.2. Bytte mellom tråder i et enkelt flertråds program

Når du avbryter tråder og overfører kontroll til andre tråder, bruker operativsystemet prinsippet om forebyggende multithreading gjennom tidsslicing. Time slicing løser også et av de vanlige problemene som pleide å oppstå i flertrådede programmer - én tråd tar opp all CPU-tiden og gir ikke fra seg kontrollen til andre tråder (vanligvis skjer dette i intensive løkker som den ovenfor). For å forhindre CPU-hogging, bør trådene dine overføre kontrollen til andre tråder fra tid til annen. Hvis programmet viser seg å være "bevisstløs", er det en annen, litt mindre ønskelig løsning: Operativsystemet foregriper alltid den løpende tråden, uavhengig av prioritetsnivået, slik at tilgang til prosessoren gis til hver tråd i systemet.

Fordi kvantiseringsskjemaene til alle versjoner av Windows som kjører .NET tildeler en minimumstidsdel til hver tråd, er CPU-hogging-problemer mindre alvorlige i .NET-programmering. På den annen side, hvis .NET-rammeverket noen gang blir tilpasset andre systemer, kan situasjonen endre seg.

Hvis vi inkluderer følgende linje i programmet vårt før vi kaller Start, vil selv tråder med minimal prioritet få litt CPU-tid:

bThread.Priority = ThreadPriority.Highest

Ris. 10.3. Tråden med høyest prioritet begynner vanligvis å løpe raskere

Ris. 10.4. CPU er også gitt til lavere prioriterte tråder

Kommandoen tildeler den nye tråden høyeste prioritet og reduserer prioriteten til hovedtråden. Fra fig. 10.3 viser at den nye tråden begynner å fungere raskere enn før, men som fig. 10.4 får hovedtråden også kontrollsjon (riktignok veldig kort og først etter at tråden har kjørt lenge med subtraksjon). Når du kjører programmet på datamaskinene dine, vil du få resultater som ligner på de som er vist i fig. 10.3 og 10.4, men på grunn av forskjeller mellom systemene våre vil det ikke være et eksakt samsvar.

ThreadPrlority-oppregningstypen inneholder verdier for fem prioritetsnivåer:

ThreadPriority.Highest

TrådPrioritet.Overnormal

ThreadPrlority.Normal

ThreadPriority.BelowNormal

Trådprioritet.Lavest

Bli med metode

Noen ganger må en programtråd suspenderes til en annen tråd er fullført. La oss si at du vil suspendere tråd 1 til tråd 2 fullfører sine beregninger. For dette fra stream 1 Join-metoden kalles for tråd 2. Med andre ord kommandoen

tråd2.Bli med()

suspenderer gjeldende tråd og venter på at tråd 2 skal fullføres. Tråd 1 går inn låst tilstand.

Hvis du kobler tråd 1 til tråd 2 ved hjelp av Join-metoden, vil operativsystemet automatisk starte tråd 1 etter at tråd 2 er fullført. Merk at oppstartsprosessen er ikke-deterministisk: Det er umulig å si nøyaktig hvor lenge etter at tråd 2 er ferdig vil tråd 1 starte. Det er en annen versjon av Join som returnerer en boolsk verdi:

tråd2.Bli med (heltall)

Denne metoden venter enten på at tråd 2 skal fullføres eller opphever blokkering av tråd 1 etter at en spesifisert tid har gått, noe som får operativsystemplanleggeren til å allokere prosessortid til tråden igjen. Metoden returnerer True hvis tråd 2 avsluttes før det angitte tidsavbruddsintervallet utløper, og False ellers.

Husk den grunnleggende regelen: om tråd 2 har gått ut eller tidsavbrutt, har du ingen kontroll over når tråd 1 er aktivert.

Trådnavn, CurrentThread og ThreadState

Thread.CurrentThread-egenskapen returnerer en referanse til trådobjektet som kjører.

Selv om det er et fantastisk trådvindu for feilsøking av flertrådede applikasjoner i VB .NET, som er beskrevet nedenfor, hjalp kommandoen oss veldig ofte.

MsgBox(Thread.CurrentThread.Name)

Det viste seg ofte at koden ble kjørt i en helt annen tråd enn den den skulle kjøres i.

La oss huske at begrepet "ikke-deterministisk planlegging av programtråder" betyr en veldig enkel ting: programmereren har praktisk talt ingen midler til rådighet for å påvirke planleggerens arbeid. Av denne grunn bruker programmer ofte ThreadState-egenskapen for å returnere informasjon om den nåværende tilstanden til tråden.

Streams-vinduet

Tråder-vinduet i Visual Studio .NET gir uvurderlig hjelp til å feilsøke flertrådede programmer. Den aktiveres av undermenykommandoen Debug > Windows i avbruddsmodus. La oss si at du har gitt et navn til en bThread med følgende kommando:

bThread.Name = "Truk av tråd"

En omtrentlig visning av trådvinduet etter å ha avbrutt programmet ved hjelp av Ctrl+Break-tastkombinasjonen (eller en annen metode) er vist i fig. 10.5.

Ris. 10.5. Streams-vinduet

Pilen i den første kolonnen indikerer den aktive tråden som returneres av egenskapen Thread.CurrentThread. ID-kolonnen inneholder numeriske tråd-ID-er. Den neste kolonnen viser trådnavnene (hvis de har blitt tildelt). Plasseringskolonnen indikerer prosedyren som utføres (for eksempel WriteLine-prosedyren for konsollklassen i figur 10.5). De resterende kolonnene inneholder informasjon om prioriterte og suspenderte tråder (se neste avsnitt).

Trådvinduet (ikke operativsystemet!) lar deg administrere programmets tråder ved hjelp av kontekstmenyer. Du kan for eksempel stoppe den gjeldende tråden ved å høyreklikke på den tilsvarende linjen og velge Frys (den stoppede tråden kan gjenopptas senere). Stoppe tråder brukes ofte i feilsøking for å forhindre at en tråd som ikke fungerer som den skal forstyrre applikasjonen. I tillegg lar Tråder-vinduet deg aktivere en annen (ikke stoppet) tråd; For å gjøre dette, høyreklikk på ønsket linje og velg Bytt til tråd-kommandoen i kontekstmenyen (eller bare dobbeltklikk på trådlinjen). Som vi skal vise senere, er dette veldig nyttig for å diagnostisere potensielle vranglåser.

Setter en tråd på pause

Midlertidig ubrukte tråder kan settes i passiv tilstand ved hjelp av Slever-metoden. En passiv tråd anses også som blokkert. Selvfølgelig, når en tråd settes i en passiv tilstand, vil de gjenværende trådene motta flere prosessorressurser. Standardsyntaksen for Sleep-metoden er som følger: Thread.Sleep(interval_in_milliseconds)

Å ringe Sleep fører til at den aktive tråden blir inaktiv i minst det angitte antallet millisekunder (det er imidlertid ikke garantert å våkne umiddelbart etter at det angitte intervallet har gått). Vennligst merk: når du kaller metoden, sendes ikke en referanse til en spesifikk tråd - Sleep-metoden kalles kun for den aktive tråden.

En annen versjon av Sleep får den gjeldende tråden til å avstå resten av sin tildelte CPU-tid:

Tråd.Søvn(0)

Følgende alternativ setter den gjeldende tråden i en passiv tilstand i ubegrenset tid (aktivering skjer bare når avbrudd kalles):

Thread.Slayer(Timeout.Uendelig)

Fordi passive tråder (selv med en uendelig tidsavbrudd) kan avbrytes av Interrupt-metoden, noe som forårsaker at en ThreadInterruptException blir kastet, er Slayer-kallet alltid omsluttet av en Try-Catch-blokk, som i følgende kodebit:

Prøve

Tråd.Søvn(200)

"Den passive flyttilstanden har blitt avbrutt

Catch e Som unntak

"Andre unntak

Avslutt Prøv

Hvert .NET-program kjører på en programtråd, så Sleep-metoden brukes også til å suspendere programmer (hvis Threadipg-navneområdet ikke importeres av programmet, må du bruke det fullstendige navnet Threading.Thread. Sleep).

Avslutte eller avbryte programtråder

Tråden avsluttes automatisk når metoden som er spesifisert når ThreadStart-delegaten opprettes avsluttes, men noen ganger vil du avslutte metoden (og dermed tråden) når visse faktorer oppstår. I slike tilfeller sjekker tråder vanligvis betinget variabel, avhengig av hvilken tilstanddet tas beslutning om nødutgang fra strømmen. Vanligvis er en Do-While-løkke inkludert i prosedyren for å gjøre dette:

Sub ThreadedMethod()

«Programmet må gi midler for avhør

" betinget variabel.

"For eksempel kan en betingelsesvariabel skrives som en egenskap

Gjør mens conditionVariable = False And MoreWorkToDo

"Hovedkode

Loop End Sub

Det tar litt tid å spørre den betingede variabelen. Kontinuerlig polling i sløyfetilstand bør kun brukes hvis du forventer at tråden avsluttes for tidlig.

Hvis betingelsesvariabelen må testes på et bestemt sted, bruk If-Then-kommandoen i kombinasjon med Exit Sub inne i en uendelig sløyfe.

Tilgang til en tilstandsvariabel må synkroniseres slik at interferens fra andre tråder ikke forstyrrer normal bruk. Dette viktige emnet er diskutert i delen "Problemløsning: Synkronisering".

Dessverre kjøres ikke koden til passive (eller på annen måte blokkerte) tråder, så alternativet for å polle en betingelsesvariabel er ikke egnet for dem. I dette tilfellet bør du kalle Interrupt-metoden på objektvariabelen som inneholder en referanse til ønsket tråd.

Avbruddsmetoden kan bare kalles på tråder som er i vente-, hvile- eller bli med-tilstand. Hvis du kaller Interrupt på en tråd som er i en av de listede tilstandene, vil tråden etter en stund begynne å kjøre igjen, og kjøretiden vil kaste et ThreadlnterruptedException på tråden. Dette skjer selv om tråden har blitt satt i en passiv tilstand på ubestemt tid ved å kalle Thread.Sleepdimeout. Uendelig). Vi sier "over tid" fordi trådplanlegging er ikke-deterministisk av natur. Unntaket ThreadlnterruptedExcepti on fanges opp av Catch-delen, som inneholder koden for å avslutte ventetilstanden. Catch-delen er imidlertid ikke nødvendig for å avslutte tråden ved å kalle Avbryt; tråden håndterer unntaket etter eget skjønn.

I .NET kan Interrupt-metoden kalles selv på ublokkerte tråder. I dette tilfellet blir tråden avbrutt ved neste blokk.

Suspendere og drepe tråder

Threading-navneområdet inneholder andre metoder som forstyrrer den normale funksjonen til tråder:

  • utsette;
  • Abort

Det er vanskelig å si hvorfor .NET inkluderte støtte for disse metodene - å ringe Suspend and Abort vil mest sannsynlig føre til at programmet blir ustabilt. Ingen av metodene lar deg deinitialisere en tråd normalt. I tillegg, når du kaller Suspend eller Abort, er det ingen måte å forutsi hvilken tilstand tråden vil forlate objektene i etter suspendering eller avbrutt.

Oppfordringen til å avbryte gir et ThreadAbortException. For å hjelpe deg å forstå hvorfor dette merkelige unntaket ikke skal håndteres i programmer, er her et utdrag fra .NET SDK-dokumentasjonen:

"...Når en tråd blir ødelagt ved å kalle Abort, kaster kjøretiden et ThreadAbortException. Dette er en spesiell type unntak som ikke kan fanges opp av et program. Når dette unntaket er hevet, kjører kjøretiden alle Finally-blokker før tråden drepes. Siden Endelig-blokker kan utføre hvilken som helst handling, ring Join for å sikre at tråden blir ødelagt."

Moral: Abort og Suspend anbefales ikke (og hvis du fortsatt ikke kan klare deg uten Suspend, gjenoppta den suspenderte tråden ved å bruke Resume-metoden). Den eneste måten å avslutte en tråd på er ved å polle en synkronisert tilstandsvariabel eller ved å kalle avbruddsmetoden diskutert ovenfor.

Bakgrunnstråder (demoner)

Noen bakgrunnstråder slutter automatisk å kjøre når andre programkomponenter stopper. Spesielt går søppelsamleren i en av bakgrunnstrådene. Vanligvis opprettes bakgrunnstråder for å motta data, men dette gjøres kun hvis det kjører kode i andre tråder som kan behandle de mottatte dataene. Syntaks: trådnavn.IsBackGround = True

Hvis det bare er bakgrunnstråder igjen i applikasjonen, avsluttes applikasjonen automatisk.

Et mer alvorlig eksempel: å trekke ut data fra HTML-kode

Vi anbefaler å bruke tråder kun når programmets funksjonalitet er tydelig delt inn i flere operasjoner. Et godt eksempel er HTML-dataekstraktoren fra kapittel 9. Klassen vår gjør to ting: henter data fra Amazon og behandler dem. Dette er et perfekt eksempel på en situasjon der flertrådsprogrammering virkelig er passende. Vi lager klasser for flere forskjellige arbeidsbøker og analyserer deretter dataene i forskjellige tråder. Å lage en ny tråd for hver arbeidsbok forbedrer programmets effektivitet fordi mens en tråd mottar data (som kan kreve venting på Amazon-serveren), vil en annen tråd være opptatt med å behandle data som allerede er mottatt.

Den flertrådede versjonen av dette programmet fungerer mer effektivt enn den entrådede versjonen bare på en datamaskin med flere prosessorer eller hvis mottak av tilleggsdata effektivt kan kombineres med analysen.

Som nevnt ovenfor kan bare prosedyrer som ikke har noen parametere kjøres i tråder, så du må gjøre noen mindre endringer i programmet. Nedenfor er hovedprosedyren, omskrevet for å ekskludere parametrene:

Public Sub FindRank()

m_Rank = ScrapeAmazon()

Console.WriteLine("rangeringen av " & m_Name & "Er " & GetRank)

End Sub

Siden vi ikke vil kunne bruke en kombinasjonsboks for å lagre og hente informasjon (skriving av flertrådede GUI-programmer er dekket i siste del av dette kapitlet), lagrer programmet dataene for de fire bøkene i en matrise hvis definisjon begynner som dette:

Dim theBook(3.1) As String theBook(0.0) = "1893115992"

theBook(0.l) = "Programmering av VB .NET" " Etc.

Fire tråder opprettes i samme løkke som lager AmazonRanker-objekter:

For i= 0, så 3

Prøve

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 unntak

Console.WriteLine(e.Message)

Avslutt Prøv

Neste

Nedenfor er hele programmets tekst:

Alternativ Streng ved import System.IO Importer System.Net

Importerer System.Threading

Modul Modul

Sub Main()

Demp boken(3.1) som streng

theBook(0.0) = "1893115992"

theBook(0.l) = "Programmering av VB .NET"

theBook(l.0) = "1893115291"

theBook(l.l) = "Databaseprogrammering VB .NET"

theBook(2,0) = "1893115623"

theBook(2.1) = "Programmørens introduksjon til 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

Demp en tråd som tråd

For i = 0 til 3

Prøve

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 unntak

Console.WriteLlnete.Message)

Avslutt Prøv neste

Console.ReadLine()

End Sub

Sluttmodul

Offentlig klasse AmazonRanker

Privat m_URL som streng

Privat m_Rank som heltall

Privat m_navn som streng

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("rangeringen av " & m_Name & "er "

& GetRank) End Sub

Offentlig skrivebeskyttet eiendom GetRank() As String Get

Hvis m_Rank<>0 Deretter

Returner CStr(m_Rank) Else

"Problemer

Slutt om

Slutt Get

Avslutt eiendom

Offentlig skrivebeskyttet egenskap GetName() Som streng Get

Returner m_Name

Slutt Get

Avslutt eiendom

Privat funksjon ScrapeAmazon() Som heltall Prøv

Demp nettadressen som ny Uri(m_URL)

Dim theRequest As WebRequest

theRequest =WebRequest.Create(nettadressen)

Dim theResponse Som WebResponse

theResponse = theRequest.GetResponse

Dim aReader As New StreamReader(theResponse.GetResponseStream())

Demp dataene som streng

theData = aReader.ReadToEnd

Returner Analyse(theData)

Catch E som unntak

Console.WriteLine(E.Message)

Console.WriteLine(E.StackTrace)

Konsoll. ReadLine()

Avslutt Prøv Avslutt funksjon

Privat funksjonsanalyse(ByVal theData As String) Som heltall

Dim plassering As.Integer Location = theData.IndexOf(" Amazon.com

Salgsrangering:") _

+ "Amazon.com salgsrangering:".Lengde

Dim temp som streng

Gjør til theData.Substring(Location.l) = "<" temp = temp

&theData.Substring(Location.l) Plassering += 1 sløyfe

ReturnClnt(temp)

Avslutt funksjon

Slutt klasse

Multithreading er vanlig i .NET- og I/O-navneområder, så .NET Framework-biblioteket tilbyr spesielle asynkrone metoder for dem. For mer informasjon om bruk av asynkrone metoder når du skriver flertrådede programmer, se BeginGetResponse- og EndGetResponse-metodene for HTTPWebRequest-klassen

Hovedfare (generelle data)

Til nå har det eneste sikre tilfellet med bruk av tråder blitt vurdert - trådene våre endret ikke de delte dataene. Hvis du tillater endringer i delte data, begynner potensielle feil å multiplisere eksponentielt og det blir mye vanskeligere å kvitte seg med programmet. På den annen side, hvis du forhindrer forskjellige tråder i å endre delte data, er .NET flertrådsprogrammering i hovedsak det samme som de begrensede mulighetene til VB6.

Vi presenterer for din oppmerksomhet et lite program som viser problemene som oppstår uten å gå i unødvendige detaljer. Dette programmet simulerer et hus med en termostat installert i hvert rom. Hvis temperaturen er 5 grader Fahrenheit eller mer (ca. 2,77 grader Celsius) for kald, ber vi varmesystemet øke temperaturen 5 grader; ellers stiger temperaturen bare 1 grad. Hvis den aktuelle temperaturen er større enn eller lik innstilt temperatur, gjøres ingen endring. Temperaturkontroll i hvert rom utføres av en egen tråd med 200 millisekunders forsinkelse. Hovedarbeidet er utført av følgende fragment:

Hvis mHouse.HouseTemp< mHouse.MAX_TEMP = 5 Then Try

Tråd.Søvn(200)

Catch tie As ThreadlnterruptedException

«Den passive ventingen ble avbrutt

Catch e Som unntak

" Andre sluttprøve-unntak

mHouse.HouseTemp +- 5" Etc.

Nedenfor er den komplette kildekoden til programmet. Resultatet er vist i fig. 10.6: Temperaturen i huset nådde 105 grader Fahrenheit (40,5 grader Celsius)!

1 Alternativ Strengt På

2 Importer System.Threading

3 modulmodul

4 Sub Main()

5 Dim myHouse As New House(l0)

6 Konsoll. ReadLine()

7 End Sub

8 Sluttmodul

9 Public Class House

10 Public Const MAX_TEMP Som heltall = 75

11 Privat mCurTemp Som heltall = 55

12 Private mRoom() Som rom

13 Public Sub New(ByVal numOfRooms As Heltall)

14 Redim mRooms(antall Rom = 1)

15 Dim i Som heltall

16 Dim aThreadStart As Threading.ThreadStart

17 Demp en tråd som tråd

18 For i = 0 Til numOfRooms -1

19 forsøk

20 mRooms(i)=NewRoom(Me, mCurTemp,CStr(i) &"throom")

21 aThreadStart - New ThreadStart(AddressOf _

mRooms(i).CheckTempInRoom)

22 aThread =Ny Thread(aThreadStart)

23 aThread.Start()

24 Catch E som unntak

25 Console.WriteLine(E.StackTrace)

26 Avslutt Prøv

27 Neste

28 End Sub

29 Offentlig eiendom HusTemp()Som heltall

tretti . Få

31 Returner mCurTemp

32 Slutt Get

33 Sett (ByVal Value As Heltall)

34 mCurTemp = Verdi 35 Sluttsett

36 Slutt eiendom

37 Sluttklasse

38 Offentlig klasserom

39 Privat mCurTemp Som heltall

40 Privat mName Som streng

41 Privat mHouse As House

42 Public Sub New(ByVal theHouse As House,

ByVal temp As Integer, ByVal roomName As String)

43 mHus = huset

44 mCurTemp = temp

45 mNavn = romnavn

46 End Sub

47 Offentlig undersjekkTempInRoom()

48 ChangeTemperature()

49 End Sub

50 Private Sub ChangeTemperature()

51 forsøk

52 Hvis mHouse.HouseTemp< mHouse.MAX_TEMP - 5 Then

53 tråd.Søvn(200)

54 mHouse.HouseTemp +- 5

55 Console.WriteLine("Am in" & Me.mName & _

56 ". Nåværende temperatur er "&mHouse.HouseTemp)

57. Elself mHouse.HouseTemp< mHouse.MAX_TEMP Then

58 tråd.Søvn(200)

59 mHouse.HouseTemp += 1

60 Console.WriteLine("Am in" & Me.mName & _

61 ". Nåværende temperatur er " & mHouse.HouseTemp)

62 Ellers

63 Console.WriteLine("Am in" & Me.mName & _

64 ". Nåværende temperatur er " & mHouse.HouseTemp)

65 "Gjør ingenting, temperaturen er normal

66 Avslutt If

67 Catch tae As ThreadlninterruptedException

68 "Passiv venting ble avbrutt

69 Catch e Som unntak

70" Andre unntak

71 Avslutt Prøv

72 End Sub

73 Sluttklasse

Ris. 10.6. Problemer med flere tråder

Sub Main-prosedyren (linje 4-7) lager et "hus" med ti "rom". Husklassen setter en maksimal temperatur på 75 grader Fahrenheit (omtrent 24 grader Celsius). Linje 13-28 definerer en ganske kompleks huskonstruktør. Nøkkelen til å forstå programmet er linje 18-27. Linje 20 oppretter et annet romobjekt, og sender en referanse til husobjektet til konstruktøren slik at romobjektet kan få tilgang til det om nødvendig. Linje 21-23 starter ti tråder for å justere temperaturen i hvert rom. Romklassen er definert på linje 38-73. House coxpa objektreferanseer definert i mHouse-variabelen i konstruktøren til Room-klassen (linje 43). Koden for å kontrollere og justere temperaturen (linje 50-66) ser enkel og naturlig ut, men som du snart vil se, er dette utseendet bedrar! Merk at denne koden er vedlagt en Try-Catch-blokk fordi programmet bruker Sleep-metoden.

Det er usannsynlig at noen vil gå med på å leve i en temperatur på 105 grader Fahrenheit (40,5 24 grader Celsius). Hva skjedde? Problemet er med følgende linje:

Hvis mHouse.HouseTemp< mHouse.MAX_TEMP - 5 Then

Det som skjer er at tråd 1 sjekker temperaturen først. Den ser at temperaturen er for lav og øker den med 5 grader. Dessverre, før temperaturen stiger, avbrytes tråd 1 og kontrollen overføres til tråd 2. Tråd 2 sjekker den samme variabelen som er ikke endret ennå bekk 1. Dermed forbereder også bekk 2 seg på å heve temperaturen med 5 grader, men har ikke tid til dette og går også i ventetilstand. Prosessen fortsetter til tråd 1 aktiveres og går videre til neste kommando - øke temperaturen med 5 grader. Økningen gjentas når alle 10 bekkene er aktivert, og beboerne i huset får dårlig tid.

Løse problemet: synkronisering

I forrige program oppstår det en situasjon hvor resultatet av programmet avhenger av rekkefølgen trådene utføres i. For å bli kvitt det, må du sørge for at kommandoer liker

Hvis mHouse.HouseTemp< mHouse.MAX_TEMP - 5 Then...

behandles fullstendig av den aktive tråden før den avbrytes. Denne egenskapen kalles atomær skam - en kodeblokk må utføres av hver tråd uten avbrudd, som en atomenhet. En gruppe kommandoer kombinert til en atomblokk kan ikke avbrytes av trådplanleggeren før den er fullført. Ethvert flertråds programmeringsspråk har sine egne måter å sikre atomitet på. I VB .NET er den enkleste måten å bruke SyncLock-kommandoen, som når den kalles, sender en objektvariabel. Gjør noen mindre endringer i ChangeTemperature-prosedyren fra forrige eksempel, og programmet vil fungere fint:

Private Sub ChangeTemperature() SyncLock (mHouse)

Prøve

Hvis mHouse.HouseTemp< mHouse.MAXJTEMP -5 Then

Tråd.Søvn(200)

mHouse.HouseTemp += 5

Console.WriteLine("Er i " & Me.mName & _

".Gjeldende temperatur er " & mHouse.HouseTemp)

Ellers

mHouse.HouseTemp< mHouse. MAX_TEMP Then

Thread.Sleep(200) mHouse.HouseTemp += 1

Console.WriteLine("Am in " & Me.mName &_ ".Gjeldende temperatur er " & mHouse.HomeTemp) Else

Console.WriteLineC"Am in " & Me.mName & _ ". Nåværende temperatur er " & mHouse.HouseTemp)

"Gjør ingenting, temperaturen er normal

End If Catch tie As ThreadlnterruptedException

" Passiv venting ble avbrutt av Catch e As Exception

"Andre unntak

Avslutt Prøv

Avslutt SyncLock

End Sub

SyncLock-blokkkode kjøres atomisk. Tilgang til den fra alle andre tråder vil bli nektet til den første tråden frigjør låsen med End SyncLock-kommandoen. Hvis en tråd i en synkronisert blokk går inn i en passiv ventetilstand, forblir låsen til tråden avbrytes eller gjenopptas.

Riktig bruk av SyncLock-kommandoen sikrer at programmet er trådsikkert. Dessverre har overbruk av SyncLock en negativ innvirkning på ytelsen. Synkronisering av kode i et flertråds program reduserer hastigheten flere ganger. Synkroniser kun den mest nødvendige koden og fjern låsen så snart som mulig.

Basissamlingsklassene er ikke trådsikre i flertrådede applikasjoner, men .NET Framework inkluderer trådsikre versjoner av de fleste samlingsklasser. I disse klassene er koden for potensielt farlige metoder inneholdt i SyncLock-blokker. Trådsikre versjoner av samlingsklasser bør brukes i flertrådede programmer der dataintegriteten er i fare.

Det gjenstår å nevne at betingede variabler enkelt kan implementeres ved å bruke SyncLock-kommandoen. For å gjøre dette trenger du bare å synkronisere skrivingen til en delt boolsk egenskap som leses og skrives, som i følgende utdrag:

Offentlig klasse tilstandVariabel

Privat delt skap som objekt= Nytt objekt()

Privat delt mOK Som boolsk delt

Eiendom TheConditionVariable()As Boolean


Retur mOK

Slutt Get

Sett (ByVal Value As Boolean) SyncLock (skap)

mOK=Verdi

Avslutt SyncLock

Sluttsett

Avslutt eiendom

Slutt klasse

SyncLock Command and Monitor Class

Det er noen finesser involvert i bruk av SyncLock-kommandoen som ikke var tydelige i de enkle eksemplene ovenfor. Dermed spiller valget av synkroniseringsobjekt en svært viktig rolle. Prøv å kjøre det forrige programmet med kommandoen SyncLock(Me) i stedet for SyncLock(mHouse). Temperaturen stiger over terskelen igjen!

Husk at SyncLock-kommandoen synkroniserer iht gjenstand, sendt som en parameter, og ikke som et kodefragment. SyncLock-parameteren fungerer som en dør for tilgang til det synkroniserte fragmentet fra andre tråder. SyncLock(Me)-kommandoen åpner faktisk flere forskjellige dører, som er akkurat det du prøvde å unngå med synkronisering. Moral:

For å beskytte delte data i en flertrådsapplikasjon, må SyncLock-kommandoen synkroniseres på et enkelt objekt.

Fordi synkronisering er objektspesifikk, kan det i noen situasjoner utilsiktet blokkere andre fragmenter. La oss si at du har to synkroniserte metoder, første og andre, og begge metodene er synkronisert på et bigLock-objekt. Når tråd 1 går inn i metode først og griper bigLock, vil ingen tråd kunne gå inn i metode nummer to fordi tilgangen til den allerede er begrenset til tråd 1!

Funksjonaliteten til SyncLock-kommandoen kan betraktes som en delmengde av funksjonaliteten til Monitor-klassen. Monitor-klassen er svært konfigurerbar og kan brukes til å løse ikke-trivielle synkroniseringsproblemer. SyncLock-kommandoen er en omtrentlig analog av Enter- og Exit-metodene i Monitor-klassen:

Prøve

Monitor.Enter(theObject) Til slutt

Monitor.Exit(theObject)

Avslutt Prøv

For noen standardoperasjoner (å øke/redusere en variabel, bytte ut innholdet i to variabler), gir .NET Framework klassen Interlocked, hvis metoder utfører disse operasjonene på atomnivå. Ved å bruke Interlocked-klassen utføres disse operasjonene mye raskere enn å bruke SyncLock-kommandoen.

Gjensidig blokkering

Under synkroniseringsprosessen blir låsen anskaffet på objekter, ikke tråder, så ved bruk annerledes gjenstander å blokkere annerledes kodefragmenter i programmer forårsaker noen ganger svært ikke-trivielle feil. Dessverre, i mange tilfeller, er synkronisering på et enkelt objekt rett og slett ikke akseptabelt fordi det vil føre til at tråder blokkeres for ofte.

La oss vurdere situasjonen forrigling(deadlock) i sin enkleste form. Se for deg to programmerere ved middagsbordet. Dessverre, mellom de to har de bare en kniv og en gaffel. Hvis vi antar at både en kniv og en gaffel er nødvendig for å spise, er to situasjoner mulig:

  • En programmerer klarer å ta en kniv og gaffel og begynner å spise. Etter å ha fått nok, legger han serviset til side, og så kan en annen programmerer ta dem.
  • En programmerer tar kniven, og den andre tar gaffelen. Ingen av dem vil kunne begynne å spise med mindre den andre gir fra seg redskapet sitt.

I et flertråds program kalles denne situasjonen gjensidig blokkering. De to metodene er synkronisert på forskjellige objekter. Tråd A henter objekt 1 og går inn i programfragmentet som er beskyttet av dette objektet. Dessverre, for å fungere, trenger den tilgang til kode beskyttet av en annen Sync Lock-blokk med et annet synkroniseringsobjekt. Men før den kan gå inn i et fragment som blir synkronisert av et annet objekt, går tråd B inn og griper det objektet. Nå kan ikke tråd A gå inn i det andre fragmentet, tråd B kan ikke gå inn i det første fragmentet, og begge trådene er dømt til å vente for alltid. Ingen tråd kan fortsette å fungere fordi objektet som trengs for å gjøre det, aldri vil bli frigjort.

Diagnostisering av vranglås er komplisert av det faktum at de kan oppstå i relativt sjeldne tilfeller. Alt avhenger av rekkefølgen som planleggeren tildeler prosessortid til dem. Det er mulig at synkroniseringsobjekter i de fleste tilfeller vil bli innhentet i en rekkefølge som ikke resulterer i vranglås.

Nedenfor er en implementering av dødlåssituasjonen som nettopp er beskrevet. Etter en kort diskusjon av de viktigste punktene, vil vi vise hvordan du gjenkjenner en fastlåst situasjon i trådvinduet:

1 Alternativ Strengt På

2 Importer System.Threading

3 modulmodul

4 Sub Main()

5 Dim Tom som ny programmerer ("Tom")

6 Dim Bob som ny programmerer ("Bob")

7 Dim aThreadStart As New ThreadStart(AddressOf Tom.Eat)

8 Demp en tråd som ny tråd (en trådstart)

9 aThread.Name= "Tom"

10 Dim bThreadStart As New ThreadStarttAddressOf Bob.Eat)

11 Dim bThread As New Thread(bThreadStart)

12 bThread.Name = "Bob"

13 aThread.Start()

14 bThread.Start()

15 End Sub

16 Sluttmodul

17 Public Class Gaffel

18 Privat delt mForkAvaiTable As Boolean = True

19 Privat delt mowner As String = "Ingen"

20 Private skrivebeskyttede eiendom OwnsUtensil() Som streng

21 Få

22 Retur mEier

23 Slutt Get

24 Slutt eiendom

25 Public Sub GrabForktByVal a As Programmer)

26 Console.Writel_ine(Thread.CurrentThread.Name &_

"prøver å gripe gaffelen.")

27 Console.WriteLine(Me.OwnsUtensil & "har gaffelen.") . .

28 Monitor.Enter(Me) "SyncLock (aFork)"

29 Hvis mForkTilgjengelig Da

30 a.HasFork = Sant

31 mEier = a.Mitt navn

32 mForkTilgjengelig = Falsk

33 Console.WriteLine(a.MyName&"just got the fork.waiting")

34 Prøv

Thread.Sleep(100) Catch e As Exception Console.WriteLine (e.StackTrace)

Avslutt Prøv

35 Avslutt If

36 Monitor.Exit(Me)

Avslutt SyncLock

37 End Sub

38 Sluttklasse

39 Kniv i offentlig klasse

40 Privat delt mKnifeAvailable As Boolean = True

41 Privat delt mowner As String = "Ingen"

42 Privat skrivebeskyttet eiendom OwnsUtensi1() Som streng

43 Få

44 Retur mEier

45 Slutt Get

46 Slutt eiendom

47 Public Sub GrabKnifetByVal a As Programmer)

48 Console.WriteLine(Thread.CurrentThread.Name & _

"prøver å gripe kniven.")

49 Console.WriteLine(Me.OwnsUtensil & "har kniven.")

50 Monitor.Enter(Me) "SyncLock (aKnife)"

51 Hvis mKnifeAvailable Da

52 mKnifeAvailable = False

53 a.HasKnife = Sant

54 mEier = a.Mitt Navn

55 Console.WriteLine(a.MyName&"har akkurat kniven.venter")

56 Prøv

Tråd.Søvn(100)

Catch e Som unntak

Console.WriteLine(e.StackTrace)

Avslutt Prøv

57 Avslutt If

58 Monitor.Exit(Me)

59 End Sub

60 Sluttklasse

61 Offentlig klasseprogrammerer

62 Privat mName Som streng

63 Privat delt mFork As Fork

64 Privat delt mKnife As Knife

65 Privat mHasKnife As Boolean

66 Private mHasFork As Boolean

67 Delt 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 Offentlig skrivebeskyttet egenskap MyName() Som streng

75 Få

76 Returner mName

77 Slutt Get

78 Slutt eiendom

79 Offentlig eiendom HasKnife() Som boolsk

80 Få

81 Returner mHasKnife

82 Slutt Get

83 Set(ByVal Value As Boolean)

84 mHasKnife = Verdi

85 Sluttsett

86 Slutt eiendom

87 Offentlig eiendom HasFork() As Boolean

88 Få

89 Returner mHasFork

90 Slutt Get

91 Set(ByVal Value As Boolean)

92 mHasFork = Verdi

93 Sluttsett

94 Slutt eiendom

95 Public Sub Eat()

96 Gjør til Me.HasKnife And Me.HasFork

97 Console.Writeline(Thread.CurrentThread.Name&"er i tråden.")

98 Hvis Rnd()< 0.5 Then

99 mFork.GrabFork(Me)

100 ellers

101 mKnife.GrabKnife(Me)

102 Avslutt If

103 Sløyfe

104 MsgBox(Me.MyName & "kan spise!")

105 mKniv = Ny kniv()

106 mFork= New Fork()

107 End Sub

108 Sluttklasse

Hovedprosedyren (linje 4-16) oppretter to forekomster av Programmer-klassen og starter deretter to tråder for å utføre den kritiske Eat-metoden til Programmer-klassen (linje 95-108), beskrevet nedenfor. Hovedprosedyren setter navnene på trådene og starter dem; Sannsynligvis er alt som skjer klart og uten kommentarer.

Koden for Fork-klassen (linje 17-38) ser mer interessant ut (en lignende Knife-klasse er definert i linje 39-60). Linje 18 og 19 setter verdiene til de generelle feltene, som kan brukes til å finne ut om gaffelen er tilgjengelig for øyeblikket, og hvis ikke, hvem som bruker den. ReadOnly-egenskapen OwnUtensi1 (linje 20-24) er ment for den enkleste overføringen av informasjon. Sentralt i Fork-klassen er GrabFork "fork grab"-metoden, definert på linje 25-27.

  1. Linje 26 og 27 skriver ganske enkelt ut feilsøkingsinformasjon til konsollen. I hovedkoden til metoden (linje 28-36) synkroniseres tilgangen til gaffelen av objektet peBelte meg. Siden programmet vårt bare bruker én gaffel, sikrer Me-synkronisering at to tråder ikke kan ta tak i den samtidig. Sleep"p-kommandoen (i blokken som starter på linje 34) simulerer forsinkelsen mellom å gripe gaffelen/kniven og begynne å spise. Vær oppmerksom på at Sleep-kommandoen ikke fjerner låsen fra gjenstander og bare fremskynder forekomsten av vranglås!
    Imidlertid er koden av størst interesse koden for Programmer-klassen (linje 61-108). Linje 67-70 definerer en generisk konstruktør, som sikrer at det kun er én gaffel og kniv i programmet. Eiendomskoden (linje 74-94) er enkel og krever ingen kommentarer. Det viktigste skjer i Eat-metoden, som utføres av to separate tråder. Prosessen fortsetter i en løkke til en tråd tar tak i gaffelen sammen med kniven. På linje 98-102 griper objektet tilfeldig gaffelen/kniven ved å bruke Rnd-kallet, som er det som forårsaker vranglåsen. Følgende skjer:
    Tråden som utfører Thoth-objektets Eat-metode aktiveres og går inn i loopen. Han griper kniven og går inn i en ventende tilstand.
  2. Tråden som utfører Bob's Eat-metoden aktiveres og går inn i loopen. Den kan ikke gripe kniven, men den griper gaffelen og går inn i en ventetilstand.
  3. Tråden som utfører Thoth-objektets Eat-metode aktiveres og går inn i loopen. Den prøver å gripe gaffelen, men gaffelen er allerede fanget av Bob-objektet; tråden går inn i ventetilstand.
  4. Tråden som utfører Bob's Eat-metoden aktiveres og går inn i loopen. Han prøver å gripe kniven, men kniven er allerede fanget av Thoth-objektet; tråden går inn i ventetilstand.

Alt dette fortsetter i det uendelige - vi står overfor en typisk fastlåst situasjon (prøv å kjøre programmet, så vil du se at ingen klarer å spise slik).
Du kan også se om det har oppstått en vranglås i trådvinduet. Kjør programmet og avbryt det med Ctrl+Break-tastene. Inkluder Me-variabelen i viewporten og åpne strømmevinduet. Resultatet ser omtrent ut som det vist i fig. 10.7. Fra bildet kan du se at Bobs flyt har fanget kniven, men han har ikke en gaffel. Høyreklikk i trådvinduet på trådlinjen og velg Bytt til tråd fra kontekstmenyen. Utsiktsporten viser at Thoth har en gaffel, men ingen kniv. Dette er selvfølgelig ikke 100 % bevis, men slik oppførsel får deg i det minste til å mistenke at noe er galt.
Hvis muligheten til å synkronisere med ett objekt (som i programmet med økende -temperatur i huset) ikke er mulig, for å forhindre gjensidig blokkering, kan du nummerere synkroniseringsobjektene og alltid fange dem i en konstant rekkefølge. La oss fortsette analogien med programmerere som spiser lunsj: Hvis en tråd alltid tar kniven først og deretter gaffelen, vil det ikke være noen fastlåste problemer. Den første tråden som tar tak i kniven vil kunne spise normalt. Oversatt til språket for programflyt betyr dette at det kun er mulig å fange objekt 2 hvis objekt 1 tidligere er fanget.

Ris. 10.7. Dødlåsanalyse i trådvinduet

Derfor, hvis vi fjerner Rnd-anropet på linje 98 og erstatter det med fragmentet

mFork.GrabFork(Me)

mKnife.GrabKnife(Me)

vranglås forsvinner!

Samarbeid om data etter hvert som de opprettes

I flertrådede applikasjoner er en vanlig situasjon at tråder ikke bare fungerer på delte data, men også venter på at de skal vises (det vil si at tråd 1 må opprette dataene før tråd 2 kan bruke dem). Fordi dataene er delt, må tilgangen til dem synkroniseres. Det er også nødvendig å gi et middel til å varsle ventende tråder om at klare data er tilgjengelige.

Denne situasjonen kalles vanligvis leverandør/forbrukerproblem. En tråd prøver å få tilgang til data som ikke eksisterer ennå, så den må sende kontrollen til en annen tråd som lager dataene den trenger. Problemet er løst med kode som dette:

  • Tråd 1 (forbrukeren) våkner, går inn i den synkroniserte metoden, ser etter data, finner den ikke og går inn i ventetilstand. InnledendeTil slutt må den frigjøre låsen for ikke å forstyrre arbeidet til leverandørtråden.
  • Tråd 2 (leverandør) går inn i en synkronisert metode utgitt av tråd 1, skaper data for tråd 1 og på en eller annen måte varsler tråd 1 om at det er data. Den frigjør deretter låsen slik at tråd 1 kan behandle de nye dataene.

Ikke prøv å løse dette problemet ved å konstant aktivere tråd 1 og sjekke tilstanden til en tilstandsvariabel hvis verdi er satt av tråd 2. Denne løsningen vil alvorlig påvirke ytelsen til programmet ditt, siden tråd 1 i de fleste tilfeller vil aktiveres for ingen grunnen til; og tråd 2 vil vente så ofte at den ikke har tid til å lage data.

Produsent/forbrukerforhold er veldig vanlige, så flertrådede programmeringsklassebiblioteker skaper spesielle primitiver for slike situasjoner. I .NET kalles disse primitivene Wait og Pulse-PulseAl 1 og er en del av Monitor-klassen. Figur 10.8 forklarer situasjonen vi er i ferd med å programmere. Programmet organiserer tre trådkøer: en ventekø, en blokkeringskø og en utførelseskø. Trådplanleggeren tildeler ikke CPU-tid til tråder i ventekøen. For at en tråd skal få tildelt tid, må den flyttes til en kjørekø. Som et resultat er driften av applikasjonen organisert mye mer effektivt enn med en vanlig undersøkelse av en betinget variabel.

I pseudokode er dataforbrukerformspråket formulert som følger:

"Å gå inn i en synkronisert blokk som dette

Mens ingen data

Gå til ventekøen

Løkke

Hvis det er data, behandle dem.

Legg igjen en synkronisert blokk

Umiddelbart etter at Vent-kommandoen er utført, suspenderes tråden, låsen frigjøres og tråden går inn i ventekøen. Når låsen frigjøres, er tråden i løpskøen fri til å løpe. Over tid vil en eller flere blokkerte tråder skape dataene som er nødvendige for at tråden i ventekøen skal fungere. Siden datavalidering utføres i en loop, skjer overgangen til databruk (etter loopen) kun når det er data klare til å behandles.

I pseudokode ser dataleverandørens formspråk slik ut:

"Å gå inn i en synkronisert visningsblokk

Mens data IKKE er nødvendig

Gå til ventekøen

Ellers produsere data

Etter at de klare dataene vises, ring Pulse-PulseAll.

for å flytte en eller flere tråder fra blokkeringskøen til kjørekøen. Forlat den synkroniserte blokken (og gå tilbake til kjørekøen)

La oss si at programmet vårt modellerer en familie med en forelder som tjener penger og et barn som bruker pengene. Når pengene er borteer mottatt, må barnet vente på at det nye beløpet kommer. Programvareimplementeringen av denne modellen ser slik ut:

1 Alternativ Strengt På

2 Importer System.Threading

3 modulmodul

4 Sub Main()

5 Dim theFamily As New Family()

6 theFamily.StartltsLife()

7 End Sub

8 Ende fjodule

9

10 offentlig klasse familie

11 Private mMoney Som heltall

12 Privat mUke som heltall = 1

13 Public Sub StartltsLife()

14 Dim aThreadStart As New ThreadStarUAddressOf Me.Produce)

15 Dim bThreadStart As New ThreadStarUAddressOf Me.Consume)

16 Demp en tråd som ny tråd (en trådstart)

17 Dim bThread As New Thread(bThreadStart)

18 aThread.Name = "Produser"

19 aThread.Start()

20 bThread.Name = "Forbruk"

21 bTråd. Start()

22 End Sub

23 Offentlig eiendom TheWeek() Som heltall

24 Få

25 Returmåned

26 Slutt Get

27 Set(ByVal Value As Integer)

28 muke - Verdi

29 Sluttsett

30 Slutt eiendom

31 Offentlig eiendom OurMoney() Som heltall

32 Få

33 Returner mMoney

34 Slutt Get

35 Set(ByVal Value As Integer)

36 mPenger =Verdi

37 Sluttsett

38 Slutt eiendom

39 Offentlig underproduksjon()

40 tråder. Søvn(500)

41 Gjør

42 Monitor.Enter(Meg)

43 Do While Me.OurMoney > 0

44 Monitor.Wait(Me)

45 Løkke

46 Me.OurMoney =1000

47 Monitor.PulseAll(Me)

48 Monitor.Exit(Me)

49 Sløyfe

50 End Sub

51 Offentlig underforbruk()

52 MsgBox("Er i forbrukstråd")

53 Gjør

54 Monitor.Enter(Meg)

55 Do While Me.OurMoney = 0

56 Monitor.Wait(Me)

57 Sløyfe

58 Console.WriteLine("Kjære forelder, jeg brukte akkurat alle dine " & _

penger i uken "&TheWeek)

59 Uken += 1

60 Hvis TheWeek = 21 *52 Da System.Environment.Exit(0)

61 Me.OurMoney =0

62 Monitor.PulseAll(Me)

63 Monitor.Exit(Me)

64 Sløyfe

65 End Sub

66 Sluttklasse

StartltsLife-metoden (linje 13-22) forbereder lanseringen av Produce og Consume-trådene. De viktigste tingene skjer i strømmene Produce (linje 39-50) og Consume (linje 51-65). Sub Produce-prosedyren sjekker tilgjengeligheten av penger, og hvis det er penger, går de til ventekøen. Ellers genererer forelderen penger (linje 46) og gir beskjed til objektene i ventekøen om at situasjonen har endret seg. Merk at Pulse-Pulse All-anropet bare trer i kraft når låsen frigjøres med Monitor.Exit-kommandoen. Omvendt sjekker Sub Consume-prosedyren for tilgjengeligheten av penger, og hvis det ikke er penger, varsler den den ventende forelderen. Linje 60 avslutter ganske enkelt programmet etter 21 betingede år; ring System. Environment.Exit(0) er .NET-ekvivalenten til End-kommandoen (End-kommandoen støttes også, men i motsetning til System. Environment. Exit returnerer den ikke en exit-kode til operativsystemet).

Tråder plassert i ventekøen må frigis av andre deler av programmet ditt. Dette er grunnen til at vi foretrekker å bruke PulseAll i stedet for Pulse. Siden det ikke er kjent på forhånd hvilken tråd som aktiveres når Pulse 1 kalles opp, kan man like gjerne ringe PulseAll hvis antallet tråder i køen er relativt lite.

Multithreading i grafikkprogrammer

Vår diskusjon om multithreading i GUI-applikasjoner vil begynne med et eksempel som forklarer hvorfor multithreading er nødvendig i GUI-applikasjoner. Lag et skjema med to knapper Start (btnStart) og Cancel (btnCancel), som vist i fig. 10.9. Ved å klikke på Start-knappen opprettes en klasse som inneholder en tilfeldig streng på 10 millioner tegn og en metode for å telle forekomster av bokstaven "E" i den lange strengen. Legg merke til bruken av StringBuilder-klassen, som gjør det mer effektivt å lage lange strenger.

Trinn 1

Tråd 1 merker at det ikke finnes data for det. Den ringer Vent, slipper låsen og går til ventekøen



Steg 2

Når låsen frigjøres, går tråd 2 eller tråd 3 ut av låsekøen og går inn i den synkroniserte blokken og henter låsen

Trinn 3

La oss si at tråd 3 går inn i en synkronisert blokk, lager data og kaller Pulse-Pulse All.

Umiddelbart etter at den går ut av blokken og frigjør låsen, flyttes tråd 1 til kjørekøen. Hvis tråd 3 kaller Pluse, går kun én til kjørekøentråd, når Pluse All kalles, går alle tråder til kjørekøen.



Ris. 10.8. Leverandør/forbrukerproblemet

Ris. 10.9. Multithreading i en enkel GUI-applikasjon

Importerer System.Text

Offentlig klasse tilfeldige tegn

Private m_Data Som StringBuilder

Privat mjength, m_count Som heltall

Offentlig Sub New (ByVal n Som heltall)

m_Length = n -1

m_Data = New StringBuilder(m_length) MakeString()

End Sub

Private Sub MakeString()

Dim i As Integer

Dim myRnd As New Random()

For i = 0 Til m_lengde

"Generer et tilfeldig tall fra 65 til 90,

" konverter det til stor bokstav

" og fest til StringBuilder-objektet

m_Data.Append(Chr(myRnd.Next(65.90)))

Neste

End Sub

Offentlig understarttelling()

GetEes()

End Sub

Private Sub GetEes()

Dim i As Integer

For i = 0 Til m_lengde

Hvis m_Data.Chars(i) = CChar("E") Da

m_count += 1

Slutt hvis neste

m_CountDone = Sant

End Sub

Offentlig skrivebeskyttet

Eiendom GetCount() Som heltall Get

Hvis ikke (m_CountDone) Da

Returner m_count

Slutt om

End Get End Property

Offentlig skrivebeskyttet

Property IsDone()As Boolean Get

Komme tilbake

m_CountDone

Slutt Get

Avslutt eiendom

Slutt klasse

Det er veldig enkel kode knyttet til to knapper på skjemaet. btn-Start_Click-prosedyren instansierer RandomCharacters-klassen ovenfor, og innkapsler en streng med 10 millioner tegn:

Private Sub btnStart_Click(ByVal sender Som System.Object.

ByVal e As System.EventArgs) Håndterer btnSTart.Click

Dim RC som nye tilfeldige tegn(10000000)

RC.StartCount()

MsgBox("Antallet e er " & RC.GetCount)

End Sub

Avbryt-knappen viser en meldingsboks:

Private Sub btnCancel_Click(ByVal sender Som System.Object._

ByVal e As System.EventArgs) Håndterer btnCancel.Click

MsgBox("Teller avbrutt!")

End Sub

Når du starter programmet og klikker på Start-knappen, viser det seg at Avbryt-knappen ikke reagerer på brukerhandlinger, siden den kontinuerlige sløyfen ikke lar knappen behandle den mottatte hendelsen. Dette er uakseptabelt i moderne programmer!

Det er to mulige løsninger. Det første alternativet, velkjent fra tidligere versjoner av VB, unngår multithreading: DoEvents-kallet er inkludert i loopen. I .NET ser denne kommandoen slik ut:

Application.DoEvents()

I vårt eksempel er dette definitivt ikke ønskelig - hvem vil bremse programmet med ti millioner DoEvents-anrop! Hvis du i stedet deler løkken i en egen tråd, vil operativsystemet bytte mellom tråder og Avbryt-knappen vil fortsatt fungere. En implementering med en egen tråd er vist nedenfor. For å tydelig vise at Avbryt-knappen fungerer, avslutter vi programmet når vi trykker på den.

Neste trinn: Vis telleknapp

La oss si at du bestemmer deg for å være kreativ og gi formen utseendet vist i fig. 10.9. Vennligst merk: Vis antall-knappen er ikke tilgjengelig ennå.

Ris. 10.10. Skjema med blokkert knapp

En egen tråd er ment å gjøre tellingen og låse opp den utilgjengelige knappen. Selvfølgelig kan det gjøres; Dessuten oppstår en slik oppgave ganske ofte. Dessverre vil du ikke være i stand til å gjøre det mest åpenbare - koble en sekundær tråd til GUI-tråden ved å lagre en referanse til ShowCount-knappen i konstruktøren, eller til og med bruke en standard delegat. Med andre ord, aldri ikke bruk alternativet nedenfor (grunnleggende feilaktig linjene er i fet skrift).

Offentlig klasse tilfeldige tegn

Privat m_0ata Som StringBuilder

Privat m_CountDone As Boolean

Privat mjength. m_count Som heltall

Private m_Button Som Windows.Forms.Button

Public Sub New(ByVa1 n Som heltall,_

ByVal b As Windows.Forms.Button)

m_lengde = n - 1

m_Data = New StringBuilder(mJength)

m_Button = b MakeString()

End Sub

Private Sub MakeString()

Dim I som heltall

Dim myRnd As New Random()

For I = 0 Til m_lengde

m_Data.Append(Chr(myRnd.Next(65. 90)))

Neste

End Sub

Offentlig understarttelling()

GetEes()

End Sub

Private Sub GetEes()

Dim I som heltall

For I = 0 Til mjength

Hvis m_Data.Chars(I) = CChar("E") Da

m_count += 1

Slutt hvis neste

m_CountDone =Sant

m_Button.Enabled=Sant

End Sub

Offentlig skrivebeskyttet

Property GetCount()As Integer


Hvis ikke (m_CountDone) Da

Kast nytt unntak ("Tell ennå ikke utført") Else

Returner m_count

Slutt om

Slutt Get

Avslutt eiendom

Offentlig skrivebeskyttet eiendom IsDone() Som boolsk


Returner m_CountDone

Slutt Get

Avslutt eiendom

Slutt klasse

Det er sannsynlig at denne koden vil fungere i noen tilfeller. Likevel:

  • Den sekundære tråden kan ikke kommunisere med tråden som oppretter GUI åpenbart midler.
  • Aldri Ikke endre elementer i grafikkprogrammer fra andre programtråder. Alle endringer skal bare skje i tråden som opprettet GUI.

Hvis du bryter disse reglene, vil vi vi garanterer at dine flertrådede grafikkprogrammer vil introdusere subtile, subtile feil.

Det vil heller ikke være mulig å organisere samspillet mellom objekter ved hjelp av hendelser. 06 - Hendelsesarbeideren kjører på den samme tråden som RaiseEvent-kallet skjedde i, så hendelser vil ikke hjelpe deg.

Likevel tilsier sunn fornuft at grafikkapplikasjoner bør ha et middel til å modifisere elementer fra en annen tråd. .NET Framework gir en trådsikker måte å kalle GUI-applikasjonsmetoder fra en annen tråd. En spesiell type delegat, Method Invoker, fra System.Windows-navneområdet, brukes til dette formålet. Skjemaer. Følgende utdrag viser den nye versjonen av GetEes-metoden (endrede linjer er med fet skrift):

Private Sub GetEes()

Dim I som heltall

For I = 0 Til m_lengde

Hvis m_Data.Chars(I) = CChar("E") Da

m_count += 1

Slutt hvis neste

m_CountDone = Sann prøve

Dim mylnvoker som ny metodelnvoker(AddressOf UpdateDate Button)

myInvoker.Invoke() Catch e As ThreadlnterruptedException

"Feil

Avslutt Prøv

End Sub

Offentlig Sub UpDateButton()

m_Button.Enabled =Sant

End Sub

Inter-threaded anrop til knappen gjøres ikke direkte, men gjennom Method Invoker. .NET Framework garanterer at dette alternativet er trådsikkert.

Hvorfor er det så mange problemer med flertrådsprogrammering?

Nå som du har en viss forståelse av flertrådsprogrammering og de potensielle problemene forbundet med det, bestemte vi oss for at det ville være hensiktsmessig å svare på spørsmålet som stilles i underseksjonstittelen på slutten av dette kapittelet.

En av grunnene er at multithreading er en ikke-lineær prosess, og vi er vant til en lineær programmeringsmodell. Til å begynne med er det vanskelig å venne seg til selve ideen om at programkjøring kan avbrytes tilfeldig, og kontrollen vil bli overført til annen kode.

Imidlertid er det en annen, mer grunnleggende grunn: programmerere i disse dager programmerer altfor sjelden på assemblerspråk eller ser til og med på den demonterte utgangen til kompilatoren. Ellers ville det være mye lettere for dem å venne seg til ideen om at én kommando på et høynivåspråk (som VB .NET) kan svare til dusinvis av monteringsinstruksjoner. Tråden kan avbrytes etter hvilken som helst av disse instruksjonene, og derfor midt i en instruksjon på høyt nivå.

Men det er ikke alt: moderne kompilatorer optimerer programytelsen, og maskinvare kan forstyrre minnebehandlingsprosessen. Som et resultat kan kompilatoren eller maskinvaren endre kommandorekkefølgen spesifisert i programmets kildekode uten din viten [ Mange kompilatorer optimerer sykliske array-kopieringsoperasjoner av formen for i=0 til n:b(i)=a(i):ncxt. Kompilatoren (eller til og med en spesialisert minnebehandling) kan ganske enkelt lage matrisen og deretter fylle den med en enkelt kopioperasjon, i stedet for å kopiere individuelle elementer flere ganger!].

Vi håper disse forklaringene vil hjelpe deg bedre å forstå hvorfor flertrådsprogrammering forårsaker så mange problemer - eller i det minste bli mindre overrasket over den merkelige oppførselen til de flertrådede programmene dine!

Tidligere innlegg snakket om multithreading i Windows ved bruk av CreateThread og andre WinAPI, samt multithreading i Linux og andre *nix-systemer som bruker pthreads. Hvis du skriver i C++11 eller senere, har du tilgang til std::thread og andre threading-primitiver introdusert i den språkstandarden. Deretter vil vi vise deg hvordan du jobber med dem. I motsetning til WinAPI og pthreads, er kode skrevet i std::thread på tvers av plattformer.

Merk: Koden ovenfor ble testet på GCC 7.1 og Clang 4.0 under Arch Linux, GCC 5.4 og Clang 3.8 under Ubuntu 16.04 LTS, GCC 5.4 og Clang 3.8 under FreeBSD 11, samt Visual Studio Community 2017 under Windows 10. CMake før versjon 3.8 kan ikke la kompilatoren bruke C++17-standarden spesifisert i prosjektegenskapene. Hvordan installere CMake 3.8 på Ubuntu 16.04. For at kode skal kompileres med Clang, må libc++-pakken være installert på *nix-systemer. For Arch Linux er pakken tilgjengelig på AUR. Ubuntu har libc++-dev-pakken, men du kan støte på et problem som hindrer koden i å bygges enkelt. Workround er beskrevet på StackOverflow. På FreeBSD, for å kompilere prosjektet, må du installere cmake-modules-pakken.

Mutexes

Nedenfor er et enkelt eksempel på bruk av tråder og mutexes:

#inkludere
#inkludere
#inkludere
#inkludere

Std::mutex mtx;
statisk int teller = 0 ;


for (;; ) (
{
std::lock_guard< std:: mutex >lås (mtx) ;

gå i stykker ;
int ctr_val = ++ teller;
std::cout<< "Thread " << tnum << ": counter = " <<
ctr_val<< std:: endl ;
}

}
}

int main() (
std::vektor< std:: thread >tråder;
for (int i = 0 ; i< 10 ; i++ ) {


}

// kan ikke bruke const auto& her siden .join() ikke er merket med const

thr.join();
}

Std::cout<< "Done!" << std:: endl ;
returner 0 ;
}

Legg merke til innpakningen av std::mutex i std::lock_guard i samsvar med RAII-idiomet. Denne tilnærmingen sikrer at mutex vil bli frigitt når du går ut av scopet i alle fall, inkludert når unntak oppstår. For å fange opp flere mutexes på en gang for å forhindre vranglås, er det std::scoped_lock-klassen. Imidlertid dukket det bare opp i C++17 og fungerer derfor kanskje ikke overalt. For tidligere versjoner av C++ er det en mal std::lock som er lik i funksjonalitet, selv om den krever å skrive tilleggskode for å frigjøre låser korrekt ved bruk av RAII.

RWLock

Det oppstår ofte en situasjon hvor man får tilgang til et objekt oftere ved å lese enn ved å skrive. I dette tilfellet, i stedet for en vanlig mutex, er det mer effektivt å bruke en lese-skrivelås, også kjent som RWLock. RWLock kan holdes av flere lesetråder samtidig, eller av bare én skrivetråd. RWLock i C++ tilsvarer klassene std::shared_mutex og std::shared_timed_mutex:

#inkludere
#inkludere
#inkludere
#inkludere

// std::shared_mutex mtx; // vil ikke fungere med GCC 5.4
std::shared_timed_mutex mtx;

statisk int teller = 0 ;
statisk konst int MAX_COUNTER_VAL = 100 ;

void thread_proc(int tnum) (
for (;; ) (
{
// se også std::shared_lock
std::unique_lock< std:: shared_timed_mutex >lås (mtx) ;
if (teller == MAX_COUNTER_VAL)
gå i stykker ;
int ctr_val = ++ teller;
std::cout<< "Thread " << tnum << ": counter = " <<
ctr_val<< std:: endl ;
}
std::thread::sleep_for(std::chrono::milliseconds(10));
}
}

int main() (
std::vektor< std:: thread >tråder;
for (int i = 0 ; i< 10 ; i++ ) {
std:: tråd thr(thread_proc, i) ;
threads.emplace_back(std::move(thr));
}

for (auto & thr : tråder) (
thr.join();
}

Std::cout<< "Done!" << std:: endl ;
returner 0 ;
}

I analogi med std::lock_guard brukes klassene std::unique_lock og std::shared_lock for å fange RWLock, avhengig av hvordan vi ønsker å fange låsen. Klassen std::shared_timed_mutex dukket opp i C++14 og fungerer på alle* moderne plattformer (for ikke å snakke om mobile enheter, spillkonsoller og så videre). I motsetning til std::shared_mutex, har den metoder try_lock_for, try_lock_unti og andre som prøver å låse mutexen innen en gitt tid. Jeg mistenker sterkt at std::shared_mutex må være billigere enn std::shared_timed_mutex. Imidlertid dukket std::shared_mutex bare opp i C++17, noe som betyr at det ikke støttes overalt. Spesielt den fortsatt mye brukte GCC 5.4 vet ikke om det.

Tråd lokal lagring

Noen ganger må du lage en variabel, som en global, men som bare én tråd kan se. Andre tråder ser også variabelen, men for dem har den sin egen lokale betydning. For dette kom de opp med Thread Local Storage, eller TLS (har ingenting med Transport Layer Security å gjøre!). TLS kan blant annet brukes til å fremskynde genereringen av pseudorandomtall betydelig. Eksempel på bruk av TLS i C++:

#inkludere
#inkludere
#inkludere
#inkludere

Std::mutex io_mtx;
thread_local int teller = 0 ;
statisk konstant int MAX_COUNTER_VAL = 10 ;

void thread_proc(int tnum) (
for (;; ) (
teller++ ;
if (teller == MAX_COUNTER_VAL)
gå i stykker ;
{
std::lock_guard< std:: mutex >lås(io_mtx) ;
std::cout<< "Thread " << tnum << ": counter = " <<
disk<< std:: endl ;
}
std::thread::sleep_for(std::chrono::milliseconds(10));
}
}

int main() (
std::vektor< std:: thread >tråder;
for (int i = 0 ; i< 10 ; i++ ) {
std:: tråd thr(thread_proc, i) ;
threads.emplace_back(std::move(thr));
}

for (auto & thr : tråder) (
thr.join();
}

Std::cout<< "Done!" << std:: endl ;
returner 0 ;
}

Mutexen her brukes kun til å synkronisere utdata til konsollen. Ingen synkronisering er nødvendig for å få tilgang til thread_local-variabler.

Atomvariabler

Atomvariabler brukes ofte til å utføre enkle operasjoner uten bruk av mutexes. For eksempel må du øke en teller fra flere tråder. I stedet for å pakke inn int i std::mutex, er det mer effektivt å bruke std::atomic_int. C++ tilbyr også typene std::atomic_char, std::atomic_bool og mange andre. Låsfrie algoritmer og datastrukturer implementeres også ved hjelp av atomvariabler. Det er verdt å merke seg at de er svært vanskelige å utvikle og feilsøke, og fungerer ikke raskere enn tilsvarende algoritmer og datastrukturer med låser på alle systemer.

Eksempelkode:

#inkludere
#inkludere
#inkludere
#inkludere
#inkludere

statisk std:: atomic_int atomic_counter(0) ;
statisk konst int MAX_COUNTER_VAL = 100 ;

Std::mutex io_mtx;

void thread_proc(int tnum) (
for (;; ) (
{
int ctr_val = ++ atomteller;
if (ctr_val >= MAX_COUNTER_VAL)
gå i stykker ;

{
std::lock_guard< std:: mutex >lås(io_mtx) ;
std::cout<< "Thread " << tnum << ": counter = " <<
ctr_val<< std:: endl ;
}
}
std::thread::sleep_for(std::chrono::milliseconds(10));
}
}

int main() (
std::vektor< std:: thread >tråder;

int nthreads = std::thread::hardware_concurrency();
if (nthreads == 0 ) nthreads = 2 ;

for (int i = 0 ; i< nthreads; i++ ) {
std:: tråd thr(thread_proc, i) ;
threads.emplace_back(std::move(thr));
}

for (auto & thr : tråder) (
thr.join();
}

Std::cout<< "Done!" << std:: endl ;
returner 0 ;
}

Legg merke til bruken av hardware_concurrency-prosedyren. Den returnerer et estimat på antall tråder som kan kjøres parallelt på det gjeldende systemet. For eksempel, på en maskin med en firekjerners prosessor som støtter hyperthreading, returnerer prosedyren tallet 8. Prosedyren kan også returnere null hvis evalueringen ikke kunne gjøres eller prosedyren rett og slett ikke er implementert.

Litt informasjon om driften av atomvariabler på assemblernivå finner du i artikkelen Cheat Sheet for Basic x86/x64 Assembly Instructions.

Konklusjon

Så vidt jeg kan se fungerer dette veldig bra. Det vil si at når du skriver applikasjoner på tvers av plattformer i C++, kan du trygt glemme WinAPI og pthreads. I ren C, siden C11, er det også tråder på tvers av plattformer. Men de støttes fortsatt ikke av Visual Studio (jeg sjekket), og vil neppe noen gang bli støttet. Det er ingen hemmelighet at Microsoft ikke ser noen interesse for å utvikle støtte for C-språket i kompilatoren, og foretrekker å fokusere på C++.

Det er fortsatt mange primitiver igjen bak kulissene: std::condition_variable(_any), std::(shared_)future, std::promise, std::sync og andre. Jeg anbefaler cppreference.com å sjekke dem ut. Det kan også være verdt å lese boken C++ Concurrency in Action. Men jeg må advare deg om at den ikke lenger er ny, inneholder mye vann og i hovedsak gjenforteller et dusin artikler fra cppreference.com.

Den fullstendige versjonen av kildekoden for dette notatet er som vanlig på GitHub. Hvordan skriver du for øyeblikket flertrådede applikasjoner i C++?

slutten av filen. Dermed blir loggoppføringer utført av forskjellige prosesser aldri blandet. Mer moderne Unix-systemer tilbyr en spesiell syslog(3C)-tjeneste for logging.

Fordeler:

  1. Enkel utvikling. Faktisk kjører vi mange kopier av en enkelt trådet applikasjon, og de kjører uavhengig av hverandre. Du trenger ikke å bruke noen spesifikt multi-threaded APIer og kommunikasjonsverktøy mellom prosesser.
  2. Høy pålitelighet. Unormal avslutning av noen prosess påvirker ikke andre prosesser på noen måte.
  3. God toleranse. Applikasjonen vil fungere på alle multitasking OS
  4. Høy sikkerhet. Ulike søknadsprosesser kan kjøres som forskjellige brukere. På denne måten er det mulig å implementere prinsippet om minste privilegium, når hver prosess kun har de rettighetene den trenger for å fungere. Selv om en feil oppdages i en av prosessene som tillater ekstern kjøring av kode, vil angriperen kun kunne få tilgangsnivået som denne prosessen ble utført med.

Feil:

  1. Ikke alle anvendte oppgaver kan leveres på denne måten. For eksempel er denne arkitekturen egnet for en server som betjener statiske HTML-sider, men er fullstendig uegnet for en databaseserver og mange applikasjonsservere.
  2. Å lage og ødelegge prosesser er en kostbar operasjon, så denne arkitekturen er ikke optimal for mange oppgaver.

Unix-systemer tar en hel rekke tiltak for å gjøre det så billig som mulig å lage en prosess og kjøre et nytt program i en prosess. Du må imidlertid forstå at det å lage en tråd i en eksisterende prosess alltid vil være billigere enn å lage en ny prosess.

Eksempler: apache 1.x (HTTP-server)

Multiprosessapplikasjoner som kommuniserer via System V IPC-sockets, rør og meldingskøer

De listede IPC-verktøyene (Interprocess communication) tilhører de såkalte harmoniske interprosesskommunikasjonsverktøyene. De lar deg organisere samspillet mellom prosesser og tråder uten å bruke delt minne. Programmeringsteoretikere er veldig glad i denne arkitekturen fordi den praktisk talt eliminerer mange typer konkurransefeil.

Fordeler:

  1. Relativ enkel utvikling.
  2. Høy pålitelighet. Unormal avslutning av en av prosessene fører til at røret eller stikkontakten lukkes, og ved meldingskøer, til at meldinger ikke lenger kommer eller hentes fra køen. Resten av applikasjonens prosesser kan enkelt oppdage denne feilen og gjenopprette fra den, muligens (men ikke nødvendigvis) ganske enkelt ved å starte den mislykkede prosessen på nytt.
  3. Mange slike applikasjoner (spesielt de som er basert på sockets) kan enkelt redesignes for å kjøre i et distribuert miljø, der ulike komponenter i applikasjonen kjører på forskjellige maskiner.
  4. God toleranse. Applikasjonen vil kjøre på de fleste multitasking-operativsystemer, inkludert eldre Unix-systemer.
  5. Høy sikkerhet. Ulike søknadsprosesser kan kjøres som forskjellige brukere. På denne måten er det mulig å implementere prinsippet om minste privilegium, når hver prosess kun har de rettighetene den trenger for å fungere.

Selv om en feil oppdages i en av prosessene som tillater ekstern kjøring av kode, vil angriperen kun kunne få tilgangsnivået som denne prosessen ble utført med.

Feil:

  1. En slik arkitektur er ikke lett å utvikle og implementere for alle applikasjonsproblemer.
  2. Alle de listede typene IPC-verktøy krever seriell dataoverføring. Hvis tilfeldig tilgang til delte data er nødvendig, er denne arkitekturen upraktisk.
  3. Overføring av data gjennom en pipe, socket og meldingskø krever utføring av systemanrop og kopiering av data to ganger – først fra kildeprosessens adresserom til kjernens adresserom, deretter fra kjernens adresserom til minnet målprosess. Dette er kostbare operasjoner. Ved overføring av store datamengder kan dette bli et alvorlig problem.
  4. De fleste systemer har begrensninger på det totale antallet rør, stikkontakter og IPC-anlegg. Så, i Solaris, som standard, er ikke mer enn 1024 åpne rør, stikkontakter og filer tillatt per prosess (dette er på grunn av begrensningene i valg av systemkall). Solaris arkitektoniske grense er 65536 rør, stikkontakter og filer per prosess.

    Grensen på det totale antallet TCP/IP-sockets er ikke mer enn 65536 per nettverksgrensesnitt (på grunn av TCP-headerformatet). System V IPC meldingskøer er plassert i kjernens adresserom, så det er strenge begrensninger på antall køer i systemet og på volumet og antall meldinger som kan settes i kø samtidig.

  5. Å lage og ødelegge en prosess, og bytte mellom prosesser er kostbare operasjoner. Denne arkitekturen er ikke optimal i alle tilfeller.

Multiprosessapplikasjoner som kommuniserer via delt minne

Det delte minnet kan være System V IPC-delt minne og fil-til-minne-kartlegging. For å synkronisere tilgang, kan du bruke System V IPC semaforer, mutexes og POSIX semaforer, og når du tilordner filer til minnet, fange opp deler av filen.

Fordeler:

  1. Effektiv tilfeldig tilgang til delte data. Denne arkitekturen er egnet for implementering av databaseservere.
  2. Høy toleranse. Kan porteres til et hvilket som helst operativsystem som støtter eller emulerer System V IPC.
  3. Relativt høy sikkerhet. Ulike søknadsprosesser kan kjøres på vegne av forskjellige brukere. På denne måten er det mulig å implementere prinsippet om minste privilegium, når hver prosess kun har de rettighetene den trenger for å fungere. Separasjonen av tilgangsnivåer er imidlertid ikke så streng som i de tidligere omtalte arkitekturene.

Feil:

  1. Relativ kompleksitet av utvikling. Feil i tilgangssynkronisering – såkalte rasefeil – er svært vanskelig å oppdage under testing.

    Dette kan resultere i en 3 til 5 ganger høyere total utviklingskostnad sammenlignet med entrådede eller enklere multi-tasking-arkitekturer.

  2. Lav pålitelighet. Avbrutt avslutning av noen av søknadsprosessene kan (og gjør det ofte) etterlate delt minne i en inkonsekvent tilstand.

    Dette fører ofte til at andre programoppgaver krasjer. Noen applikasjoner, som Lotus Domino, dreper spesifikt serveromfattende prosesser hvis noen av dem avsluttes unormalt.

  3. Å lage og ødelegge en prosess og bytte mellom dem er kostbare operasjoner.

    Derfor er ikke denne arkitekturen optimal for alle applikasjoner.

  4. Under visse omstendigheter kan bruk av delt minne føre til rettighetseskalering. Hvis det blir funnet en feil i en av prosessene som fører til ekstern kjøring av kode, er det stor sannsynlighet for at en angriper vil kunne bruke den til å eksternt kjøre kode i andre applikasjonsprosesser.

    Det vil si at i verste fall kan en angriper få et tilgangsnivå som tilsvarer det høyeste tilgangsnivået til applikasjonsprosessene.

  5. Applikasjoner som bruker delt minne må kjøre på samme fysiske datamaskin, eller i det minste på maskiner som har delt RAM. I virkeligheten kan denne begrensningen omgås, for eksempel ved å bruke minnetilordnede delte filer, men dette introduserer betydelig overhead

Faktisk kombinerer denne arkitekturen ulempene med multi-prosess og multi-threaded applikasjoner. Imidlertid bruker en rekke populære applikasjoner utviklet på 80- og tidlig 90-tallet, før Unix sine multithreading API-er ble standardisert, denne arkitekturen. Dette er mange databasetjenere, både kommersielle (Oracle, DB2, Lotus Domino), fritt distribuerte, moderne versjoner av Sendmail og noen andre e-posttjenere.

Egentlig flertrådede applikasjoner

Tråder eller applikasjonstråder kjøres innenfor samme prosess. Hele adresseområdet til en prosess deles mellom tråder. Ved første øyekast ser det ut til at dette lar deg organisere interaksjon mellom tråder uten noen spesielle API-er i det hele tatt. I virkeligheten er dette ikke sant - hvis flere tråder jobber med en delt datastruktur eller systemressurs, og minst én av trådene endrer denne strukturen, vil dataene på noen tidspunkter være inkonsekvente.

Derfor må tråder bruke spesielle midler for å kommunisere. De viktigste funksjonene er primitiver for gjensidig ekskludering (mutexes og lese-skrive-låser). Ved å bruke disse primitivene kan programmereren sikre at ingen tråd får tilgang til delte ressurser mens de er i en inkonsekvent tilstand (dette kalles gjensidig ekskludering). System V IPC, bare de strukturene som ligger i det delte minnesegmentet deles. Vanlige variabler og dynamiske datastrukturer tildelt på vanlig måte er unike for hver prosess). Feil i tilgang til delte data – rasefeil – er svært vanskelig å oppdage i testing.

  • De høye kostnadene ved å utvikle og feilsøke applikasjoner, på grunn av punkt 1.
  • Lav pålitelighet. Ødeleggelsen av datastrukturer, for eksempel på grunn av bufferoverløp eller pekerfeil, påvirker alle tråder i prosessen og fører vanligvis til en unormal avslutning av hele prosessen. Andre fatale feil, som deling med null i en av trådene, fører også vanligvis til at alle tråder i prosessen krasjer.
  • Lav sikkerhet. Alle applikasjonstråder kjøres i samme prosess, det vil si under samme brukernavn og med samme tilgangsrettigheter. Det er umulig å implementere prinsippet om minste privilegium; prosessen må kjøres som en bruker som kan utføre alle operasjoner som kreves av alle tråder i applikasjonen.
  • Å lage en tråd er fortsatt en ganske dyr operasjon. Hver tråd tildeles nødvendigvis sin egen stabel, som som standard tar opp 1 megabyte RAM på 32-bits arkitekturer og 2 megabyte på 64-bits arkitekturer, og noen andre ressurser. Derfor er ikke denne arkitekturen optimal for alle applikasjoner.
  • Manglende evne til å kjøre applikasjonen på et datasystem med flere maskiner. Teknikkene nevnt i forrige avsnitt, som minnekartlegging av delte filer, er ikke aktuelt for et flertråds program.
  • Generelt kan det sies at flertrådede applikasjoner har nesten de samme fordelene og ulempene som flerprosessapplikasjoner som bruker delt minne.

    Kostnaden for å kjøre en flertrådsapplikasjon er imidlertid lavere, og å utvikle en slik applikasjon er på noen måter enklere enn en delt minneapplikasjon. Derfor har flertrådsapplikasjoner de siste årene blitt mer og mer populære.

    Clay Breshears

    Introduksjon

    Intels multithreading-implementeringsmetoder inkluderer fire hovedtrinn: analyse, design og implementering, feilsøking og ytelsesjustering. Dette er tilnærmingen som brukes til å lage en flertrådsapplikasjon fra sekvensiell kode. Arbeid med programvare under implementeringen av første, tredje og fjerde trinn dekkes ganske bredt, mens informasjon om implementeringen av det andre trinnet tydeligvis ikke er nok.

    Det er utgitt mange bøker om parallelle algoritmer og parallell databehandling. Imidlertid omhandler disse publikasjonene hovedsakelig meldingsoverføring, distribuerte minnesystemer eller teoretiske parallelle databehandlingsmodeller som noen ganger ikke er anvendelige på virkelige flerkjerneplattformer. Hvis du er klar til å gjøre alvor av flertrådsprogrammering, trenger du sannsynligvis kunnskap om utvikling av algoritmer for disse modellene. Selvfølgelig er bruken av disse modellene ganske begrenset, så mange programvareutviklere kan fortsatt måtte implementere dem i praksis.

    Uten å overdrive kan vi si at utviklingen av flertrådede applikasjoner først og fremst er en kreativ aktivitet, og først deretter en vitenskapelig aktivitet. I denne artikkelen lærer du åtte enkle regler som vil hjelpe deg å utvide basen av parallell programmeringspraksis og forbedre effektiviteten ved å implementere tråddatabehandling i applikasjonene dine.

    Regel 1. Fremhev operasjonene som utføres i programkoden uavhengig av hverandre

    Parallell prosessering gjelder bare for de sekvensielle kodeoperasjonene som utføres uavhengig av hverandre. Et godt eksempel på hvordan handlinger uavhengig av hverandre fører til et reelt enkeltresultat er byggingen av et hus. Det involverer arbeidere med mange spesialiteter: snekkere, elektrikere, gipsere, rørleggere, taktekkere, malere, murere, landskapsarkitekter, etc. Noen av dem kan selvfølgelig ikke begynne å jobbe før andre er ferdige (for eksempel vil ikke taktekkere begynne arbeidet før veggene er bygget, og malere vil ikke male disse veggene med mindre de er pusset). Men generelt kan vi si at alle som er involvert i bygg og anlegg handler uavhengig av hverandre.

    La oss vurdere et annet eksempel - arbeidssyklusen til en DVD-utleiebutikk, som mottar bestillinger for visse filmer. Bestillinger fordeles blant stasjonsarbeiderne, som søker etter disse filmene på lageret. Naturligvis, hvis en av arbeiderne tar en plate fra et lager hvor en film med Audrey Hepburn er spilt inn, vil dette på ingen måte påvirke en annen arbeider som leter etter den neste actionfilmen med Arnold Schwarzenegger, og vil absolutt ikke påvirke deres kollega som er ser etter plater med ny sesong av Friends. I vårt eksempel antar vi at alle utsolgte problemer er løst før bestillinger kommer til utleiestedet, og at pakking og forsendelse av en bestilling ikke vil påvirke behandlingen av andre.

    I arbeidet ditt vil du sannsynligvis støte på beregninger som bare kan behandles i en bestemt sekvens, og ikke parallelt, siden de ulike iterasjonene eller trinnene i løkken avhenger av hverandre og må utføres i en streng rekkefølge. La oss ta et levende eksempel fra naturen. Se for deg en drektig hjort. Siden drektigheten i gjennomsnitt varer i åtte måneder, uansett hvordan du ser på det, vil ikke fawnen dukke opp på en måned, selv om åtte rådyr blir drektige samtidig. Men åtte reinsdyr samtidig ville gjort en god jobb hvis du spenner dem alle til julenissens slede.

    Regel 2: Bruk samtidighet på et lavt granularitetsnivå

    Det er to tilnærminger til parallell partisjonering av sekvensiell programkode: bottom-up og top-down. Først, på kodeanalysestadiet, identifiseres kodesegmenter (såkalte "hot spots"), som tar opp en betydelig del av programmets utførelsestid. Å separere disse kodesegmentene parallelt (hvis mulig) vil gi den største ytelsesgevinsten.

    Bottom-up-tilnærmingen implementerer flertrådsbehandling av kodehotspots. Hvis parallell partisjonering av de funnet punktene ikke er mulig, må du undersøke applikasjonens anropsstabel for å finne andre segmenter som er tilgjengelige for parallell partisjonering og har kjørt i lang tid. La oss si at du jobber med et program som komprimerer grafikk. Komprimering kan implementeres ved hjelp av flere uavhengige parallelle tråder som behandler individuelle bildesegmenter. Men selv om du klarer å implementere multi-threading hot spots, ikke forsøm analysen av anropsstakken, som et resultat av dette kan du finne segmenter som er tilgjengelige for parallell deling, plassert på et høyere nivå av programkoden. På denne måten kan du øke granulariteten til parallell prosessering.

    I top-down-tilnærmingen analyseres arbeidet med programkoden, og dens individuelle segmenter identifiseres, hvis utførelse fører til fullføring av hele oppgaven. Hvis hovedkodesegmenter ikke er klart uavhengige, analyser komponentdelene deres for å se etter uavhengige beregninger. Ved å analysere koden din kan du identifisere kodemodulene som tar mest CPU-tid å utføre. La oss se på implementeringen av tråding i et program designet for videokoding. Parallell prosessering kan implementeres på det laveste nivået - for uavhengige piksler i en ramme, eller på et høyere nivå - for grupper av rammer som kan behandles uavhengig av andre grupper. Hvis applikasjonen opprettes for å behandle flere videofiler samtidig, kan parallelldeling på dette nivået være enda enklere, og detaljene vil være på det laveste nivået.

    Parallell beregningsgranularitet refererer til mengden beregning som må utføres før synkronisering mellom tråder. Med andre ord, jo sjeldnere synkronisering forekommer, desto lavere detaljnivå. Gjenget databehandling med høy granularitet kan føre til at systemoverhead knyttet til organisering av tråder overskrider mengden nyttig beregning utført av disse trådene. Å øke antall tråder mens du opprettholder samme mengde beregning kompliserer prosesseringsprosessen. Multithreading med lav granularitet forårsaker mindre systemlatens og har større potensial for skalerbarhet, noe som kan oppnås ved å introdusere flere tråder. For å implementere parallell prosessering med lav granularitet, anbefales det å bruke en ovenfra-ned-tilnærming og organisere tråder på et høyt nivå av anropsstakken.

    Regel 3: Bygg inn skalerbarhet i koden din slik at ytelsen øker etter hvert som antall kjerner øker.

    For ikke så lenge siden, i tillegg til dual-core prosessorer, dukket det opp firekjerners prosessorer på markedet. Dessuten har Intel allerede annonsert etableringen av en prosessor med 80 kjerner, som er i stand til å utføre en billion flytepunktoperasjoner per sekund. Siden antall kjerner i prosessorer bare vil øke over tid, må koden din ha tilstrekkelig skalerbarhetspotensial. Skalerbarhet er en parameter som man kan bedømme en applikasjons evne til å reagere tilstrekkelig på endringer som en økning i systemressurser (antall kjerner, minnestørrelse, bussfrekvens, etc.) eller en økning i datavolum. Med tanke på at antall kjerner i fremtidige prosessorer vil øke, skriv skalerbar kode som vil øke ytelsen på grunn av økte systemressurser.

    For å omskrive en av C. Northecote Parkinsons lover, kan vi si at "databehandling opptar alle tilgjengelige systemressurser." Dette betyr at etter hvert som dataressurser (som antall kjerner) øker, vil alle sannsynligvis bli brukt til databehandling. La oss gå tilbake til videokomprimeringsapplikasjonen diskutert ovenfor. Utseendet til ytterligere prosessorkjerner vil neppe påvirke størrelsen på behandlede rammer - i stedet vil antallet tråder som behandler rammen øke, noe som vil føre til en reduksjon i antall piksler per tråd. Som et resultat, på grunn av organiseringen av ekstra tråder, vil mengden av overhead øke, og graden av parallellitet vil avta. Et annet mer sannsynlig scenario ville være en økning i størrelsen eller antallet videofiler som må kodes. I dette tilfellet vil organisering av flere tråder som vil behandle større (eller flere) videofiler tillate deg å dele hele mengden arbeid direkte på stadiet der økningen skjedde. På sin side vil en applikasjon med slike evner ha høyt potensial for skalerbarhet.

    Å designe og implementere parallell prosessering ved bruk av datadekomponering gir økt skalerbarhet sammenlignet med bruk av funksjonell dekomponering. Antall uavhengige funksjoner i programkoden er oftest begrenset og endres ikke under applikasjonskjøring. Siden hver uavhengig funksjon er tildelt en separat tråd (og følgelig en prosessorkjerne), vil ikke tilleggsorganiserte tråder føre til en ytelsesøkning med en økning i antall kjerner. Så, parallelle partisjoneringsmodeller med datanedbrytning vil gi økt potensial for applikasjonsskalerbarhet på grunn av det faktum at med en økning i antall prosessorkjerner vil volumet av behandlede data øke.

    Selv om programkoden organiserer gjenget behandling av uavhengige funksjoner, er det sannsynlig at ytterligere tråder kan brukes som startes når inngangsbelastningen øker. La oss gå tilbake til eksempelet på å bygge et hus diskutert ovenfor. Det unike målet med konstruksjon er å fullføre et begrenset antall selvstendige oppgaver. Men hvis du blir pålagt å bygge dobbelt så mange etasjer, vil du sannsynligvis ønske å ansette flere arbeidere innen enkelte spesialiteter (malere, taktekkere, rørleggere osv.). Derfor må du utvikle applikasjoner som kan tilpasse seg datanedbrytningen som oppstår som følge av økt arbeidsbelastning. Hvis koden implementerer funksjonell dekomponering, bør du vurdere å organisere flere tråder etter hvert som antallet prosessorkjerner øker.

    Regel 4: Bruk trådsikre biblioteker

    Hvis du kanskje trenger et bibliotek for å håndtere data i hot spots i koden din, bør du vurdere å bruke forhåndsbygde funksjoner i stedet for din egen kode. Kort sagt, ikke prøv å finne opp hjulet på nytt ved å utvikle kodesegmenter hvis funksjonalitet allerede er gitt i optimaliserte biblioteksprosedyrer. Mange biblioteker, inkludert Intel® Math Kernel Library (Intel® MKL) og Intel® Integrated Performance Primitives (Intel® IPP), inneholder allerede flertrådsfunksjoner optimalisert for flerkjerneprosessorer.

    Det er verdt å merke seg at når du bruker prosedyrer fra flertrådede biblioteker, må du sørge for at oppkalling av et bestemt bibliotek ikke vil påvirke den normale driften av trådene. Det vil si at hvis prosedyrekall gjøres fra to forskjellige tråder, må hvert anrop returnere de riktige resultatene. Hvis prosedyrer får tilgang til og oppdaterer delte bibliotekvariabler, kan det oppstå et "datakappløp", som vil ha en skadelig effekt på påliteligheten til beregningsresultatene. For å fungere korrekt med tråder, legges biblioteksprosedyren til som en ny (det vil si at den ikke oppdaterer noe annet enn lokale variabler) eller synkroniseres for å beskytte tilgang til delte ressurser. Konklusjon: før du bruker et tredjepartsbibliotek i programkoden din, les dokumentasjonen vedlagt det for å sikre at det fungerer riktig med tråder.

    Regel 5: Bruk en passende tremodell

    La oss si at funksjonene fra flertrådede biblioteker tydeligvis ikke er nok til å dele alle relevante kodesegmenter parallelt, og du måtte tenke på å organisere tråder. Ikke skynd deg å lage din egen (tunge) trådstruktur hvis OpenMP-biblioteket allerede inneholder all funksjonaliteten du trenger.

    Ulempen med eksplisitt multithreading er manglende evne til å kontrollere tråder nøyaktig.

    Hvis du bare trenger parallell separasjon av ressurskrevende løkker, eller den ekstra fleksibiliteten som eksplisitte tråder gir er av underordnet betydning for deg, så er det i dette tilfellet ingen vits i å gjøre ekstra arbeid. Jo mer kompleks implementeringen av multithreading, jo større er sannsynligheten for feil i koden og desto vanskeligere er den påfølgende modifikasjonen.

    OpenMP-biblioteket er fokusert på datanedbrytning og er spesielt godt egnet for gjenget behandling av løkker som jobber med store mengder informasjon. Til tross for det faktum at bare datadekomponering er aktuelt for noen applikasjoner, er det nødvendig å ta hensyn til tilleggskrav (for eksempel en arbeidsgiver eller kunde), ifølge hvilke bruken av OpenMP er uakseptabel og det gjenstår å implementere multithreading ved bruk av eksplisitte metoder . I dette tilfellet kan OpenMP brukes til å forhåndstråde for å estimere potensielle ytelsesgevinster, skalerbarhet og den estimerte innsatsen som kreves for senere å partisjonere koden ved hjelp av eksplisitt tråding.

    Regel 6. Resultatet av programkoden bør ikke avhenge av utførelsessekvensen til parallelle tråder

    For sekvensiell kode er det tilstrekkelig å bare definere et uttrykk som vil bli utført etter et hvilket som helst annet uttrykk. I flertrådskode er rekkefølgen for utførelse av tråder ikke definert og avhenger av instruksjonene fra operativsystemplanleggeren. Strengt tatt er det nesten umulig å forutsi rekkefølgen av tråder som vil bli lansert for å utføre en operasjon, eller å bestemme hvilken tråd som vil bli lansert av planleggeren på et senere tidspunkt. Prediksjon brukes først og fremst for å redusere applikasjonsforsinkelse, spesielt når du kjører på en plattform med en prosessor som har færre kjerner enn tråder. Hvis en tråd er blokkert fordi den trenger tilgang til et område som ikke er skrevet til hurtigbufferen eller fordi den må utføre en I/O-forespørsel, vil planleggeren suspendere den og starte en tråd klar til å kjøre.

    Et direkte resultat av usikkerhet i trådplanlegging er datarassituasjoner. Forutsatt at en tråd vil endre verdien til en delt variabel før en annen tråd leser den verdien kan være feil. Med hell vil rekkefølgen for utførelse av tråder for en spesifikk plattform forbli den samme på tvers av alle kjøringer av applikasjonen. Imidlertid kan små endringer i systemets tilstand (for eksempel plasseringen av data på harddisken, minnehastighet eller til og med et avvik fra den nominelle AC-frekvensen til strømforsyningen) utløse en annen rekkefølge for trådutførelse. For programkode som bare fungerer riktig med en viss sekvens av tråder, er det derfor sannsynlig at problemer knyttet til datarace-situasjoner og vranglåser.

    Fra et ytelsessynspunkt er det å foretrekke å ikke begrense rekkefølgen som tråder utføres i. En streng sekvens av trådutførelse er kun tillatt i tilfeller av ekstrem nødvendighet, bestemt av et forhåndsbestemt kriterium. Hvis slike omstendigheter oppstår, vil tråder bli lansert i den rekkefølgen som er spesifisert av de angitte synkroniseringsmekanismene. Tenk deg for eksempel to venner som leser en avis som er lagt ut på bordet. For det første kan de lese med ulik hastighet, og for det andre kan de lese forskjellige artikler. Og her spiller det ingen rolle hvem som leser avisen først - i alle fall må han vente på vennen før han snur siden. Samtidig er det ingen begrensninger på tid eller rekkefølge for å lese artikler - venner leser i hvilken som helst hastighet, og synkronisering mellom dem skjer umiddelbart når de snur siden.

    Regel 7: Bruk lokal strømlagring. Om nødvendig, tilordne låser til individuelle dataområder

    Synkronisering øker uunngåelig belastningen på systemet, noe som på ingen måte fremskynder prosessen med å oppnå resultatene av parallelle beregninger, men sikrer deres korrekthet. Ja, synkronisering er nødvendig, men det bør ikke misbrukes. For å minimere synkronisering, brukes lokal lagring av tråd eller tildelte minneområder (for eksempel array-elementer merket med identifikatorene til de tilsvarende trådene).

    Behovet for å dele midlertidige variabler mellom ulike tråder oppstår ganske sjelden. Slike variabler må deklareres eller allokeres lokalt til hver tråd. Variabler hvis verdier er mellomresultater av trådutførelse må også erklæres lokale for de tilsvarende trådene. Synkronisering vil være nødvendig for å oppsummere disse mellomresultatene i et vanlig minneområde. For å minimere mulig belastning på systemet, er det å foretrekke å oppdatere dette generelle området så sjelden som mulig. Eksplisitte flertrådsmetoder gir trådlokale lagrings-APIer som sikrer integriteten til lokale data fra starten av ett flertrådskodesegment til det neste (eller fra ett flertrådsfunksjonskall til neste kjøring av samme funksjon).

    Hvis det ikke er mulig å lagre tråder lokalt, synkroniseres tilgang til delte ressurser ved hjelp av ulike objekter, for eksempel låser. Det er viktig å tilordne låser riktig til spesifikke datablokker, noe som er enklest å gjøre hvis antall låser er lik antall datablokker. En enkelt låsemekanisme som synkroniserer tilgang til flere minneregioner brukes bare når alle disse områdene ligger i den samme kritiske delen av programkoden.

    Hva bør du gjøre hvis du trenger å synkronisere tilgang til en stor mengde data, for eksempel en matrise bestående av 10 000 elementer? Å organisere en enkelt lås for hele arrayet vil sannsynligvis skape en flaskehals i applikasjonen. Må du virkelig organisere låsing for hvert element separat? Deretter, selv om 32 eller 64 parallelle tråder får tilgang til dataene, må du forhindre tilgangskonflikter til et ganske stort minneområde, og sannsynligheten for at slike konflikter oppstår er 1 %. Heldigvis finnes det en slags gylden middelvei, de såkalte "modulo-låsene". Hvis N modulo-låser brukes, vil hver lås synkronisere tilgang til den N-te delen av det totale dataområdet. For eksempel, hvis to slike låser er organisert, vil en av dem forhindre tilgang til partall matriseelementer, og den andre vil forhindre tilgang til odde elementer. I dette tilfellet bestemmer tråder, som får tilgang til det nødvendige elementet, dets paritet og setter riktig lås. Antall modulo-låser velges under hensyntagen til antall tråder og sannsynligheten for samtidig tilgang av flere tråder til samme minneområde.

    Merk at samtidig bruk av flere låsemekanismer ikke er tillatt for å synkronisere tilgang til ett minneområde. La oss huske Segals lov: «En person som har én klokke, vet nøyaktig hva klokken er. En mann som har noen få klokker er ikke sikker på noe.» Anta at tilgangen til en variabel styres av to forskjellige låser. I dette tilfellet kan den første låsen brukes av ett kodesegment, og den andre av et annet segment. Da vil trådene som utfører disse segmentene finne seg i en rasesituasjon for de delte dataene de får tilgang til samtidig.

    Regel 8. Endre programvarealgoritmen hvis nødvendig for å implementere multithreading

    Kriteriet for å vurdere ytelsen til applikasjoner, både sekvensielle og parallelle, er utførelsestid. Den asymptotiske rekkefølgen er egnet som et estimat av algoritmen. Ved å bruke denne teoretiske indikatoren er det nesten alltid mulig å evaluere ytelsen til en applikasjon. Det vil si at alt annet likt vil en applikasjon med veksthastighet O(n log n) (rask sortering) kjøre raskere enn applikasjon med veksthastighet O(n2) (selektiv sortering), selv om resultatene av disse applikasjonene er de samme.

    Jo bedre den asymptotiske utførelsesrekkefølgen er, jo raskere kjører den parallelle applikasjonen. Selv den mest produktive sekvensielle algoritmen kan imidlertid ikke alltid deles inn i parallelle tråder. Hvis et program-hotspot er for vanskelig å splitte, og det ikke er noen måte å implementere multithreading på et høyere nivå av hotspotens anropsstabel, bør du først vurdere å bruke en annen sekvensiell algoritme som er lettere å dele enn den opprinnelige. Selvfølgelig er det andre måter å forberede programkode for trådbehandling på.

    For å illustrere det siste utsagnet, vurdere multiplikasjonen av to kvadratiske matriser. Strassens algoritme har en av de beste asymptotiske utførelsesordrene: O(n2.81), som er mye bedre enn O(n3)-rekkefølgen til den vanlige trippel nestede løkkealgoritmen. I følge Strassens algoritme er hver matrise delt inn i fire submatriser, hvoretter syv rekursive anrop utføres for å multiplisere n/2 × n/2 submatriser. For å parallellisere rekursive anrop kan du opprette en ny tråd som sekvensielt vil utføre syv uavhengige submatrisemultiplikasjoner til de når en gitt størrelse. I dette tilfellet vil antall tråder øke eksponentielt, og granulariteten til beregningene utført av hver nyopprettede tråd vil øke når størrelsen på submatrisene reduseres. La oss vurdere et annet alternativ - organisere en pool med syv tråder som jobber samtidig og utføre en submatrisemultiplikasjon hver. Når trådpoolen er ferdig å kjøre, kalles Strassen-metoden rekursivt for å multiplisere submatrisene (som i den sekvensielle versjonen av koden). Hvis systemet som kjører et slikt program har mer enn åtte prosessorkjerner, vil noen av dem være inaktive.

    Matrisemultiplikasjonsalgoritmen er mye lettere å parallellisere ved å bruke en trippel nestet sløyfe. I dette tilfellet brukes datadekomponering, der matrisene er delt inn i rader, kolonner eller submatriser, og hver tråd utfører visse beregninger. Implementeringen av en slik algoritme utføres ved hjelp av OpenMP-pragmaer satt inn på et eller annet nivå av løkken, eller ved eksplisitt å organisere tråder som utfører matrisedeling. For å implementere denne enklere sekvensielle algoritmen, vil mye mindre modifikasjoner av programkoden være nødvendig sammenlignet med implementeringen av den flertrådede Strassen-algoritmen.

    Så nå vet du åtte enkle regler for effektivt å konvertere sekvensiell programkode til parallell. Ved å følge disse reglene vil du mye raskere lage flertrådede løsninger som har økt pålitelighet, optimal ytelse og færre flaskehalser.

    For å gå tilbake til Multithreaded Programming tutorials-websiden, gå til