Forum: PC-Programmierung C++ / Threads


von Timm R. (Firma: privatfrickler.de) (treinisch)


Lesenswert?

Hallo,

wäre nett, wenn mir jemand bestätigen könnte, dass das so stimmt, ich 
habe keine Erfahrung mit Threads und muss jetzt aber damit "arbeiten".

1.
static und extern (ohne thread_local) Variablen / Membervariablen sind 
in allen threads identisch, d.h. nur bei solchen können eigtl. race 
conditions auftreten

2.
Ein extern / static pointer den nich nach der Erzeugung eines Threads 
auf ein neu erzeugtes "normales" Objekt (d.h. nicht static oder extern) 
zeigen lasse ist in den anderen Threads ungültig bzw. zeigt dort ins 
Nirvana

3.
Ein extern pointer den nich nach der Erzeugung eines Threads auf ein 
bereits vor dem Entstehen der anderen Threads erzeugtes "normales" 
Objekt (d.h. nicht static oder extern) zeigen lasse ist zeigt in jedem 
Thread auf die lokale Kopie für den Thread, er ist also gültig, die 
Objekte auf die gezeigt wird sind aber unabhängig voneinander.

Richtig?

Vielen Dank

 Timm

von Dr. Sommer (Gast)


Lesenswert?

Timm R. schrieb:
> d.h. nur bei solchen können eigtl. race conditions auftreten

Nö, das kann bei allen passieren

Timm R. schrieb:
> Ein extern / static pointer den nich nach der Erzeugung eines Threads
> auf ein neu erzeugtes "normales" Objekt (d.h. nicht static oder extern)
> zeigen lasse ist in den anderen Threads ungültig bzw. zeigt dort ins
> Nirvana

Nein, du kannst auf alle Variablen aller anderen Thread zugreifen. Das 
ist genau der Unterschied zwischen Threads und Prozessen.

Timm R. schrieb:
> Ein extern pointer den nich nach der Erzeugung eines Threads auf ein
> bereits vor dem Entstehen der anderen Threads erzeugtes "normales"
> Objekt (d.h. nicht static oder extern) zeigen lasse

Hä?

Timm R. schrieb:
> ist zeigt in jedem Thread auf die lokale Kopie für den Thread,

Wo kommt hier eine lokale Kopie her?

Timm R. schrieb:
> Richtig

Kaum. extern und static haben  nichts mit Threads zu tun. Alle Variablen 
außer solche mit thread_local verhalten sich mit oder ohne Threads 
gleich. Du musst bei jeder Variablen auf Race Conditions achten. Du 
kannst sogar auf die thread_local Variablen anderer Threads zugreifen, 
wenn du einen Pointer darauf hast.

Das ist alles ein sehr kompliziertes Thema. Lies ein gutes C++ Buch:

https://stackoverflow.com/a/388282

Beitrag #5855716 wurde von einem Moderator gelöscht.
von Haben wir alles schon ausprobiert (Gast)


Lesenswert?

> Nein, du kannst auf alle Variablen aller anderen Thread zugreifen.

Du willst nicht auf Variablen anderer Threads zugreifen. Wenn du so 
etwas machst, wirst du wochenlang sporadisch auftretende Fehler suchen, 
irgendwann aufgeben und das Zusammenspiel mit Message-Queues neu 
konzipieren.

Ignoriere diese komplizierten Details. Benutze keine Konstruktionen, bei 
denen du diese Details kennen musst.

von MikeH (Gast)


Lesenswert?

Threads sind nicht so kompliziert wenn man verstanden hat, wie sie 
funktionieren ;).
Im Prinzip wird bei einem Thread einfach der Programmablauf auf zwei 
(oder mehreren) Wegen fortgesetzt. Alles was die Lebensdauer, Gültigkeit 
und den Zugriff auf Objekte betrifft bleibt genauso erhalten, als wenn 
es nur jeweils einen Ausführungpfad gäbe.

Es entstehen aber zwei Arten von Parallelität:
1. Ausführungsparallelität (z.B. Ein Thread wartet auf die Ergebnisse 
eines anderen) Dabei müssen Synchronisationspunkte implementiert werden, 
wobei ein Thread vorübergehend angehalten wird und der andere 
signalisiert, wann die Ausführung fortgesetzt werden kann.

2. Daten/Resourcenparallelität
Mehrere Threads können auf die gleichen Ressourcen/Daten zugreifen. 
(z.B. das Einfügen und Entfernen in eine verkettete Liste). Hier muss 
man z.B. durch crititical sections, Monitore, locks dafür sorgen, dass 
die Operation nacheinander durchgeführt wird.

Solche Konstrukte, bei denen ein Thread auf Objekte zugreift, die nur in 
einem anderen Thread gültig sind sollte man vermeiden oder muss dafür 
sorgen, dass der Zugriff nur erfolgt, solange der andere Thread 
existiert.

von Yalu X. (yalu) (Moderator)


Lesenswert?

Wenn es um gemeinsame Zugriffe aus mehreren Threads, solltest du nicht
in Variablen, sondern in Objekten¹ denken. Greifen mehrere Threads auf
ein und dasselbe Objekt zu, davon mindestens einer schreibend, besteht
die Gefahr einer Race-Condition, sonst nicht.

Haben wir alles schon ausprobiert schrieb:
> Du willst nicht auf Variablen anderer Threads zugreifen.

Wenn es sich vermeiden lässt, sollte es vermieden werden, das ist schon
richtig. Aber oft liegt es in der Natur der Dinge, dass zwei Threads
untereinander Information austauschen müssen. Dann muss man sich eben
notgedrungener Weise Gedanken um Race-Conditions u.ä. machen.

—————————————
¹) Mit "Objekte" sind hier nicht (ausschließlich) Klasseninstanzen
   gemeint, sondern (wie in der C++-Norm) allgemein Speicherbereiche,
   die Daten eines bestimmten Datentyps enthalten.

von Dr. Sommer (Gast)


Lesenswert?

MikeH schrieb:
> Threads sind nicht so kompliziert wenn man verstanden hat, wie sie
> funktionieren ;).

Die konkrete Umsetzung kann sehr komplex werden. Selbst so etwas simples 
wie ein observer Pattern wird unübersehbar.
Es treten Probleme mit Cache Kohärenz auf - Speicherzugriffe aus einem 
Thread können z.B. in anderen Threads in anderer Reihenfolge erscheinen.
Einfach alles mit Mutexen zukleistern hilft auch nicht - da bekommt man 
früher oder später Deadlocks, man muss höllisch aufpassen das richtig zu 
machen (siehe Philosophenproblem). Schnell wird der Locking Overhead so 
groß dass die Performance leidet.

Etwas Lesestoff:
https://www2.eecs.berkeley.edu/Pubs/TechRpts/2006/EECS-2006-1.pdf

Ein Ausweg kann das Aktor Modell sein:
https://actor-framework.org/

von MikeH (Gast)


Lesenswert?

Dr. Sommer schrieb:
> Die konkrete Umsetzung kann sehr komplex werden.

Das ist richtig, aber wenn man die Grundprinzipien, die ich beschrieben 
habe berücksichtigt und vernünftig einsetzt, wird man zu >95% gut 
zurecht kommen.
Gib doch mal ein konkretes Beispiel, wo Threads wegen "Cache Inkoherenz" 
nicht funktionieren. Ich bin in 20 Jahren Softwareentwicklung noch nicht 
darüber gestolpert.

von tictactoe (Gast)


Lesenswert?

Um's nochmal zusammenzufassen:

Timm R. schrieb:
> 1.
> static und extern (ohne thread_local) Variablen / Membervariablen sind
> in allen threads identisch, d.h. nur bei solchen können eigtl. race
> conditions auftreten

Nun ja, nicht ganz. Da alle Threads im gleichen Speicher leben, kann 
prinzipiell jeder Thread alles sehen, wenn er denn nur eine Referenz 
darauf hat, was z.B. durch eine globale Variable sein kann, aber auch 
ein Pointer, den man auf irgendeine Weise erhalten hat. Es kann sich 
also ein Thread ein Objekt anlegen (mit new oder auf dem Stack) und 
einen Pointer darauf einem anderen Thread zukommen lassen; somit können 
Race-Conditions auch bei Objekten auftreten, die nicht in static/extern 
Variablen leben.

>
> 2.
> Ein extern / static pointer den nich nach der Erzeugung eines Threads
> auf ein neu erzeugtes "normales" Objekt (d.h. nicht static oder extern)
> zeigen lasse ist in den anderen Threads ungültig bzw. zeigt dort ins
> Nirvana

Das ist nicht richtig. Der Pointer zeigt für alle Threads auf das 
gleiche gültige Objekt. Merke: Alle Threads leben im gleichen Speicher 
und sehen die selben Objekte.

>
> 3.
> Ein extern pointer den nich nach der Erzeugung eines Threads auf ein
> bereits vor dem Entstehen der anderen Threads erzeugtes "normales"
> Objekt (d.h. nicht static oder extern) zeigen lasse ist zeigt in jedem
> Thread auf die lokale Kopie für den Thread, er ist also gültig, die
> Objekte auf die gezeigt wird sind aber unabhängig voneinander.

Das ist auch nicht richtig. Die Threads legen sich keine eigenen Kopien 
an. Alle Threads leben im gleichen Speicher und alle sehen die selben 
Objekte.

von Dr. Sommer (Gast)


Lesenswert?

MikeH schrieb:
> Das ist richtig, aber wenn man die Grundprinzipien, die ich beschrieben
> habe berücksichtigt und vernünftig einsetzt, wird man zu >95% gut
> zurecht kommen.

Dann formuliere doch mal eindeutige und leicht einhaltbare Regeln dafür. 
Solche hat nämlich bis dato niemand gefunden. Auf welcher 
Architekturebene setzt man Mutexe ein? Es passiert sehr schnell, dass 
man mal eine Funktion oder Callback aufruft, während man einen Mutex 
hält, die wiederum einen Mutex sperrt. Mit etwas Pech ist das dann die 
falsche Reihenfolge, und man hat einen Deadlock. Schnell passiert es 
auch, dass man Funktionen eines Objekts aus mehreren Threads 
gleichzeitig aufruft; dann hat man im Objekt Race Conditions. Man könnte 
einfach im Objekt einen Mutex einbauen - das geht aber sofort schief, 
wenn das Objekt Callbacks aufruft. Schau dir mal im Android-Sourcecode 
die Implementation der diversen System-Services an. Die sind voll 
komplizierter Mutex-Frickelei, um sicher zu stellen, dass man nie fremde 
Funktionen aufruft ohne Mutexe zu halten. Da sind garantiert noch 
diverse seltene Deadlocks versteckt. Diese Anwendung schreit förmlich 
nach dem Aktor-Modell...

MikeH schrieb:
> Gib doch mal ein konkretes Beispiel, wo Threads wegen "Cache Inkoherenz"
> nicht funktionieren.
1
#include <thread>
2
#include <iostream>
3
4
int a, b;
5
6
void threadFun () {
7
  while (1) {
8
    ++a;
9
    ++b;
10
  }
11
}
12
13
int main () {
14
  std::thread t (threadFun);
15
  
16
  while (1)
17
    std::cout << a << ", " << b << std::endl;
18
}

Eine mögliche Ausgabe ist:
1
0, 0
2
0, 1
3
0, 2
4
0, 3
5
1, 3
6
2, 3
7
3, 3
8
3, 4
9
3, 5

MikeH schrieb:
> Ich bin in 20 Jahren Softwareentwicklung noch nicht
> darüber gestolpert.

Wie kann das sein?! Noch nie einen Mehrkernprozessor benutzt?

von MikeH (Gast)


Lesenswert?

Dr. Sommer schrieb:

Ja und? Dein Beispiel zeigt doch gerade, was passiert, wenn man die 
Prinzipien nicht beachtet. Auf welchem System hast du diese Ausgabe 
erhalten?

Ich habe überhaupt nicht bestritten, dass man Fehler bei Threads machen 
kann und dass es auch diffizile Fallstricke gibt. Was du hier zum besten 
gibst sind aber konstruierte Vermutungen und helfen dem TO überhaupt 
nicht weiter.

EoD

von Yalu X. (yalu) (Moderator)


Lesenswert?

Dr. Sommer schrieb:
> MikeH schrieb:
>> Gib doch mal ein konkretes Beispiel, wo Threads wegen "Cache Inkoherenz"
>> nicht funktionieren.
>
> #include <thread>
> ...
>
> Eine mögliche Ausgabe ist:
>
> 0, 0
> 0, 1
> 0, 2
> 0, 3
> 1, 3
> ...

Das ist ja keine so große Überraschung, da dies auf einem Single-Core-
Prozessor genauso passieren könnte. Der Effekt kommt ganz einfach
dadurch zustande, dass in main b später als a gelesen wird. In der
Zwischenzeit kann der Thread t ein paarmal weiteriteriert haben.

Ich muss zugeben, dass ich bisher auch noch keine Probleme mit der
Cache-Inkohärenz hatte, zumindest habe ich sie nie bewusst wahrgenommen.
Für auftretende Threading-Probleme habe ich immer eine klassische
Erklärung (wie bspw. die obige) gefunden. Könnte es vielleicht sein,
dass die aktuellen Prozessoren das Problem so gut im griff haben, dass
der Programmierer davon außer Performance-Einbußen gar nichts merkt?

von Timm R. (Firma: privatfrickler.de) (treinisch)


Lesenswert?

Ein herzliches Dankeschön an die netten Helfer! Besonders MikeH, Yalu 
und TicTacToe.

Ja, das mit dem Static ist scheinbar tatsächlich Unsinn, ich bin fest 
überzeugt, dass in einem Tutorial genau das stand, aber um so besser, 
dass ich nachgefragt habe, bevor ich an das Thema gehe :-)

ich glaube, die Problematik in meinem Fall ist tatsächlich recht 
übersichtlich, weil der Programmablauf an den Stellen wo Threads 
auftreten sehr wohlgeordnet und determiniert ist, so dass es bestimmt 
recht lehrreich wird, das anzugehen.


vlg
 Timm

von Dr. Sommer (Gast)


Lesenswert?

MikeH schrieb:
> Ja und? Dein Beispiel zeigt doch gerade, was passiert, wenn man die
> Prinzipien nicht beachtet.

Ja, und der Grund dafür könnte Cache-Inkohärenz sein. Bei komplexeren 
Strukturen passiert es schnell mal, dass man die Prinzipien nicht 
richtig einhält.

MikeH schrieb:
> Auf welchem System hast du diese Ausgabe
> erhalten?

Auf keinem. Das ist nur eine mögliche Ausgabe, die eintreten kann. Das 
ist ja das Gemeine: In 999999 von 1000000 Fällen kann es so 
funktionieren wie gewünscht. Nur weil es mal zu funktionieren scheint, 
heißt das noch lange nicht, dass es korrekt ist.

MikeH schrieb:
> Ich habe überhaupt nicht bestritten, dass man Fehler bei Threads machen
> kann und dass es auch diffizile Fallstricke gibt.

Ja, sehr diffizile.

MikeH schrieb:
> Was du hier zum besten
> gibst sind aber konstruierte Vermutungen und helfen dem TO überhaupt
> nicht weiter.

Ich finde der Hinweis auf mögliche Probleme hilft bei der Implementation 
korrekter Software.

Yalu X. schrieb:
> Das ist ja keine so große Überraschung, da dies auf einem Single-Core-
> Prozessor genauso passieren könnte.

Stimmt, noch eine weitere mögliche Ursache. Selbst wenn man explizit b 
vor a einlesen würde, könnte die Ausgabe aber dennoch so aussehen.

Yalu X. schrieb:
> Ich muss zugeben, dass ich bisher auch noch keine Probleme mit der
> Cache-Inkohärenz hatte, zumindest habe ich sie nie bewusst wahrgenommen.

Ich habe es zugegeben etwas blöd ausgedrückt. Cache-Koheränz-Probleme 
sind die Ursache einer bestimmten Problemfamilie. Auf Anwendungs-Ebene 
arbeitet man hier mit dem Speichermodell der Programmiersprache (z.B. C, 
C++, Java haben eins). Dies garantiert im gezeigten Beispiel eben kein 
bestimmtes Verhalten. Es könnte auch immer "0, 0" ausgegeben werden. Der 
Grund, warum kein Verhalten garantiert ist, liegt daran, das u.a. 
Cache-Kohärenz-Effekte solches Verhalten zunichte machen können.

Yalu X. schrieb:
> Könnte es vielleicht sein,
> dass die aktuellen Prozessoren das Problem so gut im griff haben, dass
> der Programmierer davon außer Performance-Einbußen gar nichts merkt?

Viele Prozessoren haben Cache-Kohärenz-Mechanismen ("Snooper"). Das ist 
aber nicht immer so, und das Speichermodell der Sprache geht auch nicht 
davon aus, dass es die gibt.

Es ist eben so, dass das Problem nur unter ganz bestimmten, durch den 
Anwendungsprogrammierer nicht kontrollierbaren Bedingungen auftritt. Es 
kann sein dass das mit diesem simplen Programm auf keiner Plattform so 
ist. Das macht solche Dinge schwer nachstellbar/beweisbar. Außerdem 
würde man natürlich "intuitiv" bei solchen Problemen Mutexe einsetzen, 
bei denen garantiert ist, dass sie das Problem verhindern 
(Cache-Maintenance-Operations im OS). Die haben aber wieder ihre eigenen 
Fallstricke.

Timm R. schrieb:
> ich bin fest
> überzeugt, dass in einem Tutorial genau das stand,

Ging es vielleicht um Java? Da ist das mit "static" und "synchronized" 
noch mal speziell.

Timm R. schrieb:
> weil der Programmablauf an den Stellen wo Threads
> auftreten sehr wohlgeordnet und determiniert ist,

Na hoffentlich... Threads bringen eine Menge Nicht-Determinismus hinein.

von Gerd E. (robberknight)


Lesenswert?

Dr. Sommer schrieb:
1
> int a, b;

Müsste man hier nicht eigentlich volatile verwenden bevor man sich da 
über Cache-Kohäranz Gedanken machen kann?

Ohne volatile ist es komplett dem Compiler überlassen wann er den 
Speicher ausliest/schreibt und wann er die Variablen in Registern hält. 
Es könnte also auch die ganze Zeit 0,0 oder so bei rauskommen - und das 
ganz ohne Prozessor-Cache, Multicore oder sonstwas.

von Dr. Sommer (Gast)


Lesenswert?

Gerd E. schrieb:
> Müsste man hier nicht eigentlich volatile verwenden bevor man sich da
> über Cache-Kohäranz Gedanken machen kann?

Nein. volatile bringt bei Threads gar nichts. Bei 
Anwendungs-Programmierung mit OS hat es nichts zu suchen.

Gerd E. schrieb:
> Ohne volatile ist es komplett dem Compiler überlassen wann er den
> Speicher ausliest/schreibt und wann er die Variablen in Registern hält.

Nö, wenn man Synchronsations-Mechanismen wie Atomics und Mutexe korrekt 
nutzt macht der Compiler automatisch die richtigen Speicherzugriffe. 
Diese wirken nämlich als (teilweise) memory barrier:

https://en.cppreference.com/w/cpp/atomic/memory_order

von Gerd E. (robberknight)


Lesenswert?

Dr. Sommer schrieb:
>> Ohne volatile ist es komplett dem Compiler überlassen wann er den
>> Speicher ausliest/schreibt und wann er die Variablen in Registern hält.
>
> Nö, wenn man Synchronsations-Mechanismen wie Atomics und Mutexe korrekt
> nutzt macht der Compiler automatisch die richtigen Speicherzugriffe.

ja, schon klar. Aber von Synchronsations-Mechanismen sehe ich in dem 
Beispielcode nichts.

Daher könnte der Compiler entscheiden, a und b in beiden Schleifen 
komplett in Registern zu behalten und nie aus dem Speicher auszulesen. 
Ein volatile würde dagegen erzwingen, daß die Variablen zumindest im 
Speicher und nicht in Registern gehalten werden.

Erst wenn die Variablen tatsächlich aus dem Speicher und nicht aus 
Registern kommen, kann man sich über Cache-Effekte und ähnliches 
unterhalten. Bis dahin versteckt der Registerzugriff die Cache-Effekte 
die Du mit diesem Beispielcode eigentlich zeigen wolltest.

: Bearbeitet durch User
von Dr. Sommer (Gast)


Lesenswert?

Gerd E. schrieb:
> ja, schon klar. Aber von Synchronsations-Mechanismen sehe ich in dem
> Beispielcode nichts.

Richtig. Mit entsprechenden Konstrukten kann man den flicken.

Gerd E. schrieb:
> Ein volatile würde dagegen erzwingen, daß die Variablen zumindest im
> Speicher und nicht in Registern gehalten werden.

Bringt aber auch nix, da sie in verschiedenen Cache-Pages landen 
könnten, die nie abgeglichen werden könnten.

Gerd E. schrieb:
> Bis dahin versteckt der Registerzugriff die Cache-Effekte
> die Du mit diesem Beispielcode eigentlich zeigen wolltest.

Kann sein, kann auch nicht sein. Als ich das Beispiel ausgeführt habe, 
war die Ausgabe jedenfalls nicht "0, 0" - daher waren die Werte durchaus 
im Speicher.

Die korrekte Lösung wären atomics oder Mutexe, je nach gewünschtem 
Verhalten. Die weisen den Compiler auch an, die Werte in den Speicher zu 
schreiben - volatile also überflüssig.

Also: Ohne atomics oder Mutexe ist volatile nicht ausreichend, mit 
atomics oder Mutexen unnötig. Also ist "volatile" bei 
Anwendungsprogrammierung (fast) immer falsch. Das braucht man nur im 
Kernel/Treibern.

https://stackoverflow.com/a/4558031

von Yalu X. (yalu) (Moderator)


Lesenswert?

Dr. Sommer schrieb:
> Gerd E. schrieb:
>> Müsste man hier nicht eigentlich volatile verwenden bevor man sich da
>> über Cache-Kohäranz Gedanken machen kann?
>
> Nein. volatile bringt bei Threads gar nichts.

Doch, es bringt immer dann etwas, wenn der Programmfluss in einer vom
Compiler nicht zu kontrollierenden Weise geändert wird, also bspw. bei
Interrupts oder Multithreading.

Wenn der Compiler sich sicher ist, dass in der Endlosschleife in main a
und b nicht beschrieben werden, darf er sie innerhalb der Schleife als
konstant annehmen und den Lesezugriff vor die Schleife setzen. Der GCC
(hier 8.3.0) macht das tatsächlich so, so dass in dem Beispiel nur
Nullen ausgegeben werden. Erst mit einem volatile werden die Variablen
in jedem Schleifendurchlauf erneut gelesen.

von Dr. Sommer (Gast)


Lesenswert?

Yalu X. schrieb:
> Doch, es bringt immer dann etwas, wenn der Programmfluss in einer vom
> Compiler nicht zu kontrollierenden Weise geändert wird, also bspw. bei
> Interrupts oder Multithreading.

Interrupts treten in Anwendungsprogrammen nicht auf. "Bringt etwas" aber 
nur in dem Sinne, dass sich das Verhalten ändert, aber immer noch nicht 
deterministisch und kontrollierbar ist.

von Manfred M. (bittbeisser)


Lesenswert?

Ich bin da kürzlich über das Buch:
 C++ Concurrency in Action (practical multithreading) von Anthony 
Williams
gestoßen.

Ist wohl DAS Buch zu diesem Thema. Allerdings habe ich festgestellt, das 
ich, um alles zu verstehen, noch einige Hausaufgaben machen muss.

von loeti2 (Gast)


Lesenswert?

Yalu X. schrieb:
>> Nein. volatile bringt bei Threads gar nichts.
>
> Doch, es bringt immer dann etwas, wenn der Programmfluss in einer vom
> Compiler nicht zu kontrollierenden Weise geändert wird, also bspw. bei
> Interrupts oder Multithreading.

Da denke ich bist du Yalu auf dem Holzweg, in der Tat ist volatile bei 
Threadprogrammierung a) überflüssig und b) gefährlich.

Es ist in der Tat nur für Systeme, wo sich der Speicherinhalt auf 
nicht vom normalen Programmfluss abhängende Ereignisse ändern kann 
gedacht.
Beispiel Interrupts, memory-mapped Ports...

Gefährlich weil z.B. Microsoft volatile (nicht standard-gemäß) 
implementiert (hat/oder noch immer macht) daß da automatisch Memory 
Barries gesetzt werden was z.B. GCC nicht tut.

von Dr. Sommer (Gast)


Lesenswert?

Das Buch hier ist auch ganz gut um über den Thread-Tellerrand zu 
blicken:

https://pragprog.com/book/pb7con/seven-concurrency-models-in-seven-weeks

PS: Die tödlichen Therac-25-Unfälle sind teilweise auf schlampig 
programmierte Nebenläufigkeit zurückzuführen... und seitdem sind 
Computer-Architekturen nicht simpler geworden.

von loeti2 (Gast)


Lesenswert?

Dr. Sommer schrieb:
> ch habe es zugegeben etwas blöd ausgedrückt. Cache-Koheränz-Probleme
> sind die Ursache einer bestimmten Problemfamilie. Auf Anwendungs-Ebene
> arbeitet man hier mit dem Speichermodell der Programmiersprache (z.B. C,
> C++, Java haben eins). Dies garantiert im gezeigten Beispiel eben kein
> bestimmtes Verhalten. Es könnte auch immer "0, 0" ausgegeben werden. Der
> Grund, warum kein Verhalten garantiert ist, liegt daran, das u.a.
> Cache-Kohärenz-Effekte solches Verhalten zunichte machen können.

Ich glaube wir unterscheiden hier Cache-Kohärenz zwischen mehreren 
Prozessorkernen. Die Desktop (Intel, AMD) Prozessoren sichern das 
automatisch. Es soll wohl Architekturen geben wo das nicht so ist.

Das Speichermodell der Programmiersprache spielt hier eine entscheidende 
Rolle. Ich denke daß "C" hier failt, denn im C-Standard gibt es keine 
Threads.

In C++ wird beim Verwenden der <atomic>-Konstrukte das speichermodell 
sichergestellt, insbesondere werden hier bei korrekter Verwendung die 
Memory-Barries eingefügt und es ist eine garantierte Anordnung der 
Lese-/und Schreibbefehle vom Compiler zu befolgen.

Zum Anschauen:
https://channel9.msdn.com/Shows/Going+Deep/Cpp-and-Beyond-2012-Herb-Sutter-atomic-Weapons-1-of-2

von Gerd E. (robberknight)


Lesenswert?

Dr. Sommer schrieb:
> Interrupts treten in Anwendungsprogrammen nicht auf.

Doch, selbstverständlich, regelmäßig. Die heißen zwar Signalhandler, 
funktionieren aber letztendlich wie Interrupts.

> "Bringt etwas" aber
> nur in dem Sinne, dass sich das Verhalten ändert, aber immer noch nicht
> deterministisch und kontrollierbar ist.

Nichts anderes hab ich oben behauptet.

Erst mit volatile kommt es dazu, daß Dein Beispiel wirklich von 
Cache-Verhalten abhängt. Vorher zeigt das Beispiel nur die Probleme von 
Registerzugriffen bei Nebenläufigkeit.

Um diesen Code deterministisch und kontrollierbar zu bekommen braucht es 
selbstverständlich anderer Konstrukte als volatile.

von loeti2 (Gast)


Lesenswert?


von M.K. B. (mkbit)


Lesenswert?

Timm R. schrieb:
> weil der Programmablauf an den Stellen wo Threads auftreten sehr
> wohlgeordnet und determiniert ist, so dass es bestimmt recht lehrreich
> wird, das anzugehen.

Es von den Anderen schon im Detail beschrieben, aber nur das es klar 
ist.
Wohlgeordnet in C muss nicht so in Maschinencode übersetzt werden. 
Gerade mit Optimierung darf der Compiler da auch umsortieren.

von Jemand (Gast)


Lesenswert?

loeti2 schrieb:
> denn im C-Standard gibt es keine
> Threads.

Wie kommst du auf das schmale Brett?

von Jan (Gast)


Lesenswert?

Wie man hier ließt scheint multithreading in C++ nich ganz einfach zu 
sein. Sehr komplex, unübersichtlich, die Wahrscheinlichkeit das man es 
nicht ganz sauber löst (irgendwann mal treten doch Probleme auf) ist 
relative hoch.
Wie sieht es mit C#? Async/Await, ist das nicht die Lösung?
Gibt es auch so etwas für C++?

von M.K. B. (mkbit)


Lesenswert?

Jan schrieb:
> Gibt es auch so etwas für C++?

Ja, ab C++11.

https://en.cppreference.com/w/cpp/thread

: Bearbeitet durch User
von Timm R. (Firma: privatfrickler.de) (treinisch)


Lesenswert?

M.K. B. schrieb:
> Timm R. schrieb:
>> weil der Programmablauf an den Stellen wo Threads auftreten sehr
>> wohlgeordnet und determiniert ist, so dass es bestimmt recht lehrreich
>> wird, das anzugehen.
>
> Es von den Anderen schon im Detail beschrieben, aber nur das es klar
> ist.
> Wohlgeordnet in C muss nicht so in Maschinencode übersetzt werden.
> Gerade mit Optimierung darf der Compiler da auch umsortieren.

danke für den Hinweis. Ist schon klar geworden. Mit wohlgeordnet meinte 
ich nur, dass ich erwarte, den Großteil Stellen an denen solche Probleme 
auftreten können zu kennen / kennen zu können, weil es nicht allzu wild 
durcheinander geht.

vlg
 Timm

von loeti2 (Gast)


Lesenswert?

Jemand schrieb:
> loeti2 schrieb:
>> denn im C-Standard gibt es keine
>> Threads.
>
> Wie kommst du auf das schmale Brett?

z.B. hier:

https://www.geeksforgeeks.org/multithreading-c-2/

"Can we write multithreading programs in C?
Unlike Java, multithreading is not supported by the language standard. 
POSIX Threads (or Pthreads) is a POSIX standard for threads. 
Implementation of pthread is available with gcc compiler."

Ja, auch eine OpenMP-Unterstützung des Compilers funktioniert mit 
C-Code...

von Jemand (Gast)


Lesenswert?

loeti2 schrieb:
> z.B. hier:
>
> https://www.geeksforgeeks.org/multithreading-c-2/
>
> "Can we write multithreading programs in C?
> Unlike Java, multithreading is not supported by the language standard.
> POSIX Threads (or Pthreads) is a POSIX standard for threads.
> Implementation of pthread is available with gcc compiler."
>
> Ja, auch eine OpenMP-Unterstützung des Compilers funktioniert mit
> C-Code...

Threads und Interaktionen zwischen welchen sind in C seit C11 definiert 
(die dazugehörige threads.h wird in der Praxis allerdings noch nicht so 
lange unterstützt).

von Dr. Sommer (Gast)


Lesenswert?

Gerd E. schrieb:
> Doch, selbstverständlich, regelmäßig. Die heißen zwar Signalhandler,
> funktionieren aber letztendlich wie Interrupts.

Die sind aber von Standard C bzw. C++ nicht vorgesehen und vertragen 
sich nicht besonders toll mit Multithreading.

loeti2 schrieb:
> Es soll wohl Architekturen geben wo das nicht so ist.

Manche ARMs z.B.

loeti2 schrieb:
> Ich denke daß "C" hier failt, denn im C-Standard gibt es keine Threads.

C11 hat die Threads und Speichermodell von C++11 übernommen (ja, in der 
Richtung).

Gerd E. schrieb:
> Erst mit volatile kommt es dazu, daß Dein Beispiel wirklich von
> Cache-Verhalten abhängt.

Das hängt vom Compiler ab. Wie gesagt, mit GCC unter Linux hängt es auch 
ohne volatile schon davon ab, weil der auch ohne volatile 
Speicherzugriffe macht.

Jan schrieb:
> Async/Await, ist das nicht die Lösung?

Mit Futures hat C++ etwas ähnliches. Allerdings passt das sowieso nicht 
auf alle Probleme.

von Dr. Sommer (Gast)


Lesenswert?

Jan schrieb:
> Wie man hier ließt scheint multithreading in C++ nich ganz einfach zu
> sein.

Alle Sprachen, die mit dem Threads-Locks-Modell arbeiten, haben 
letztlich die gleichen Probleme. Mit Threads wird eine Menge 
Nichtdeterminismus eingeführt, und mit Locks (Mutexe) muss man diesen 
wieder reduzieren. Dabei kann man zu wenig reduzieren sodass das 
Programm nichtdeterministisch bleibt und in 0,001% der Fälle etwas 
falsches ausgibt oder hängen bleibt (deadlock). Oder man reduziert zu 
viel, sodass die Optimierungen von Compiler, OS und Prozessor nicht mehr 
greifen, sodass das Programm durch die Nebenläufigkeit sogar langsamer 
wird als vorher.

Daher gibt es diverse alternative Modelle, wie Aktor-Modell und CSP. 
Siehe dazu o.g. Paper und Buch. M.m.n kann man mit dem Aktor Modell 
ziemlich intuitiv auch komplexe nebenläufige Programme korrekt 
schreiben. Es gibt für diverse Sprachen Implementationen dieser Modelle. 
Am Besten ist es natürlich wenn das Modell Teil der Sprache ist, wie bei 
Erlang.

von Rolf M. (rmagnus)


Lesenswert?

Dr. Sommer schrieb:
> Yalu X. schrieb:
>> Doch, es bringt immer dann etwas, wenn der Programmfluss in einer vom
>> Compiler nicht zu kontrollierenden Weise geändert wird, also bspw. bei
>> Interrupts oder Multithreading.
>
> Interrupts treten in Anwendungsprogrammen nicht auf.

Aber Signale. Die sind quasi die User-Space-Version von Interrupts. Es 
gelten im Wesentlichen die gleichen Regeln.

> "Bringt etwas" aber nur in dem Sinne, dass sich das Verhalten ändert, aber
> immer noch nicht deterministisch und kontrollierbar ist.

Es hat ja keiner behauptet, dass volatile alleine alle Probleme von 
Nebenläufigkeiten löst.

Dr. Sommer schrieb:
> Dabei kann man zu wenig reduzieren sodass das Programm
> nichtdeterministisch bleibt und in 0,001% der Fälle etwas falsches
> ausgibt oder hängen bleibt (deadlock). Oder man reduziert zu viel, sodass
> die Optimierungen von Compiler, OS und Prozessor nicht mehr greifen,
> sodass das Programm durch die Nebenläufigkeit sogar langsamer wird als vorher.

Das ist aber nicht alles. Ein ganz wesentliches Element ist die Struktur 
des Programms. Es kommt letztendlich darauf an, wie viel die Threads 
miteinander kommunizieren müssen. Wenn sie weitgehend unabhängig von 
einander laufen, ist es sehr einfach, und man gewinnt auch wirklich 
durch die Verteilung auf mehrere Kerne. Wenn sie dagegen sehr viel 
kommunizieren und eine komplexe Kommunikationsstruktur haben, wird's 
auch entsprechend schwierig. Und generell eignet sich nicht jedes 
Programm für Parallelisierung.

von Yalu X. (yalu) (Moderator)


Lesenswert?

Getriggert durch diesen Thread habe ich versucht, mich mal etwas
schlauer zu machen und festgestellt, dass sauberes Multithreading
tatsächlich nicht mehr ganz so einfach ist, seit es reordernde Compiler
und Prozessoren gibt. Da habe ich noch einiges an Nachholbedarf,
insbesondere in Bezug auf die in C[++]11 eingeführten Threads und
Speichermodelle.

Was ich mich jetzt aber frage: War es vor der Einführung von C[++]11
überhaupt möglich, ein multithreaded Programm zu schreiben, das sich
selbst auf dem "böswilligsten" Compiler und Prozessor korrekt verhält,
oder ist es eher dem Zufall zu verdanken, dass viele dieser Programme
auch heute noch funktionieren?

von Wilhelm M. (wimalopaan)


Lesenswert?

Yalu X. schrieb:
> Getriggert durch diesen Thread habe ich versucht, mich mal etwas
> schlauer zu machen und festgestellt, dass sauberes Multithreading
> tatsächlich nicht mehr ganz so einfach ist, seit es reordernde Compiler
> und Prozessoren gibt. Da habe ich noch einiges an Nachholbedarf,
> insbesondere in Bezug auf die in C[++]11 eingeführten Threads und
> Speichermodelle.
>
> Was ich mich jetzt aber frage: War es vor der Einführung von C[++]11
> überhaupt möglich, ein multithreaded Programm zu schreiben, das sich
> selbst auf dem "böswilligsten" Compiler und Prozessor korrekt verhält,
> oder ist es eher dem Zufall zu verdanken, dass viele dieser Programme
> auch heute noch funktionieren?

Natürlich ist/war das möglich.
Wo siehst Du ein Problem?

von Dr. Sommer (Gast)


Lesenswert?

Yalu X. schrieb:
> Was ich mich jetzt aber frage: War es vor der Einführung von C[++]11
> überhaupt möglich, ein multithreaded Programm zu schreiben, das sich
> selbst auf dem "böswilligsten" Compiler und Prozessor korrekt verhält,

Da die Sprachstandards kein Multithreading vorsahen, war es eben mit 
Standard-C(++) nach ISO nicht möglich. Die jeweiligen Plattformen haben 
aber (unportable) Unterstützung hinzugefügt, z.B. in Form von 
POSIX-Threads. Diese war dann natürlich so umgesetzt, dass sie auch 
funktionierte; z.B. würde sem_post Speicher-Modifikationen für andere 
Threads sichtbar machen, d.h. den Cache raus schreiben.
Das wird auch immer noch so sein, sodass solche Programme auch immer 
noch funktionieren. Tatsächlich nutzen die Standard-C++-Funktionen 
wahrscheinlich die POSIX-Thread-Funktionen. Wenn man sich aber irgendwo 
implizit auf die Reihenfolge von Speicherzugriffen verlassen hat, könnte 
das jetzt schief gehen.

von DPA (Gast)


Lesenswert?

Dr. Sommer schrieb:
> Die jeweiligen Plattformen haben
> aber (unportable) Unterstützung hinzugefügt, z.B. in Form von
> POSIX-Threads

POSIX ist ein Standard. Sauberer POSIX Code ist auf mit allen 
POSIX-Kompatiblen verwendbar, also portabel. Klar, es gibt etwas 
Spielraum bei den Implementierungen, wo gewisse Sachen als optional 
definiert wurden, aber das steht ja dabei. Und C11 Thread support ist ja 
nach Standard ebenfalls optional.

von Dr. Sommer (Gast)


Lesenswert?

DPA schrieb:
> POSIX ist ein Standard. Sauberer POSIX Code ist auf mit allen
> POSIX-Kompatiblen verwendbar, also portabel.

Aber nicht auf Nicht-POSIX-Systemen, wie Windows (jaja ich weiß, es gibt 
ein POSIX- und ein Linux-Subsystem... das ist aber nicht das gleiche). 
Code welcher die Standard-C(++)-Threading-Funktionen nutzt hingegen 
schon. Gerade in C++ ist std::thread sauberer als die 
Posix-Threading-Funktionen.

von DPA (Gast)


Lesenswert?

Dr. Sommer schrieb:
> Gerade in C++ ist std::thread sauberer als die
> Posix-Threading-Funktionen.

Ich wollte schon 2 mal wechseln, aber zu den Zeitpunkten war das in den 
Compilern meiner Distros noch nicht implementiert. Posix-Threading geht 
aber, und mit mingw gehen die auch auf Windows. Vorerst ist das also 
noch portabler.

Und falls noch jemand vom Standard Committee mit liest: Wer hatte die 
bescheuerte Idee, dass Compiler _STDC_NO_THREADS_ definieren sollen 
wenn threads.h nicht da ist? Die Compiler/Standard Libraries vergessen 
das immer zu definieren! Hätte man stattdessen umgekehrt ein 
_STDC_HAS_THREADS_ festgelegt, könnte man einen einfachen polyfill 
schreiben und verwenden falls nötig, aber so muss man extra immer mit 
einem Build-Tool wie z.B. automake nachprüfen, ob das 
_STDC_NO_THREADS_ jetzt nicht da ist, weil es implementiert ist, oder 
weil es vergessen wurde!!!

von loeti2 (Gast)


Lesenswert?

Jemand schrieb:
> Threads und Interaktionen zwischen welchen sind in C seit C11 definiert
> (die dazugehörige threads.h wird in der Praxis allerdings noch nicht so
> lange unterstützt).

OK, danke. Visual Studio 2017 kennt sie noch nicht ;-(

Und für alle die mal Multithreading im Trockenen üben wollen:

Ein großes Hotel hat das Problem das das Ein- und Auschecken der Gäste 
zu lange dauert, die Gäste sind genervt und manche die Einchecken wollen 
gehen dann lieber zu einem anderen Hotel.
Idee der Verwaltung: Wir bauen eine zweite Rezeption, in einem anderen 
Stockwerk (d.h. die können sich nicht abstimmen).
Die Vergabe der Zimmer folgt über je ein Computer-Terminal, das an einen 
zentralen Vergabe-Server angeschlossen ist.
Die Ausgabe/Annahme der Zimmerschlüssel erfolgt so daß die zweite 
Rezeption einen Schlüssel bei der ersten anfordert und dieser per 
Rohrpost geschickt wird, Abgabe genauso.

Wie würdet ihr die Probleme lösen:
- Gäste auf die Rezeptionen verteilen
- Konzeption des Vergabe-Servers, es muß in jedem Fall verhindert werden 
daß Zimmer mehrfach vergeben werden :-)
- Instruktionen in welcher Reihenfolge die Angestellten die Arbeiten 
beim Gäste aufnehmen/auschecken durchführen, wobei die Angestellten 
gerne auch mal die Reihenfolge ändern (Instruktion Reordering :-)

Und es soll danach auch ein größerer Durchsatz erfolgen, im Idealfall 
daß in der gleichen Zeit die doppelte Anzahl Gäste abgearbeitet werden 
kann.

von DPA (Gast)


Lesenswert?

loeti2 schrieb:
> - Gäste auf die Rezeptionen verteilen

Jeden 2ten zur zweiten Rezeption?
Oder einfach zufällig, sollte langfristig auch ne gleichmässige 
Verteilung geben.
Oder vielleicht nachsehen, wo am wenigsten warten.
Oder die Rezeptionisten die Gäste von der schlange abholen lassen.

> - Konzeption des Vergabe-Servers, es muß in jedem Fall verhindert werden
> daß Zimmer mehrfach vergeben werden :-)

Es braucht ja nur einen Server. Mehrere Threads braucht man auch nicht 
zwangsläufig dafür. Über die DB & Transaktionen könnte man es ansonsten 
auch synchronisieren. Wenn es wirklich keinen zentralen knoten und 
keinen master server gibt, eventuel Paxos Protokolle anwenden. Oder man 
könnte jeder Rezeption im vornherein nur eine Hälfte der Schlüssel 
geben. Oder für jeden gast schnell ein neues Zimmer bauen und danach 
wieder abreissen. etc.

> - Instruktionen in welcher Reihenfolge die Angestellten die Arbeiten
> beim Gäste aufnehmen/auschecken durchführen, wobei die Angestellten
> gerne auch mal die Reihenfolge ändern (Instruktion Reordering :-)

Jeder holt sich den nächsten Gast, in der selben reihenfolge, wie diese 
eingecheckt haben?

(PS: Die analogie ist zu Praxisfern, als das da was sinvolles bei 
rauskäme)

von loeti2 (Gast)


Lesenswert?

DPA schrieb:
> Es braucht ja nur einen Server. Mehrere Threads braucht man auch nicht
> zwangsläufig dafür. Über die DB & Transaktionen könnte man es ansonsten
> auch synchronisieren.

Die "Threads" sind ja in meinem Modell auch die zwei Rezeptionen, die 
parallel arbeiten ;-)

Und mit dem Zugriff auf die reservierten/freien Zimmer kann man sich 
über Locking-Mechanismen Gedanken machen, man soll keinen 
Reservierungsserver programmieren.

> > - Instruktionen in welcher Reihenfolge die Angestellten die Arbeiten
> > beim Gäste aufnehmen/auschecken durchführen, wobei die Angestellten
> > gerne auch mal die Reihenfolge ändern (Instruktion Reordering :-)

> Jeder holt sich den nächsten Gast, in der selben reihenfolge, wie diese
> eingecheckt haben?

> (PS: Die analogie ist zu Praxisfern, als das da was sinvolles bei
> rauskäme)

Deshalb muß man hier "Memory-Barriers" an die Angestellten rausgeben, 
wann sie die Arbeitsfolge nicht ändern dürfen.

Ansonsten gehen deine Gedanken schon in die richtige Richtung, wie 
verteile ich die Last, welche Arbeiten sind unabhängig (wie Adresse 
aufnehmen..).

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.