6
Tags:
c++
Skrevet af
Bruger #2695
@ 09.03.2004
Indledning
Hej igen.
I denne artikel vil jeg bygge lidt videre på projektet, som jeg startede i artiklen om multithreading. I den artikel nævnte jeg, at trådes adgang til resourcer kan komme ret ubelejligt, og vi så endda et eksempel på det. Jeg vil starte med at forstærke effekten ved at skrive et multitrådet program, som crasher, fordi to tråde bruger samme resourcer på en usmart måde. Så vil vi prøve at synkronisere trådene bagefter, så samme program ikke længere har problemer.
Alle operativsystemer har flere forskellige metoder til at synkronisere tråde, og den, vi skal se på i denne artikel, hedder mutex (Mutual Exclusion: Gensidig udelukkelse).
Kritisk sektion
Før vi går i gang, skal vi lige se på, hvad problemet er. Forestil jer følgende kode:
void enFunktion()
{
if(enPointerTilEtObjekt != NULL)
{
enPointerTilEtObjekt->funktion();
}
}
Hvis
enPointerTilEtObjekt peger på noget, så kalder vi en funktion på den. Det ser udemærket ud, men hvis programmet, som denne kode er en del af, er multitrådet, kan der komme problemer.
Forestil dig at følgende bliver udført af tråd 1:
void enFunktion()
{
if(enPointerTilEtObjekt != NULL)
{
Derefter skiftes der til tråd 2 inden funktionen på
enPointerTilEtObjekt bliver udført. Tråd 2 kører følgende kode:
if(enPointerTilEtObjekt != NULL)
{
delete enPointerTilEtObjekt;
enPointerTilEtObjekt = NULL;
}
..og derefter skiftes der igen. Nu peger
enPointerTilEtObjekt ikke længere på noget, men det tror tråd 1, at den gør og fortsætter:
enPointerTilEtObjekt->funktion();
}
}
---CRASH---
Adgangen til en delt resource kaldes en kritisk sektion, hvis kun én tråd af gangen må tilgå resourcen, og man skal derfor sørge for, at kun én tråd er i en kritisk sektion. Dette kan gøres med mutex objekter.
Et program der crahser
Vi starter med at kigge på et helt program, der crasher. Så vil vi senere synkronisere det, så det ikke længere crasher.
Her er koden:
#include "Thread.h"
#include "Exception.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;
//Dette er den delte resource som skal give os problemer
static int * g_integer = NULL;
class Thread1 : public Thread
{
protected:
void run()
{
int counter = 0;
bool again = true;
while(again)
{
//Her starter den kritiske sektion
if(g_integer != NULL)
{
//g_integer er allokeret
SLEEP(2);//Gør noget som tager lang tid
(*g_integer) = counter;//Skriv til den delte resource
counter++;
//Læs fra den delte resource
cout << "g_integer: " << (*g_integer) << endl;
}
else
{
//g_integer er væk
again = false;
}
//Her slutter den kritiske sektion
}
}
public:
Thread1()
{
}
virtual ~Thread1(){}
};
class Thread2 : public Thread
{
protected:
void run()
{
//Lad trådene køre lidt
SLEEP(5);
//Her starter den kritiske sektion
if(g_integer != NULL)
{
//g_integer er allokeret
delete g_integer;//Dealloker den
g_integer = NULL;//Vis at den er deallokeret
}
//Her slutter den kritiske sektion
}
public:
Thread2()
{
}
virtual ~Thread2(){}
};
int main(int argc, char ** argv)
{
try
{
//Alloker vores delte resource
g_integer = new int;
//Opret de to tråde
Thread1 t1;
Thread2 t2;
//Start trådene
t1.start();
t2.start();
//Lad trådene køre lidt
SLEEP(10);
} catch (Exception & e)
{
cout << e << endl;
}
return 0;
}
Tråd 1 tjekker om resourcen
g_integer er allokeret, men først to sekunder efter, bruges den. Det giver tråd 2 masser af tid til at slette
g_integer, og tråd 1 vil derfor crashe programmet.
Koden giver følgende under Linux:
[robert@codemachine Thread synchronization]$ ./test
g_integer: 0
g_integer: 1
Segmentation fault
[robert@codemachine Thread synchronization]$
Og dette under Windows:
H:\\Thread synchronization>test
g_integer: 0
g_integer: 1
H:\\Thread synchronization>
Desuden får jeg følgende skærmbillede under Windows:
Et program der ikke crasher
Disse problemer er faktisk ret nemme at komme udenom. Linux har følgende funktioner:
int pthread_mutex_init(pthread_mutex_t * mutex, const pthread_mutexattr_t *mutexattr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_lock(pthread_mutex_t *mutex));
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
pthread_mutex_init initialiserer en mutex.
pthread_mutex_destroy nedlægger en mutex.
pthread_mutex_lock tjekker, om en mutex er låst. Hvis den er låst, så lægger den kaldende tråd sig til at sove, til mutexen bliver låst op, hvorefter den låser mutexen og returnerer. Hvis mutexen ikke er låst, låses den, og funktionen returnerer.
pthread_mutex_trylock virker næsten ligesom
pthread_mutex_lock, bortset fra, at tråden ikke sover hvis, mutexen allerede er låst. Funktionen returnerer en boolsk værdi, som indikerer, om den kaldende tråd låste mutexen eller ej.
pthread_mutex_unlock låser en mutex op og signalerer evt. ventende tråde om, at mutexen er fri.
Windows har det samme sæt af funktioner med samme funktionalitet. De hedder bare noget andet:
void InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
void DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
void EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
BOOL TryEnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
void LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
Man starter med at oprette en mutex med
pthread_mutex_init/InitializeCriticalSection for hver delt resource, som kan skabe problemer. Denne mutex er delt mellem alle tråde. Derefter skal man låse mutexen med
pthread_mutex_lock/EnterCriticalSection eller
pthread_mutex_trylock/TryEnterCriticalSection, hver gang man vil tilgå resourcen. Det bevirker, at kun én tråd ad gangen kan komme til at bruge resourcen. Når en tråd er færdig med resourcen, låses mutexen op med
pthread_mutex_unlock/LeaveCriticalSection.
Når programmet afsluttes, eller resourcen ikke længere er tilgængelig, frigives mutexen med
pthread_mutex_destroy/DeleteCriticalSection.
Nu, hvor vi ved, hvordan man beskytter sin kritiske sektion, skriver vi lige programmet fra før om, så det ikke længere crasher:
#include "Thread.h"
#include "Exception.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;
//Dette er den delte resource som skal give os problemer
static int * g_integer = NULL;
#if defined(__linux__)
static pthread_mutex_t g_mutex;
#elif defined(_WIN32)
static CRITICAL_SECTION g_mutex;
#endif
class Thread1 : public Thread
{
protected:
void run()
{
int counter = 0;
bool again = true;
while(again)
{
//Her starter den kritiske sektion
#if defined(__linux__)
pthread_mutex_lock(&g_mutex);
#elif defined(_WIN32)
EnterCriticalSection(&g_mutex);
#endif
if(g_integer != NULL)
{
//g_integer er allokeret
SLEEP(2);//Gør noget som tager lang tid
(*g_integer) = counter;//Skriv til den delte resource
counter++;
//Læs fra den delte resource
cout << "g_integer: " << (*g_integer) << endl;
}
else
{
//g_integer er væk
again = false;
}
//Her slutter den kritiske sektion
#if defined(__linux__)
pthread_mutex_unlock(&g_mutex);
#elif defined(_WIN32)
LeaveCriticalSection(&g_mutex);
#endif
}
}
public:
Thread1()
{
}
virtual ~Thread1(){}
};
class Thread2 : public Thread
{
protected:
void run()
{
//Lad trådene køre lidt
SLEEP(5);
//Her starter den kritiske sektion
#if defined(__linux__)
pthread_mutex_lock(&g_mutex);
#elif defined(_WIN32)
EnterCriticalSection(&g_mutex);
#endif
if(g_integer != NULL)
{
//g_integer er allokeret
delete g_integer;//Dealloker den
g_integer = NULL;//Vis at den er deallokeret
}
//Her slutter den kritiske sektion
#if defined(__linux__)
pthread_mutex_unlock(&g_mutex);
#elif defined(_WIN32)
LeaveCriticalSection(&g_mutex);
#endif
}
public:
Thread2()
{
}
virtual ~Thread2(){}
};
int main(int argc, char ** argv)
{
try
{
//Initialisér vores mutex
#if defined(__linux__)
pthread_mutex_init(&g_mutex,NULL);
#elif defined(_WIN32)
InitializeCriticalSection(&g_mutex);
#endif
//Alloker vores delte resource
g_integer = new int;
//Opret de to tråde
Thread1 t1;
Thread2 t2;
//Start trådene
t1.start();
t2.start();
//Lad trådene køre lidt
SLEEP(10);
//Nedlæg mutexen
#if defined(__linux__)
pthread_mutex_destroy(&g_mutex);
#elif defined(_WIN32)
DeleteCriticalSection(&g_mutex);
#endif
} catch (Exception & e)
{
cout << e << endl;
}
return 0;
}
Simpelt nok. Vi kører det under Linux:
[robert@codemachine Thread synchronization]$ ./test
g_integer: 0
g_integer: 1
g_integer: 2
[robert@codemachine Thread synchronization]$
...og under Windows:
H:\\Thread synchronization>test
g_integer: 0
g_integer: 1
g_integer: 2
H:\\Thread synchronization>
Meget bedre.
En Mutex klasse
Vi er som sædvanlig ikke tilfredse med en masse grim preprocessor kode over det hele, så vi pakker funktionaliteten ind i en klasse, som jeg har valgt at kalde
Mutex:
#if !defined(MUTEX_H)
#define MUTEX_H
#if defined(__linux__)
#include <pthread.h>
#elif defined(_WIN32)
#include <windows.h>
#endif
class Mutex
{
private:
#if defined(__linux__)
//Linux mutex objekt
pthread_mutex_t m_mutex;
#elif defiend(_WIN32)
//Windows mutex objekt
CRITICAL_SECTION m_mutex;
#endif
public:
//Initialiserer mutex objektet
Mutex();
//Frigiver mutex objektet
~Mutex();
//Låser mutex objektet. Blokerer til mutexen kan låses.
void enter();
//Låser mutex objektet medmindre det allerede er låst.
//Returnerer true hvis mutexen blev låst. Ellers returneres false.
bool tryEnter();
//Låser mutex objektet op
void leave();
};
#endif
Implementeringen er ikke så svær:
#include "Mutex.h"
#if defined(__linux__)
#include <errno.h>
#endif
Mutex::Mutex()
{
#if defined(__linux__)
pthread_mutex_init(&m_mutex,NULL);
#elif defined(_WIN32)
InitializeCriticalSection(&m_mutex);
#endif
}
Mutex::~Mutex()
{
#if defined(__linux__)
pthread_mutex_destroy(&m_mutex);
#elif defined(_WIN32)
DeleteCriticalSection(&m_mutex);
#endif
}
void Mutex::enter()
{
#if defined(__linux__)
pthread_mutex_lock(&m_mutex);
#elif defined(_WIN32)
EnterCriticalSection(&m_mutex);
#endif
}
bool Mutex::tryEnter()
{
#if defined(__linux__)
return pthread_mutex_trylock(&m_mutex) != EBUSY;
#elif defined(_WIN32)
return TryEnterCriticalSection(&m_mutex);
#endif
}
void Mutex::leave()
{
#if defined(__linux__)
pthread_mutex_unlock(&m_mutex);
#elif defined(_WIN32)
LeaveCriticalSection(&m_mutex);
#endif
}
Et revideret program der ikke crasher
Det var det!!
Vi slutter selvfølgelig af med at lægge vores nye klasse ind i vores program, så vi kan se den i aktion:
#include "Thread.h"
#include "Exception.h"
#include "Mutex.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;
//Dette er den delte resource som skal give os problemer
static int * g_integer = NULL;
//Vores mutex objekt
Mutex g_mutex;
class Thread1 : public Thread
{
protected:
void run()
{
int counter = 0;
bool again = true;
while(again)
{
//Her starter den kritiske sektion
g_mutex.enter();
if(g_integer != NULL)
{
//g_integer er allokeret
SLEEP(2);//Gør noget som tager lang tid
(*g_integer) = counter;//Skriv til den delte resource
counter++;
//Læs fra den delte resource
cout << "g_integer: " << (*g_integer) << endl;
}
else
{
//g_integer er væk
again = false;
}
//Her slutter den kritiske sektion
g_mutex.leave();
}
}
public:
Thread1()
{
}
virtual ~Thread1(){}
};
class Thread2 : public Thread
{
protected:
void run()
{
//Lad trådene køre lidt
SLEEP(5);
//Her starter den kritiske sektion
g_mutex.enter();
if(g_integer != NULL)
{
//g_integer er allokeret
delete g_integer;//Dealloker den
g_integer = NULL;//Vis at den er deallokeret
}
//Her slutter den kritiske sektion
g_mutex.leave();
}
public:
Thread2()
{
}
virtual ~Thread2(){}
};
int main(int argc, char ** argv)
{
try
{
//Alloker vores delte resource
g_integer = new int;
//Opret de to tråde
Thread1 t1;
Thread2 t2;
//Start trådene
t1.start();
t2.start();
//Lad trådene køre lidt
SLEEP(10);
} catch (Exception & e)
{
cout << e << endl;
}
return 0;
}
Konklusion
Mutexes er én af mange måder at beskytte kritiske sektioner på. Den er god, når kun én tråd må bruge en resource, og det er det, jeg selv oftest har haft brug for. En anden måde at synkronisere tråde på er med semaforer. Det vil jeg måske skrive en artikel om senere. Indtil da..håber du lærte 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 (3)
Udmærket artikel.
Jeg har dog en kommentar til din mutex klasse.
Et vigtigt idiom i c++ programmering er RAII (søg på Google). Det ville i dette tilfælde betyde at man ikke benytter enter/leave funktioner men i stedet lader constructor/destructorer ordne det. På den måde behøver man blot at erklære en instans af objektet når man vil låse det, og man behøver ikke skrive noget som helst for at låse det op.
Det har den fordel at man får sine resourcer (i dette tilfælde mutex'en) frigivet uanset om man har indre return statements som man havde glemt - eller hvis der smides exceptions.
Det kan du have ret i. Jeg vil tilføje et afsnit, som beskriver, hvordan man kan bruge constructor/destructor til at låse/åbne sin mutex.
Du skal være
logget ind for at skrive en kommentar.