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
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.
> 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.
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.
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.
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/
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.
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.
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?
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
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?
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
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.
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.
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
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
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
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.
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.
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.
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.
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.
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
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.
Und noch mehr zum Anschauen: https://channel9.msdn.com/Shows/Going+Deep/C-and-Beyond-2012-Herb-Sutter-Concurrency-and-Parallelism
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.
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++?
Jan schrieb: > Gibt es auch so etwas für C++? Ja, ab C++11. https://en.cppreference.com/w/cpp/thread
:
Bearbeitet durch User
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
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...
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).
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.
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.
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.
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?
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?
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.
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.
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.
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!!!
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.
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)
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
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.