So nach dieser Einleitung nun meine Meinung:
Bei der Syncronisation gilt wohl wie bei vielen anderen Themen: "soviel wie nötig, sowenig wie möglich". Also wo braucht man Syncronisation?
Natürlich da wo Daten von 2 oder mehr Threads benutzt werden. Dabei kann es sich um primitive Datentypen handeln, um Containerklassen(Listen/Vektoren usw.) oder um komplexe Datentypen, bei denen es nur um den Zugriff geht.
Im Laufe meiner "Multithreadingkarriere" habe ich 2 bzw. 3 Konzepte erarbeitet:
1. Absichern von Listen/Maps/Vektoren
|
C-/C++-Quelltext
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
template <class T> class SecureList : private std::list<T>
{
CRITICAL_SECTION Critic;
void init(void) { InitializeCriticalSection(&Critic); }
void uninit(void) { DeleteCriticalSection(&Critic); }
public:
void lock(void) { EnterCriticalSection(&Critic); }
void unlock(void) { LeaveCriticalSection(&Critic); }
public:
SecureList(void) { init(); }
~SecureList() { uninit(); }
iterator lockbegin(void) { lock(); return std::list<T>::begin(); }
iterator erase(iterator it) { return std::list<T>::erase(it); }
iterator end(void) { return std::list<T>::end(); }
size_type size(void) const { return std::list<T>::size(); }
bool empty(void) const { return std::list<T>::empty(); }
void clear(void) { lock(); std::list<T>::clear(); unlock(); }
void push_back(const T& e) { lock(); std::list<T>::push_back(e); unlock(); }
void push_front(const T& e) { lock(); std::list<T>::push_front(e); unlock(); }
void remove(const T& e) { lock(); std::list<T>::remove(e); unlock(); }
};
|
So. Diese Klasse habe ich mir zusammen gebastelt, weil ich nirgends eine threadsichere Variante von std::list gefunden habe(oder gibt es die irgendwo und ich habe sie nur übersehen?).
Natürlich lässt sich diese Klasse auch nur unter bestimmten Bedingungen als Threadsafe definieren, weil z.B. weiß ich nicht was passiert wenn man die Methoden empty() oder size() aufruft und gleichzeitig ein anderer Thread ein Element aus der Liste löscht. Weiß dazu jemand vielleicht was zu sagen? Auch dürfen end() und erase() nur noch einem lockbegin() aufgrufen werden.
Hat jemand Verbesserungsvorschläge, kennt threadsichere Containerklassen oder bessere Konzepte?
2. Objekte, die zwischen mehreren Threads genutzt werden absichern
|
C-/C++-Quelltext
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
class NetDataContainer
{
CRITICAL_SECTION Critic;
void init(void) { InitializeCriticalSection(&Critic); }
void uninit(void) { DeleteCriticalSection(&Critic); }
void lock(void) { EnterCriticalSection(&Critic); }
void unlock(void) { LeaveCriticalSection(&Critic); }
unsigned short RefCounter;
~NetDataContainer() { uninit(); }
public:
NetDataContainer(void) { init(); }
DWORD GetID(void) { return ID; }
void grab(void) { lock(); RefCounter++; unlock(); }
void drop(void) { lock(); RefCounter--; if(RefCounter == 0) delete this; else unlock(); }
}
|
Damit kann man sicher stellen, dass die Instanz solange nicht gelöscht wird, bis alle "Zuhörer" gegangen sind. Natürlich geht das nur unter der Bedingung, dass das grab immer nur dann ausgeführt wird, wenn es mind. noch einen Zuhörer gibt. Denn wenn man einfach einen Zeiger von der Klasse X nimmt und das grab() ausführt in einem anderen Thread aber gerade das drop ausgeführt wurde, dann ist der Zeiger ungültig.
Allgemein ist der Umgang mit den grab() und drop() immernoch recht heikel. Daher habe ich folgende Zusatzklasse entwickelt.
3. Verbessertes absichern von Objekten
|
C-/C++-Quelltext
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
class ContainerConnector
{
friend ContainerConnector* NetDataContainer::CreateNetDataContainer(DWORD);
protected:
NetDataContainer* pointer;
ContainerConnector(void) : pointer(NULL) { }
public:
ContainerConnector(const ContainerConnector& c) { pointer = c.pointer; pointer->grab(); }
~ContainerConnector() { pointer->drop(); }
DWORD GetID(void) { return pointer->GetID(); }
};
|
Den Konstruktor von NetDataContainer sollte man dann noch private machen, sodass nur noch die Methode CreateNetDataContainer() eine NetDataContainer erstellen kann.
Die Idee hinter dem Ganzen ist:
Solange mind. ein ContainerConnector exisitiert noch das NetDataContainer-Objekt. Wenn alle ContainerConnector weg sind, wird auch das NetDataContainer-Objekt gelöscht.
Natürlich werden die Methoden von NetDataContainer nicht direkt aufgerufen sondern werden durch Methoden von ContainerConnector verfügbar (s. Code). Auch müssen diese Methodenaufrufe natürlich threadsicher sein, weil durch dieses Konzept verhindert man nur, dass das Objekt einfach gelöscht wird. Die Aufrufe ansich werden nciht abgesichert!
(Anmerkung: das System ist an Smart_pointer angelehnt. Sind die eigentlich threadsicher?)
Ja soweit erstmal aus meinem Nähkästchen. Diskussion und Nachfragen erwünscht