Thread safety / Atomarer Zugriff auf primitive Datentypen



  • Hallo,
    ich weiß zwar wie man Threads und deren Zugriff auf gemeinsam genutzte Variablen und Speicherbereiche synchronisiert, aber eine Frage habe ich da:
    Was muss ich überhaupt synchronisieren und was nicht?

    Die Suche im Internet ergab leider nur widersprüchliche Aussagen in irgendwelchen Foren. Der eine sagt so, der andere wieder anders, der dritte meint es kommt darauf an, welches Betriebsystem, welcher Compiler, ob 32Bit oder 64Bit. Manche meinen, der Zugriff auf static Variablen sei sicher.

    Aber wie ist es denn nun?

    Ich würde halt gerne mal mit Sicherheit wissen, wann ich Critical Section, Mutex etc. benutzen muss und wann es unnötig ist. Mir gehts eher nicht darum ob die Variablen beim Lesen den aktuellsten Wert haben oder nicht. Ich will einfach nur nicht riskieren, dass mein Programm abstürzt!

    Daher würde ich gerne wissen, ob ich den Zugriff auf Variablen der primitiven Datentypen wie bool, byte, char, short, int, float, double, long (sowohl signed als auch unsigned) sowie auf Zeiger und Referenzen in EnterCriticalSection()/LeaveCriticalSection() einbetten muss oder nicht.

    Ich arbeite mit VC++ 2010 und progammiere Windows-Applikationen. Aussgen über andere Entwicklungsumgebungen/Compiler und Betriebssysteme sind auch willkommen 😉

    MfG



  • SchlechterInformatiker schrieb:

    Daher würde ich gerne wissen, ob ich den Zugriff auf Variablen der primitiven Datentypen wie bool, byte, char, short, int, float, double, long (sowohl signed als auch unsigned) sowie auf Zeiger und Referenzen in EnterCriticalSection()/LeaveCriticalSection() einbetten muss oder nicht.

    Hängt von der Art des Zugriffes ab. Grundsätzlich mal eher Ja...



  • dot schrieb:

    Hängt von der Art des Zugriffes ab. Grundsätzlich mal eher Ja...

    Wenn man ausschliesslich (!) lesend zugreift eher nicht, ansonsten nachlesen :xmas1:

    Laut Intel Reference gibt es einige atomar ablaufende Maschinenbefehle ...

    Zitat: System Programming Guide von Intel:

    LOCKED ATOMIC OPERATIONS
    The 32-bit IA-32 processors support locked atomic operations on locations in system memory. These operations are typically
    used to manage shared data structures (such as semaphores, segment descriptors, system segments, or page tables) in which two
    or more processors may try simultaneously to modify the same field or flag.

    und weiter:

    Guaranteed Atomic Operations
    The Intel486 processor (and newer processors since) guarantees that the following basic memory operations will always
    be carried out atomically:
    . Reading or writing a byte
    . Reading or writing a word aligned on a 16-bit boundary
    . Reading or writing a doubleword aligned on a 32-bit boundary
    The Pentium processor (and newer processors since) guarantees that the following
    additional memory operations will always be carried out atomically:
    . Reading or writing a quadword aligned on a 64-bit boundary
    . 16-bit accesses to uncached memory locations that fit within a 32-bit data bus
    The P6 family processors (and newer processors since) guarantee that the following
    additional memory operation will always be carried out atomically:
    . Unaligned 16-, 32-, and 64-bit accesses to cached memory that fit within a cache line

    u.s.w.



  • Nun, dann ist es wohl so wie ich befürchtet habe: da ich ja nicht sicher gehen kann ob das Programm auf einem Intel Prozessor laufen wird oder nicht, muss ich wohl tatsächlich bei jedem Zugriff auf solch eine Variable mit EnterCriticalSection() / LeaveCriticalSection() arbeiten.

    Habe ich das richtig verstanden?

    Eine Variable, auf die alle Threads nur lesend zugreifen, würde man ja nur anlegen um Speicherplatz zu sparen, oder? Ansonsten könnte man solche Variablen einem neu erzeugten Thread als Aufrufparameter übergeben, denn der Wert einer solchen Variablen ändert sich nie mehr weil ja keiner der Threads schreibend darauf zugreift. Denn würde doch einer der Threads schreibend drauf zugreifen, müsste ich alle Lese- und Schreibzugriffe in EnterCriticalSection() / LeaveCriticalSection() einbetten.


  • Mod

    Das gilt für alle Prozessoren auf denen Windows 32bit/64bit läuft.



  • Ich befürchte halt dass das ganze sehr langsam wird. Ich habe z.B. in meinem Thread eine while-Schleife, deren Bedingung die meiste Zeit wahr ist. Um den Thread beenden zu können schreibe ich nicht einfach

    while(true)
    {
      //...
    }
    

    sondern mache das von einer Variablen abhängig, die ich "von außen" auf 'false' setzen kann:

    DWORD WINAPI VerbindenThread(LPVOID p)
    {
      Verwaltung* verwaltung = (Verwaltung*)p;
      while(verwaltung->running)
      {
        //...
      }
    }
    

    Ich hatte bisher nie Probleme (und auch jetzt immer noch nicht), die Variable 'running' einfach auf 'false' zu setzen - ohne Critical Section.

    Aber wenn ich euch richtig verstanden habe, dann ist das nicht sicher und ich müsste es in etwa so implementieren:

    DWORD WINAPI VerbindenThread(LPVOID p)
    {
      Verwaltung* verwaltung = (Verwaltung*)p;
    
      EnterCriticalSection(verwaltung->critSec);
      bool läuft = verwaltung->running;
      LeaveCriticalSection(verwaltung->critSec);
    
      while(läuft)
      {
        //...
    
        EnterCriticalSection(verwaltung->critSec);
        läuft = verwaltung->running;
        LeaveCriticalSection(verwaltung->critSec);
      }
    }
    

    und "außen":

    //...
    
    EnterCriticalSection(verwaltung->critSec);
    verwaltung->running = false;
    LeaveCriticalSection(verwaltung->critSec);
    
    //...
    

    Ist das die gängige Art, sowas zu machen?

    Wenn ja, dann bräuchte ich für jede Variable, die in verschiedenen Threads an verschiedenen Stellen innerhalb der while-Schleife gelesen/geschrieben werden müssen, eine eigene RTL_CRITICAL_SECTION (wenn mehrere Variablen an der gleichen Stelle geschrieben/gelesen werden können, dann wird das natürlich unter ein und demselben RTL_CRITICAL_SECTION zusammengefasst).

    Es kann gut sein dass in meinem Programm 10 Threads aufgemacht werden müssen... Wird sich das sehr auf die Performanz auswirken oder meint ihr das geht?



  • Hallo,

    Schau mal ob Du das Buch "Win32 Multithreaded Programming" ISBN 1-56592-296-4 noch Irgendwo bekommst. Mir hat dieses Buch sehr geholfen.

    Bei unseren Applikationen laufen meistens 20 bis 100 Threads und fast alle benutzen CriticalSections für die Synchronisation. Ich hatte noch nie ein Geschwindigkeitsproblem mit den CriticalSections.

    Ich steuere die Threads mit Events ich finde das ist die schönere Methode als mit dem boolean.

    Der boolean sollte eigentlich aber auch gehen, mindestens der sollte Atomar sein.

    Ich würde ihn aber als

    volatile bool  running;
    

    Deklarieren, ich glaube er könnte sonst wegoptimiert werden.

    Herzliche Grüsse
    Walter



  • Es gibt keine atomaren Typen, nur atomare Operationen. Aber das ist bei deinen Schleifen bool eh alles irrelevant, den brauchst du nicht zu locken, da reicht volatile aus.
    Ansonsten schau vielleicht auch mal hier: http://msdn.microsoft.com/en-us/library/windows/desktop/ms686360.aspx
    Die WinAPI bietet einen Haufen atomarer Primitive...


  • Mod

    +1 für die Posts von weicher und dot



  • Danke für die Antworten, weicher und dot!

    @weicher: Langsam wird mein Programm bisher noch nicht, ich habs ja noch nicht programmiert 😉 Bevor ich mir die ganze Arbeit mache und hinterher feststelle, es hackt irgendwo, wollte ich mal hier im Forum nachfragen. Gut zu wissen dass CriticalSections keine Probleme machen werden. Ich schaue mal ob es das Buch in der Uni Bibliothek gibt.

    @dot: Ich weiß, es gibt allerhand Funktionen für sowas. Ich kenne die Seite. Wollte nur wissen ob und wann es auch wirklich nötig ist, solche Funktionen zu benutzen. Du sagst jetzt z.B. bei einem bool brauche ich diese Funktionen nicht. Gilt das nur für bool oder auch noch für weitere primitive Datentypen? Ich meine, ich kann schon überall wo ich mir nicht sicher bin, diese Funktionen benutzen. Allerdings wäre es mir lieber, wenn sie nur dort benutze wo es auch wirklich nötig ist und sonst nirgends.



  • Ich glaub du hast eine grundlegend falsche Vorstellung davon, wofür solche atomaren Operationen gut sind. Es ist jedenfalls nicht so, dass man einfach für Alles diese Funktionen benutzt und der Code dann auf magische Weise threadsafe wird. Abgesehen davon möchte ich nochmals betonen dass "atomarer Datentyp" keinen Sinn macht. Es gibt nur atomare Operationen. Die Aussage dass bool atomar wäre macht keinen Sinn. Und Operationen auf einem bool oder int oder sonstwas sind im Allgemeinen ganz gewiss nicht atomar.



  • Ich habe nie etwas von atomaren Datentypen geschrieben. Im Titel steht "Atomarer Zugriff auf primitive Datentypen" - damit sind Lese- und Schreibzugriffe gemeint. Sorry wenn das irreführend ausgedrückt war.

    dot schrieb:

    Und Operationen auf einem bool oder int oder sonstwas sind im Allgemeinen ganz gewiss nicht atomar.

    Dass z.B. die Anweisung

    i++;
    

    aus mehreren Maschinenbefehlen besteht, die mitten in der Ausführung unterbrochen werden können, ist mir klar.
    Aber Keine Ahnung ob eine Zuweisung schon als Operation gilt oder nicht. Kann eine Zuweisung unterbrochen werden? Und was ist mit Zeigern? Wird eine Anweisung wie

    *i = 5;
    

    aufgeteilt in eine Dereferenzierung und eine Zuweisung oder ist das auch nur eine Maschinenanweisung, die nicht unterbrochen werden kann?

    dot schrieb:

    Ich glaub du hast eine grundlegend falsche Vorstellung davon, wofür solche atomaren Operationen gut sind. Es ist jedenfalls nicht so, dass man einfach für Alles diese Funktionen benutzt und der Code dann auf magische Weise threadsafe wird.

    Mag sein, dass ich da was falsch verstanden habe. Ich denke es ist so: Wenn mehrere Threads lesend und schreibend auf ein ganze struct zugreifen bzw. mehrere Variablen lesen/ändern wollen, muss ich natürlich synchronisieren, sonst bekommt ein lesender Thread möglicherwiese inkonsistente Daten wenn er während des Lesens von einem schreibenden Thread unterbrochen wird.

    Ist das soweit richtig?

    Aber ich wollte doch nur, dass mein Programm nicht abstürzt wenn zwei Threads gleichzeitig auf eine Variable zugreifen. Deshalb: Kann ein Thread unterbrochen werden, wenn er einen primitiven Datentyp liest? Wenn ja, dann muss ich das verhindern, wenn nein, dann nicht. Kann sowas überhaupt zu einem Absturz führen oder ist diese Befürchtung völliger Quatsch?



  • SchlechterInformatiker schrieb:

    Ich habe nie etwas von atomaren Datentypen geschrieben. Im Titel steht "Atomarer Zugriff auf primitive Datentypen" - damit sind Lese- und Schreibzugriffe gemeint. Sorry wenn das irreführend ausgedrückt war.

    Ich wollt es nur extra betonen. Die Antwort auf diese Frage lautet immer noch: Das kann man allgemein nicht sagen. Das hängt von allem möglichen ab, evtl. bis hin zu irgendwelchen Compilerflags.

    dot schrieb:

    Kann eine Zuweisung unterbrochen werden?

    Das kann man allgemein nicht sagen. Eine Zuweisung an einen int der richtig aligned ist (!) wird vermutlich meistens atomar sein. Aber bei einem long ist das schon überhaupt nichtmehr so sicher. Der könnte auf der jeweiligen Plattform z.B. zwei Worte haben...

    SchlechterInformatiker schrieb:

    Und was ist mit Zeigern? Wird eine Anweisung wie

    *i = 5;
    

    aufgeteilt in eine Dereferenzierung und eine Zuweisung oder ist das auch nur eine Maschinenanweisung, die nicht unterbrochen werden kann?

    Auch hier wieder: Hängt davon ab. Evtl. muss z.B. zuerst die Adresse gelesen werden...

    Das einzige was man dazu allgemein sagen kann ist: Von solchen Annahmen sollte man sich am besten einfach nicht abhängig machen.

    SchlechterInformatiker schrieb:

    Mag sein, dass ich da was falsch verstanden habe. Ich denke es ist so: Wenn mehrere Threads lesend und schreibend auf ein ganze struct zugreifen bzw. mehrere Variablen lesen/ändern wollen, muss ich natürlich synchronisieren, sonst bekommt ein lesender Thread möglicherwiese inkonsistente Daten wenn er während des Lesens von einem schreibenden Thread unterbrochen wird.

    Ist das soweit richtig?

    Ja.

    SchlechterInformatiker schrieb:

    Aber ich wollte doch nur, dass mein Programm nicht abstürzt wenn zwei Threads gleichzeitig auf eine Variable zugreifen.

    Das kann nicht passieren.

    SchlechterInformatiker schrieb:

    Deshalb: Kann ein Thread unterbrochen werden, wenn er einen primitiven Datentyp liest?

    Hängt davon ab, diese Frage lässt sich allgemein nicht beantworten. Inwiefern ist das denn ein Problem?

    SchlechterInformatiker schrieb:

    Kann sowas überhaupt zu einem Absturz führen oder ist diese Befürchtung völliger Quatsch?

    Wenn dein Programm abstürzt, dann weil es versucht mit kaputten Daten zu arbeiten. Allein nur weil zwei Threads gleichzeitig auf die selbe Speicherstelle schreiben oder von der selben Stelle lesen passiert ganz sicher nichts.



  • Hallo,

    dot meinte mich 😉

    diese Aussage ist ihm sauer aufgestossen

    Der boolean sollte eigentlich aber auch gehen, mindestens der sollte Atomar sein.

    Wenn man diesen Satz für sich alleine Betrachtet, dann hat dot natürlich Recht.

    Aber wir waren ja ganz klar bei

    while (running)
    {
      ...
    }
    

    Natürlich hätte ich besser "mindestens dieser Zugriff sollte Atomar sein" gesagt.

    Herzliche Grüsse
    Walter



  • dot schrieb:

    Die Aussage dass bool atomar wäre macht keinen Sinn. Und Operationen auf einem bool oder int oder sonstwas sind im Allgemeinen ganz gewiss nicht atomar.

    Im Prinzip richtig!

    Laut Intel gibt es "basic memory operations" die atomar sind. Leider können Hochsprachenbefehle aber aus mehreren Maschinenbefehlen bestehen.

    Wenn eine Konstante (z.B. true, false) per atomarer "basic memory operation" gelesen oder geschrieben wird, scheint das aber hier zu gehen.

    Bei INC, DEC und anderen ist jedoch die Verwendung von LOCK dokumentiert, woraus
    man schliesssen kann, das es notwendig ist sich den Datenbus exlusiv zu sichern.
    (Datenwort lesen, inkrementieren und zurückschreiben ist bei mehreren CPUs wohl
    nicht sicher ...)

    SchlechterInformatiker schrieb:

    Dass z.B. die Anweisung

    i++;
    

    aus mehreren Maschinenbefehlen besteht, die mitten in der Ausführung unterbrochen werden können, ist mir klar.

    Genau da liegt das Problem. i++ ist EIN Maschinenbefehl (inc), aber TROTZDEM
    NICHT atomar ...



  • Ok, und was ist denn nun mit Zuweisungen? Sind die atomar?
    Kann höchstens "nur" vorkommen, dass eben ein veralteter Wert gelesen wird weil ein lesender Thread unterbrochen wurde und inzwischen ein schreibender Thread einen neuen Wert geschrieben hat?

    Oder kann da sogar ein Programmabsturz vorkommen wenn ein Thread während des Zugriffs auf eine Variable unterbrochen wird (nicht dass ich das bisher hätte, will aber trotzdem sicher gehen und vorbeugen)?



  • dot schrieb:

    SchlechterInformatiker schrieb:

    Und was ist mit Zeigern? Wird eine Anweisung wie

    *i = 5;
    

    aufgeteilt in eine Dereferenzierung und eine Zuweisung oder ist das auch nur eine Maschinenanweisung, die nicht unterbrochen werden kann?

    Auch hier wieder: Hängt davon ab. Evtl. muss z.B. zuerst die Adresse gelesen werden...

    [/quote]

    Wo wir gerade dabei sind: Im zitierten Programming Guide ist übrigens nur die Rede von Speicherzugriffen. Es gibt Instruktionen, die haben sowohl Lese- als auch Schreibzugriffe. Natürlich wird der Prozessor nicht unterbrochen, wenn er mitten in einer Instruktion steckt. Deswegen heisst es aber noch lange nicht, dass sich in einer Mehrprozessorumgebung zeitlich ein anderer Prozessor zwischen die Speicherzugriffe schieben könnte.



  • merano schrieb:

    dot schrieb:

    Die Aussage dass bool atomar wäre macht keinen Sinn. Und Operationen auf einem bool oder int oder sonstwas sind im Allgemeinen ganz gewiss nicht atomar.

    Im Prinzip richtig!

    Laut Intel gibt es "basic memory operations" die atomar sind. Leider können Hochsprachenbefehle aber aus mehreren Maschinenbefehlen bestehen.

    Natürlich ist auf Maschinenebene ganz klar definiert was atomar ist und was nicht. Aber hier gehts um C++ und da ist das eben nicht definiert und man sollte daher auch keine Annahmen über solche Dinge treffen 😉



  • Aaah Thread-Safety! Ich war gerade dabei meinen letzten Post zu editieren...

    Können Programmabstürze vorkommen?


  • Mod

    [quote="SchlechterInformatiker"]Ok, und was ist denn nun mit Zuweisungen? Sind die atomar?

    Steht doch alles in dem posting von Merano:

    Guaranteed Atomic Operations
    The Intel486 processor (and newer processors since) guarantees that the following basic memory operations will always
    be carried out atomically:
    . Reading or writing a byte
    . Reading or writing a word aligned on a 16-bit boundary
    . Reading or writing a doubleword aligned on a 32-bit boundary
    The Pentium processor (and newer processors since) guarantees that the following
    additional memory operations will always be carried out atomically:
    . Reading or writing a quadword aligned on a 64-bit boundary
    . 16-bit accesses to uncached memory locations that fit within a 32-bit data bus
    The P6 family processors (and newer processors since) guarantee that the following
    additional memory operation will always be carried out atomically:
    . Unaligned 16-, 32-, and 64-bit accesses to cached memory that fit within a cache line

    [quote="SchlechterInformatiker"]
    Oder kann da sogar ein Programmabsturz vorkommen wenn ein Thread während des Zugriffs auf eine Variable unterbrochen wird (nicht dass ich das bisher hätte, will aber trotzdem sicher gehen und vorbeugen)?

    Wie denn das bitte?
    Wir reden hier doch von atomaren Operationen in dem Sinn wie es Mearon schreibt. Wie soll überhaupt so etwas einen Programmabsturz verursachen?



  • x86 schrieb:

    Natürlich wird der Prozessor nicht unterbrochen, wenn er mitten in einer Instruktion steckt. Deswegen heisst es aber noch lange nicht, dass sich in einer Mehrprozessorumgebung zeitlich ein anderer Prozessor zwischen die Speicherzugriffe schieben könnte.

    genau. 100 Punkte. (s.o.)


Anmelden zum Antworten