Multithreading

Tags:    c++
Skrevet af Bruger #2695 @ 05.02.2004

Indledning


I denne artikel vil jeg prøve at forklare et komplekst emne som er meget anvendelig i selv simple programmer og nødvendig efterhånden som kompleksiteten i softwaren stiger.
Når et program kører siges det at det kører i en tråd. Dvs. du ved altid præcis at programmet nu skal til at eksekvere koden på linje X i fil Y og når du kalder en blokerende funktion som f.eks. læsning af tastaturet, så stopper hele programmet. Det kan være nok i meget simple programmer, men nogle gange ville det være rart, hvis programmet kunne køre to tråde. Altså at eksekveringen blev delt i to så programmet både lavede en søgning på harddisken, samtidig med at den ventede på input fra brugeren.
Specielt i netværksprogrammering ville dette være en rar feature, da en server kunne stå og vente på en klient. Når nogen så forbinder, kunne programmet dele sig i to dele: én som servicerede klienten og én som ventede på den næste klient. Når der så er 10 klienter, ville der køre 11 tråde. Én per klient og én som ventede på den næste.
Dette koncept kaldes multithreading og det er det vi skal kigge nærmere på.
Denne artikel bygger videre på min tidligere artikel om multi platform udvikling, da Linux og Windows kører tråde lidt forskelligt, og vi vil udvikle en tråd klasse, som er arkitektur uafhængig. Derfor vil jeg foreslå, at du læser artiklen om multi platform udvikling først.

Hvordan virker tråde ?


Multithreading virker ca. ligesom multitasking, som de fleste kender. Flere processer kører samtidig (sådan ser det i hvert fald ud). Det fungerer ved at én proces får lov at køre i ca. et hundrededel af et sekund, hvorefter den standses, og den næste får lov at køre. Sådan fortsætter det, så længe der er processer at køre. For brugeren ser det ud til at alle processerne kører samtidig. Multithreading virker ligesådan, men da hører alle "processerne" sammen i samme program. Først kører én tråd i processen, så skiftes den ud med en anden tråd i samme proces, og så videre.
Forskellen mellem en tråd og en proces er at to processer har hvert deres hukommelses område og derfor ikke kan aflæse eller skrive til hinandens variabler, hvorimod tråde har adgang til processens hukommelse. Det kan give komplekse problemer, som jeg måske vil skrive en artikel om senere.

Multitrådet "Hello, World!"


Gammel programmør overtro siger at hvis du ikke starter med et hello world program, så er du dømt til evig fiasko, så vi må hellere starte blidt ud.
Først lidt kode, derefter beskrivelse:
Fold kodeboks ind/udKode 


Koden skulle compile og lænke uden problemer på Windows. Under Linux skal du lænke med pthread biblioteket:
Fold kodeboks ind/udKode 


Lad os gå gennem programmet. Vi starter med at inkludere vores headere og definere en makro som får programmet til at standse i et antal sekunder. Derefter implementerer vi den funktion som skal køre vores to skrive tråde.
Under Windows skal prototypen være:
Fold kodeboks ind/udKode 

og under Linux:
Fold kodeboks ind/udKode 

De er egentlig ret éns. Begge tager en void pointer som parameter, men Windows udgaven tager bare en typedefineret void pointer i stedet. Vi kan derfor nøjes med at implementere funktionen én gang, éns for både Windows og Linux men med forskellige funktions prototyper.
Parametren sætter vi til at pege på et string objekt som vi vil have skrevet ud med 1 sekunds intervaller, indtil vi beder tråden om at standse, hvilket vi gør ved at sætter den globale variabel 'running' til false. Ingen magi i det.
Så kommer vi til main funktionen. Den opretter to string objekter, som vi vil give videre til vores to tråde senere.
Derefter starter vi de to tråde. Under Linux gøres det med funktionen:
Fold kodeboks ind/udKode 

og under Windows:
Fold kodeboks ind/udKode 

De fleste parametre er ligegyldige for vores simple eksempel. Hvis du vil vide mere så læs på MSDN eller Linux' man-page for tråd oprettelse.
Det eneste vi sætter, er funktionen, som skal eksekveres (threadProcedure), og parametren (enten hello eller world).
Trådfunktionerne startes med det samme, efter de er blevet oprettet, og programmets main tråd fortsætter også efter tråd oprettelsen.
Derfor sover vi lige i 10 sekunder, så trådene kan få lov at køre lidt.
Når main tråden vågner efter 10 sekunder, fortæller den trådene, at de skal standse, ved at sætte 'running' parametren til false.
Derefter venter main tråden først på, at tråden, som skriver "Hello", standser, og derefter på at tråden, som skriver "World", standser.
Det gøres i Windows med:
Fold kodeboks ind/udKode 

og i Linux med:
Fold kodeboks ind/udKode 


Efter trådene er standset, kan main tråden også standse ved at returnere fra main metoden.
Det var det. Ret smertefrit.
Hvis du under kørslen får noget i denne stil:
Fold kodeboks ind/udKode 

Så er det altså fordi, man aldrig kan vide, hvornår en tråd bliver standset, og den næste tager over. Det kan komme på ret ulejlige tidspunkter.

En tråd klasse


Koden kan hurtigt blive grim, hvis vi plastrer den til med #if defined og forskellige måder at implementere tråd funktionerne på, så det kunne være rart, hvis vi kunne slippe for det. Og det er jo det indkapsling er til for. Vi vil derfor lave en klasse, som skjuler trådenes kompleksitet og forskellige implementeringer for programmøren og i stedet tilbyder et simpelt interface til tråd programmering.
Meningen med klassen bliver at man skal arve fra den og overstyre en metode, som så bliver kørt i en ny tråd. Vi skal også have en metode, som en anden tråd kan kalde for at vente på, at tråden standser. Jeg er kommet frem til følgende klassedefinition:
Fold kodeboks ind/udKode 


Operativ systemet kan have problemer med at oprette tråde, ligesom en tråd ikke kan vente på sig selv. Der er en del, som kan gå galt, og et godt design skal tage højde for dette. C++ håndterer fejl ved at kaste exceptions, så jeg vil definere et sæt af exception klasser, som kan indkapsle fejl i trådene.
Jeg vil i efterfølgende artikler udvikle et større sæt af exception klasser, som hænger sammen i et arvehieraki. Indtil videre har vi en generel Exception klasse og en arving, som hedder ThreadException:



Exception klassen skal kunne sendes til in ostream (f.eks. cout) eller konverteres til et string objekt, så vi let kan debugge vores kode.

Definitionerne af Exception og ThreadException bliver:
Fold kodeboks ind/udKode 

og
Fold kodeboks ind/udKode 


Implementeringerne er:
Fold kodeboks ind/udKode 

og
Fold kodeboks ind/udKode 


Meget simpelt. Nu kan vi så implementere vores tråd klasse, så den kan kaste exceptions til højre og venstre:
Fold kodeboks ind/udKode 


Det var det!

Et revideret hello world program


Nu hvor vi har en pænere arkitektur, vil vi prøve at implementere vores hello world program fra før, men det burde være lidt nemmere og pænere denne gang.
Fold kodeboks ind/udKode 


Under Windows skulle filerne gerne compile og lænke uden problemer. Under Linux skal du stadig lænke med pthread:

Fold kodeboks ind/udKode 


Konklusion


Som det ses er koden meget mere generel. Ikke alle de #if defined som før. Kun en enkelt til at definere SLEEP makroen og den kunne man have defineret i en utility header.
Multithreading er meget brugbar, og man slipper ikke for at bruge det når man laver større programmer. Der er dog også problemer med tråde, såsom synkronisering og adgang til data. Disse problemer findes der flere løsninger på, som jeg måske vil skrive om i en senere artikel.
Håber du lærte noget, du kan bruge til noget.

Hvad synes du om denne artikel? Giv din mening til kende ved at stemme via pilene til venstre og/eller lægge en kommentar herunder.

Del også gerne artiklen med dine Facebook venner:  

Kommentarer (6)

User
Bruger #5266 @ 16.02.04 08:52
En rigtig god artikel.
Resultatet er dejlig overskuelig og struktureret kode.
User
Bruger #2695 @ 16.02.04 12:36
Mange tak, det var også meningen. Der er flere på vej :-)
User
Bruger #3009 @ 15.05.04 16:24
Jeg syntes denne artikel går for hurtig frem (har læst din artikel om multi platform udvikling), jeg kan i hvertfald ikke helt forstå det sidste af denne... ellers en skam, da det er to gode artikler...
User
Bruger #4575 @ 20.11.04 18:30
Det er en genial artikkel, men jeg kan kune give Søren ret... Det bliver altså forvirrende, i hvert fald for mig, når du begynder at bruge fx Exception::Exception(string description) : m_description(description) og ostream & operator << (ostream & out, Exception & e), da jeg ikke aner hvad disse ting gør... Måske du skulle beskrive dem lidt nærmere :)

Men det er sq alligevel en god artikkel, som giver god indsigt i multithreading princippet...
User
Bruger #2695 @ 09.12.04 10:57
Exception::Exception(string description) : m_description(description)

Dette er en constructor. Exception objektet har en member variabel som hedder m_description, og den får værdien indeholdt i 'description' parametren. Det kaldes en initialiserings liste, når jeg tildeler variable værdier på den måde. Læs mere her: http://www.parashift.com/c++-faq-lite/ctors.html#faq-10.6

ostream & operator << (ostream & out, Exception & e)

Dette er en operator overloading. Du har sikkert set det her før:
cout << "Hello world!" << endl;
cout er en ostream som man altså kan skrive til med '<<' operatoren. Jeg sørger bare for at mine Exception objekter også kan skrives til ostreams så jeg kan gøre følgende:

try {
//.....
} catch (Exception & ex) {
cout << ex << endl;
}
Læs mere her: http://www.codeproject.com/cpp/cfraction.asp
User
Bruger #10298 @ 26.07.06 20:53
Ne note: Man behøver ikke bruge windows' threading interface på windows. Man kan bruge pthreads som også er tilgængelig under Windows. Det kræver dog at man gider installere det.
Du skal være logget ind for at skrive en kommentar.
t