Du bist nicht angemeldet.

Stilllegung des Forums
Das Forum wurde am 05.06.2023 nach über 20 Jahren stillgelegt (weitere Informationen und ein kleiner Rückblick).
Registrierungen, Anmeldungen und Postings sind nicht mehr möglich. Öffentliche Inhalte sind weiterhin zugänglich.
Das Team von spieleprogrammierer.de bedankt sich bei der Community für die vielen schönen Jahre.
Wenn du eine deutschsprachige Spieleentwickler-Community suchst, schau doch mal im Discord und auf ZFX vorbei!

Werbeanzeige

Nox

Supermoderator

  • »Nox« ist der Autor dieses Themas

Beiträge: 5 272

Beruf: Student

  • Private Nachricht senden

1

29.09.2007, 10:51

[Multithreading] absichern von komplexen Strukturen

Da die Grundlagen oft genug erklärt und eigentlich leicht nachvollziehbar sind, gibt es die Meinung, dass Multithreading einfach ist.
Ist es auch! Nur muss man dabei einige Sachen beachten. Welche das sind wollen wir hier rausarbeiten.
PRO Lernkurs "Wie benutze ich eine Doku richtig"!
CONTRA lasst mal die anderen machen!
networklibbenc - Netzwerklibs im Vergleich | syncsys - Netzwerk lib (MMO-ready) | Schleichfahrt Remake | Firegalaxy | Sammelsurium rund um FPGA&Co.

$nooc

Alter Hase

Beiträge: 873

Wohnort: Österreich / Kärnten

Beruf: Schüler

  • Private Nachricht senden

2

29.09.2007, 11:12

also ich persönlich kann da jetzt nicht viel mitreden, da ich von multithreading im prinzip keine ahnung habe..
hab zwar schon ein kleines tutorial gemacht, aber bis jetzt noch nicht gebraucht..

trotzdem fallen mir da schon ein paar konkrete fragen ein zb wie das mit der synchronisation von threads funktioniert und wie man das genau macht, sodass kein objekt gelöscht wird, in dem ein anderer thread gerade liest usw.

der einstieg in multithreading ist ja wirklich leicht. nen thread hat man ja schnell mal offen, aber ich denke die fehlerbehandlung wird dadurch um einiges verkompliziert :roll:
Am Anfang der Weisheit steht die eigene Erkenntnis, dass man selbst nichts weiß! - Sokrates

Nox

Supermoderator

  • »Nox« ist der Autor dieses Themas

Beiträge: 5 272

Beruf: Student

  • Private Nachricht senden

3

29.09.2007, 11:34

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 :)
PRO Lernkurs "Wie benutze ich eine Doku richtig"!
CONTRA lasst mal die anderen machen!
networklibbenc - Netzwerklibs im Vergleich | syncsys - Netzwerk lib (MMO-ready) | Schleichfahrt Remake | Firegalaxy | Sammelsurium rund um FPGA&Co.

Nox

Supermoderator

  • »Nox« ist der Autor dieses Themas

Beiträge: 5 272

Beruf: Student

  • Private Nachricht senden

4

30.09.2007, 12:24

Hmm keine Fragen und/oder Kritik? Man muss ich gut sein :D

Ne Scherz bei Seite. Bei diesem Thema geht es mir persönlich vorallem darum zu schauen, ob es noch einfachere(weniger rechenintensive) Lösungen gibt, als die ich gerade nutze. Vorallem ob diese Lösungen auch wirklich threadsafe sind, weil oft übersieht man bestimmte "Randfälle".

Also wenn jemand meint irgendwas zu sehen, wo diese Konzepte eben nicht sicher sind oder es noch was einfachers gibt, bitte meldet euch!
PRO Lernkurs "Wie benutze ich eine Doku richtig"!
CONTRA lasst mal die anderen machen!
networklibbenc - Netzwerklibs im Vergleich | syncsys - Netzwerk lib (MMO-ready) | Schleichfahrt Remake | Firegalaxy | Sammelsurium rund um FPGA&Co.

rklaffehn

Treue Seele

Beiträge: 267

Wohnort: Braunschweig

  • Private Nachricht senden

5

01.10.2007, 13:18

Hi, also von mir auch mal (wieder) ein paar Kommentare.

Was mir an deiner Implementation 1) nicht gut gefällt, ist folgendes: es gibt eine Asymetrie zwischen "lockbegin ()" und "end ()" (ist dir ja auch schon aufgefallen und geht so auch nicht anders).

IMHO ist es da besser, explizit vorher "lock ()" zu nutzen, dann ganz normal mit "begin ()" und "end ()" über die Liste zu iterieren und hinterher wieder mit "unlock ()" freizugeben. Dieses explizit geschriebene "lock ()" ist nicht weiter schlimm; das Argument, dass man sowas vergessen kann, gilt hier nicht, weil man das "unlock ()" auch nicht vergessen darf, sonst gibt es sicher einen DeadLock. Außerdem kann man die Klasse dann auch außerhalb von Threads (oder nur innerhalb eines einzigen Thread) benutzen und hat eben nicht immer den Overhead für die Synchronisation.

Dieses Asymmetrie macht dann auch den Zugriff mittels size/empty etwas gefählich, die nicht explizit gesperrt sind. Das ist aber auch nicht weiter schwer:

C-/C++-Quelltext

1
2
3
4
5
6
7
size_type size () const
{
  lock ();
  size_type result = std::list<T>::size ();
  unlock ();
  return result;
}


Die "per Method" Locks (clear und so weiter...) sind auch okay, wenn man genau weiss, dass nur diese eine Aktion stattfinden will. Für mehrere "push_back" nacheinander ist wieder ein explizites lock/unlock Paar rund um die Zugriffe.

Btw: die std:: Container schehren sich nicht um Thread-Sicherheit, aber immerhin sind sie reentrant, können also unbeschränkt innerhalb von Threads benutzt werden.

Punkt 2) sieht wie eine synchronisierte und magere :) Version des boost::shared_ptr aus, die allerdings im Falle der Zerstörung ein Problem bekommen kann, weil der kritische Abschnitt im gesperrten Zustand gelöscht werden soll. Außerdem ist ein "selfdelete" immer mit extremer Vorsicht zu genießen. Besser ist hier in jedem Fall eine Kapselung um einen Zeiger (eben wie boost::shared_ptr).
God is real... unless declared integer.
http://www.boincstats.com/signature/user_967277_banner.gif

Nox

Supermoderator

  • »Nox« ist der Autor dieses Themas

Beiträge: 5 272

Beruf: Student

  • Private Nachricht senden

6

01.10.2007, 14:52

Zur Kritik an 1: weißt du wie oft ich schon ein unlock vergessen habe :D ? Sehr oft und durch das lockbegin stelle ich das spätestens dann mit, wenn das komplette Programm nichts mehr macht^^. Klar könnte man alles per explizienten lock()/unlock() Aufruf machen, aber in dem Fall nehme ich persönlich lieber den Overhead in Kauf.
KANN aber jeder für sich entscheiden :)

zur Kritik an 2: Daher habe ich ja die Erweiterung zu dem zweiten Konzept, um eben sicher zu stellen, dass immer mind. ein "Zuhörer" noch existiert. Weil nur über eben einen solchen "Zuhörer" kann man auf das Obejkt zugreifen bzw. sich einen neuen "Zuhörer" erstellen. Beim reinen grab()/drop() ist das natürlich ein wenig heikeler, aber solange das grab() durch einen Thread ausgeführt wird welcher selbst schon den Refcounter erhöht hat, ist es unproblematisch.

Oder habe ich da was übersehen?
PRO Lernkurs "Wie benutze ich eine Doku richtig"!
CONTRA lasst mal die anderen machen!
networklibbenc - Netzwerklibs im Vergleich | syncsys - Netzwerk lib (MMO-ready) | Schleichfahrt Remake | Firegalaxy | Sammelsurium rund um FPGA&Co.

David Scherfgen

Administrator

Beiträge: 10 382

Wohnort: Hildesheim

Beruf: Wissenschaftlicher Mitarbeiter

  • Private Nachricht senden

7

03.10.2007, 11:25

Nox:

1. Man sollte sich auch bewusst sein, dass Iteratoren ungültig werden können (je nach Container), wenn neue Elemente hinzugefügt oder alte gelöscht werden.
Darum könnte es sinnvoll sein, zwei kritische Sektionen zu verwenden: eine für's Lesen, eine für's Schreiben.

Angenommen ich durchlaufe nur den Container, ohne etwas zu ändern (mit einem const_iterator).

C-/C++-Quelltext

1
2
3
4
5
6
for(std::list<int>::const_iterator it = myList.begin();
    it != myList.end();
    it++)
{
    // tu was

}


Wenn ich hier nicht absichere, könnte es passieren, dass ein anderer Thread währenddessen etwas aus der Liste löscht. Dadurch könnte mein Iterator in der for-Schleife ungültig werden.
Also könnte man auf folgende Idee kommen:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
myList.lock();
for(std::list<int>::const_iterator it = myList.begin();
    it != myList.end();
    it++)
{
    // tu was

}
myList.unlock();


Aber das ist auch nicht gut, weil nun auch nur 1 Thread gleichzeitig lesen kann, obwohl paralleles Lesen ungefährlich ist. Also müsste während des Durchlaufs dafür gesorgt werden, dass lediglich keine Schreibzugriffe stattfinden.

2. Dein Absicherungskonzept ist nicht Exception-safe. Wenn zwischen lock() und unlock() eine Exception geworfen wird (und so gut wie alle STL-Container können Exceptions werfen), wird unlock() nie aufgerufen. Was das bedeutet, kannst du dir ja denken.

Eine Lösung dafür ist, ein Lock als Objekt zu konstruieren, das auf dem Stack angelegt wird (so habe ich es gemacht). Wird das Objekt zerstört, wird der Lock aufgehoben. Da beim Werfen einer Exception alle Objekte auf dem Stack zerstört werden, wird auch der Lock zerstört, und alles ist wieder in Ordnung. Das könnte so aussehen:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
{
    ScopedLock lock(myList); // Konstruktor: ScopedLock(Lockable&)

    for(std::list<int>::const_iterator it = myList.begin();
        it != myList.end();
        it++)
    {
        // tu was

    }
}
// lock wurde zerstört.


So kann ich auch nicht "vergessen", unlock() aufzurufen. Trotzdem könnte man der ScopedLock-Klasse noch ein unlock() verpassen, wenn man nicht so lange warten will, bis das Objekt zerstört wurde.

Nox

Supermoderator

  • »Nox« ist der Autor dieses Themas

Beiträge: 5 272

Beruf: Student

  • Private Nachricht senden

8

03.10.2007, 11:30

Hmm das mit dem Lock als Instanz ist natürlich auch eine interessante Idee. Was mich allerdings interessiert ist das mit dem zwei locks, weil darüber habe ich mir auch schonmal Gedanken gemacht, bin aber auf keine gute Lösung gekommen.
PRO Lernkurs "Wie benutze ich eine Doku richtig"!
CONTRA lasst mal die anderen machen!
networklibbenc - Netzwerklibs im Vergleich | syncsys - Netzwerk lib (MMO-ready) | Schleichfahrt Remake | Firegalaxy | Sammelsurium rund um FPGA&Co.

rewb0rn

Supermoderator

Beiträge: 2 773

Wohnort: Berlin

Beruf: Indie Game Dev

  • Private Nachricht senden

9

03.10.2007, 11:39

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
26
27
void readlock()
{
    EnterCriticalSection(ReadCounter);
    Readers++;
    if(Readers == 1)
        EnterCriticalSection(CriticalSection);
    LeaveCriticalSection(ReadCounter);
}

void readunlock()
{
    EnterCriticalSection(ReadCounter);
    Readers--;
    if(Readers == 0)
        LeaveCriticalSection(CriticalSection);
    LeaveCriticalSection(ReadCounter);
}

void writelock()
{
    EnterCriticalSection(CriticalSection);
}

void writeunlock()
{
    LeaveCriticalSection(CriticalSection);
}



Also das ist jetzt ausm Kopf, also nicht garantiert, fehlerfrei zu sein, aber man erkennt das Konzept. So ein "Zählerschutz" ist ne feine Sache, und sehr oft zu gebrauchen, wenn man mehrere Zugriffe gleichzeitig realisieren will.

Das hier ist jetzt ohne Bevorzugung, und der Schreiber muss solange warten, bis alle Leser fertig sind, man kann da aber auch noch so rumbasteln, dass die Schreiber bevorzugt werden, wahlweise auch die Leser, abwechselnd, usw. Das sollte bei einer Handvoll Threads aber noch nicht so wichtig sein.

Nox

Supermoderator

  • »Nox« ist der Autor dieses Themas

Beiträge: 5 272

Beruf: Student

  • Private Nachricht senden

10

03.10.2007, 11:50

Das die Idee so banal ist, ist echt grausam :lol: .
Wegen dem impliziten lock() eine Frage:

Wie wollt ihr sicherstellen, dass immer ein lock() aufgerufen wird, wenn die Liste benutzt wird? Denn ein fehlendes lock() macht sich nicht so massiv bemerkbar, wie ein fehlendes unlock(). Wenn ein unlock fehlt stellt man das schnell fest. Wenn aber ein lock() fehlt bemerkt man es unter Umständen nur weil das Programm dauernd abschmiert und man nicht weiß wieso nun der verdammte Iterator im Eimer ist.

Also was tun?
PRO Lernkurs "Wie benutze ich eine Doku richtig"!
CONTRA lasst mal die anderen machen!
networklibbenc - Netzwerklibs im Vergleich | syncsys - Netzwerk lib (MMO-ready) | Schleichfahrt Remake | Firegalaxy | Sammelsurium rund um FPGA&Co.

Werbeanzeige