25
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:
#include <iostream>
#include <string>
#if defined(__linux__)
//Under Linux ligger al tråd funktionalitet
//i pthread.h headeren
#include <pthread.h>
//Og sleep funktionen ligger i unistd.h headeren
#include <unistd.h>
#define SLEEP(x) sleep(x)
#elif defined(_WIN32)
//Under Windows ligger næsten alt i windows.h
//headeren. Også tråd funktionalitet.
#include <windows.h>
#define SLEEP(x) Sleep(1000*x)
#endif
using namespace std;
//Trådene kører så længe denne variabel er true
bool running = true;
#if defined(__linux__)
//Sådan skal tråd funktionen se ud under Linux
void * threadProcedure(void * parameter)
#elif defined(_WIN32)
//Og sådan her under Windows
DWORD WINAPI threadProcedure(LPVOID parameter)
#endif
{
//parameter er en void pointer som peger
//på et string objekt.
string * str = (string*)parameter;
while(running)
{
cout << (*str);
cout.flush();
//Sov et sekund
SLEEP(1);
}
return 0;
}
int main(int argc, char ** argv)
{
string hello("Hello ");
string world("World ");
#if defined(__linux__)
pthread_t helloThread, worldThread;
//Start tråden som skriver "Hello"
pthread_create(&helloThread,NULL,threadProcedure,&hello);
//Start tråden som skriver "World"
pthread_create(&worldThread,NULL,threadProcedure,&world);
#elif defined(_WIN32)
HANDLE helloThread, worldThread;
//Start tråden som skriver "Hello"
helloThread = CreateThread(NULL,0,threadProcedure,&hello,0,NULL);
//Start tråden som skriver "World"
worldThread = CreateThread(NULL,0,threadProcedure,&world,0,NULL);
#endif
SLEEP(10);
//Fortæl trådene at de skal standse
running = false;
#if defined(__linux__)
//Vent på at hello tråden standser
pthread_join(helloThread,NULL);
//Vent på at world tråden standser
pthread_join(worldThread,NULL);
#elif defined(_WIN32)
//Vent på at hello tråden standser
WaitForSingleObject(helloThread,INFINITE);
//Vent på at world tråden standser
WaitForSingleObject(worldThread,INFINITE);
#endif
return 0;
}
Koden skulle compile og lænke uden problemer på Windows. Under Linux skal du lænke med pthread biblioteket:
[rcl@sideshow rcl]$ g++ -o hello hello.cpp -lpthread
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:
DWORD WINAPI (*)(LPVOID);
og under Linux:
void * (*)(void*);
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:
int pthread_create(pthread_t * thread, pthread_attr_t *
attr, void * (*start_routine)(void *), void * arg);
og under Windows:
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);
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:
DWORD WaitForSingleObject(
HANDLE hHandle,
DWORD dwMilliseconds
);
og i Linux med:
int pthread_join(pthread_t th, void **thread_return);
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:
C:\\Documents and Settings\\robert\\Desktop\\Thread>Thread.exe
Hello World Hello World HeWorld llo World Hello World Hello Hello World Hello World HelWorld lo World Hello World Hello
C:\\Documents and Settings\\robert\\Desktop\\Thread>
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:
//Thread.h
#if !defined(THREAD_H)
#define THREAD_H
#if defined(__linux__)
#include <pthread.h>
#elif defined(_WIN32)
#include <windows.h>
#endif
class Thread
{
private:
#if defined(__linux__)
//Funktionen som kaldes af Linux
static void * thread_function(void * parameter);
//Handle til tråden
pthread_t m_thread;
#elif defined(_WIN32)
//Funktionen som kaldes af Windows
static DWORD WINAPI thread_function(LPVOID parameter);
//Handle til tråden
HANDLE m_thread;
#endif
protected:
//Denne funktion skal overstyres af nedarvende klasser
virtual void run() = 0;
public:
//Trådens constructor
Thread();
//Denne funktion får den kaldende tråd til at standse
//til dette tråd objekt er færdig med at eksekvere
void waitForThread();
//Denne funktion vil starte tråden
void start();
};
#endif
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:
//Exception.h
#if !defined(EXCEPTION_H)
#define EXCEPTION_H
#include <iostream>
#include <string>
using namespace std;
class Exception
{
//Denne funktion gør at vi kan skrive en
//exception til en strøm som f.eks. cout
friend ostream & operator<<(ostream & out, Exception & e);
public:
//Denne funktion gør at vi kan caste en
//exception til et string objekt
operator string ();
protected:
Exception(string description);
private:
string m_description;
};
#endif
og
//ThreadException.h
#if !defined(THREADEXCEPTION_H)
#define THREADEXCEPTION_H
#include "Exception.h"
class ThreadException : public Exception
{
public:
ThreadException(string description);
};
#endif
Implementeringerne er:
//Exception.cpp
#include "Exception.h"
Exception::Exception(string description) : m_description(description)
{}
Exception::operator string ()
{
return m_description;
}
ostream & operator << (ostream & out, Exception & e)
{
out << e.m_description;
return out;
}
og
//ThreadException.cpp
#include "ThreadException.h"
ThreadException::ThreadException(string description) : Exception(description)
{
}
Meget simpelt. Nu kan vi så implementere vores tråd klasse, så den kan kaste exceptions til højre og venstre:
//Thread.cpp
#include "Thread.h"
#include "ThreadException.h"
#if defined(__linux__)
void * Thread::thread_function(void * parameter)
#elif defined(_WIN32)
DWORD WINAPI Thread::thread_function(LPVOID parameter)
#endif
{
//parameter er en pointer til et Thread objekt
Thread * t = (Thread*)parameter;
//Kør trådens run-metode
if(t != NULL)
t->run();
t->m_thread = 0;
return 0;
}
Thread::Thread() : m_thread(0)
{
}
void Thread::waitForThread()
{
if(m_thread == 0)
{
throw ThreadException("Thread not running");
}
#if defined(__linux__)
int err = pthread_join(m_thread,NULL);
if(err != 0)
{
switch(err)
{
case ESRCH: throw ThreadException("Not valid thread ID.");break;
case EINVAL: throw ThreadException("Another thread already waiting.");break;
case EDEADLK:throw ThreadException("Thread attempted to wait for itself.");break;
}
}
#elif defined(_WIN32)
DWORD err = WaitForSingleObject(m_thread,0);
if(err == WAIT_FAILED)
{
throw ThreadException("Could not wait for thread.");
}
#endif
}
void Thread::start()
{
if(m_thread != NULL)
{
throw ThreadException("Thread already running");
}
#if defined(__linux__)
//Start thread_function med 'this' som parameter.
int err = pthread_create(&m_thread, 0, thread_function, this);
if(err != 0)
{
throw ThreadException("Could not create thread.");
}
#elif defined(_WIN32)
//Start thread_function med 'this' som parameter.
m_thread = CreateThread(NULL, 0, thread_function, this, 0, NULL);
if(m_thread == NULL)
{
throw ThreadException("Could not create thread.");
}
#endif
}
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.
#include "Thread.h"
#include "ThreadException.h"
#include <iostream>
#if defined(__linux__)
#include <unistd.h>
#define SLEEP(x) sleep(x)
#elif defined(_WIN32)
#define SLEEP(x) Sleep(1000*x);
#endif
using namespace std;
class HelloThread : public Thread
{
protected:
string m_whatToSay;
virtual void run()
{
while(s_keepRunning)
{
cout << m_whatToSay;
cout.flush();
SLEEP(1);
}
}
public:
HelloThread(string s) : m_whatToSay(s)
{
}
static bool s_keepRunning;
};
bool HelloThread::s_keepRunning = true;
int main(int argc, char ** argv)
{
HelloThread hello("Hello ");
HelloThread world("World ");
hello.start();
world.start();
SLEEP(10);
HelloThread::s_keepRunning = false;
hello.waitForThread();
world.waitForThread();
return 0;
}
Under Windows skulle filerne gerne compile og lænke uden problemer. Under Linux skal du stadig lænke med pthread:
[rcl@sideshow rcl]$ g++ -o hello hello.cpp Exception.cpp ThreadException.cpp Thread.cpp -lpthread
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)
En rigtig god artikel.
Resultatet er dejlig overskuelig og struktureret kode.
Mange tak, det var også meningen. Der er flere på vej :-)
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...
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...
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.6ostream & 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
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.