Lesezugriff auf gemeinsame Variablen in Multithreadanwendungen

    Lesezugriff auf gemeinsame Variablen in Multithreadanwendungen

    Hallo,

    hier mal wieder eine Frage.
    Nehmen wir an, ich habe eine Multithreadanwendung, bei der drei oder mehr "Horchthreads" Sensor- und andere informationen in gemeinsamen Objekten ablegen.
    Tritt eine bestimmte Situation ein, wird ein Workerthread gestartet, der dann aber während seiner Existenz lesend auf diese Daten zugreifen muß.
    Der Würkerthread läuft nur einmal durch und wird bei Bedarf neu erzeugt.

    Jetzt wird ja in jedem Tutorial zu Threads als Erstes gelärmt, daß Schreibvorgänge in den Mainthread synchronisiert werden müssen.
    a) Aber wie ist des mit Lesezugriffen ?
    b) Und ist das Hauptformular ein guter Ort für die gemeinsamen Datenbehälter ?
    c) Und muß ich, wenn ich nur Speichervariablen / Properties ohne grafische Effekte auf die GUI beschreibe, überhaupt synchronisieren ?

    :help: In tiefem Nachdenken :help:
    ism
    Morgen ist Heute schon Gestern
    1. Jein, das hängt vom Lesezugriff ab. Wenn der Lesevorgang mit einer atomaren Operation realisiert werden kann, musst du nicht synchronisieren. Atomar heißt, dass im Assember eine einzige MOV-Operation verwendet wird. Das legt schon einmal die maximale Größe der Daten, die ohne Synchronisation zu übermitteln sind, auf die maximale Registergröße fest (Voraussetzung ist bei allem, dass die Adresse, an der die Daten stehen, bekannt und unveränderlich ist). Also ein NativeInt (32/64 Bit je nach Architektur). Prinzipiell gibt es auch die Möglichkeit - je nach CPU-Befehlssatz -, das noch gewaltig aufzumöbeln. Denn seit mittlerweile vielen Jahren gibt es Vektor-Register, die wesentlich größer sind.
      Exkurs: Für das Rechnen mit Gleitkommazahlen gibt es mehrere Möglichkeiten. Es gibt den alten Float-Stack, der hat den Vorteil, dass eine Gleitkommazahl bis zu 80 Bit verwendet wird (Extended-Precision). Außerdem gibt es für sehr viele mathematische Operationen vordefinierte CPU-Befehle (was nicht unbedingt heißt, dass so ein Befehl schneller wäre als eine gute Bibliothek). Er ist also eine rechte Mühe für Prozessorhersteller, wird aber bisher noch in jedem Prozessormodell unterstützt. Die andere Möglichkeit ist dann der SSE-Befehlssatz. Ein SSE-Register (Bezeichnung: XMM) ist 128 Bit breit - das heißt aber nicht, dass wir "super-Extended Precision" haben, sondern dass man mit zwei Doubles gleichzeitig rechnen kann. Wenn heutzutage irgendein Compiler eine Gleitkommaoperation durchführt, dann läuft das normalerweise so ab, dass er die Zahl in die untere Hälfte des XMM-Registers lädt und dann irgendeine Operation durchführt. Was gleichzeitig mit der oberen Hälfte passiert, wird einfach ignoriert. Im Grund genommen verschenkt man damit die Möglichkeit, gleichzeitig eine zweite Operation durchzuführen. Und natürlich verschenkt man 16 Bit Präzision, die man haben könnte, wenn man den alten Float-Befehlssatz verwendet. Das ist aber Standard heutzutage. Exkurs Ende.
      Nun haben wir also auf jeden Fall die XMM-Register, die man eigentlich bei jedem Prozessor voraussetzen kann. Das macht 128 Bit, die man "in einem Rutsch" laden kann, ohne dass etwas dazwischen funkt1 (das musst du natürlich mit Assembler machen, meines Wissens kann man das Delphi nicht direkt sagen. Aber der Inline-Assembler sollte das können.).
      Dann gibt es AVX, eine neuere Erweiterung (~ 2011), die die YMM-Register bereitstellt. Die haben 256 Bit Breite. Bei neueren Rechnern kann man auch die voraussetzen. Aber, wichtig: Auch das Betriebssystem muss die unterstützen, sonst geht der Registerinhalt bei einem Kontextwechsel flöten (ab Win7, Server 2008 SP2, Linux Kernel 2.6.30). Und ich meine mich zu erinnern, dass man im 32-Bit Modus Probleme bekommen könnte, eventuell ist da nur die Hälfte der Register verfügbar. Die wichtigste Einschränkung: Delphi unterstützt AVX nicht. Nicht einmal in der neuesten Version. Das ist eine ziemliche Schande, weil es dem High-Performance-Computing mit Delphi ernste Grenzen setzt. Aber nur weil Delphi es nicht nativ unterstützt, heißt das nicht, dass es unmöglich wäre. Die unangenehme Methode wäre, die Bytecodes im Assembler zu emittieren, die angenehme Methode, sich einen externen Assembler zu nehmen und die OBJ-Datei einzubinden. Damit ist man von allen Delphi-Einschränkungen befreit (außer natürlich, dass du im 64-Bit-Modus compilieren solltest. Hast du nichts neueres als Delphi 7?).
      Und schließlich gibt es AVX512, das ist noch fast brandneu. Ich habe mir im März ein neues Notebook gekauft, das unterstützt diesen Befehlssatz noch nicht. Die neuesten Skylake und Knights Landing von Intel haben die. Mit der könntest du theoretisch 512 Bits auf einmal laden.
      Alles, was darüber hinausgeht, benötigt mehrere Operationen. Also kann quasi "während des Lesens" der Inhalt geändert werden und damit inkonsistente Daten erzeugen.
    2. Nein, ein Formular ist überhaupt kein guter Ort für Daten. Dafür bietet sich eine extra Unit an, die nur die Spezifikation der Daten enthält. Das macht das Einbinden und vermeiden von zyklischen Referenzen auch leichter.
      Es gibt ja auch noch die DataModules, aber auch wenn die vom Namen her für so etwas gedacht sind, würde ich sie eher als Sammelort von Datenbankkomponenten sehen. Deine Datenstruktur wirst du vermutlich nicht in eine Komponente verpacken, oder?
    3. s.o. Synchronisation hat nichts mit GUI zu tun, sondern mit Datenkonsistenz. Du musst die GUI an sich überhaupt nicht synchronisieren. Denn das "Graphical" wird ja schließlich von einem einzigen Thread gerendert, der die Message-Queue abhört und zeichnet und sich um alles kümmert. Wenn du irgendetwas am Zustand deines Programms änderst, was ein Neuzeichnen der GUI bewirken soll, dann rufe einfach anstatt Repaint immer nur Invalidate auf. Invalidate sorgt dafür, dass beim nächsten Abarbeiten der Queue der verantwortliche Thread das neuzeichnet, was neuzuzueichnen ist.
      Eine andere Frage ist, wie man Eigenschaften von Komponenten in anderen Threads verändert (wenn man der Meinung ist, dass man das wirklich tun muss - ein Rechenthread sollte eigentlich nichts von der GUI-Struktur wissen). Das lässt sich pauschal nicht beantworten und muss mit einem Blick in den Quellcode der Komponente geprüft werden. Wenn das Setzen einfach nur ein Kopieren eines primitiven Typs an eine andere Speicherstelle gefolgt von Invalidate ist, dann gibt - aus SIcht des Setters - es keinen Grund zur Synchronisation. Wenn mehr Berechnung angestellt wird, dann schon.
      Aber der Setter allein zählt nicht, denn auch das Lesen ist durchaus kritisch: Wenn ich etwa in die Control.pas schaue - wie wird da die Eigenschaft TControl.Text ausgelesen? Zunächst wird GetTextLen aufgerufen, was die Länge des Textes mithilfe einer Message ermittelt (der Text wird also gar nicht von Delphi gespeichert, sondern von Windows über das Handle!). Dann wird ein Buffer von der entsprechenden Größe allokiert, und dieser wird im Anschluss mittels GetTextBuf gefüllt. Dumm nur, wenn zwischen dem Lesen der Länge und dem Lesen des eigentlichen Texts ein anderer Thread (das kann auch ein externes Programm sein, etwa WinSpy oder EDA von Assarbad, was ich gerne verwende) den Text ändert. Das produziert kein Speicherleck oder so, aber ggf. wird der Text eben nicht vollständig ausgelesen (oder aber mit einem Null-Zeichen mitten im String, was für einen Delphi-String die falsche Darstellung ist).
      Wegen dieser ganzen Seiteneffekte ist man normalerweise am sichersten beraten, alles, was die GUI angeht, von einem einzigen Thread erledigen zu lassen. Weiterer Vorteil: Die GUI hängt niemals, wenn alle Berechnungen in anderen Threads ausgeführt werden.
    4. Betrifft die nicht-Frage "Der Würkerthread läuft nur einmal durch und wird bei Bedarf neu erzeugt."
      Das Erstellen eines Threads ist teuer. Denke ggf. über den Einsatz eines Thread-Pools bzw. einer Thread-Bibliothek (z.B. OmniThreadLibrary) nach, die dir das Neuerstellen von Threads erspart.

    1 Na ja, eigentlich erst seit SSE2 (seit 2001), die allerersten Implementierungen haben eine 128-Bit-Anweisung intern in zwei 64-Bit-Anweisungen zerlegt. Aber die hatten auch noch keine mehreren Kerne, also würde die Anweisung auf jedem Fall trotzdem in einem Rutsch ausgeführt (das Betriebssystem kann ja schließlich einen Kontextwechsel nur nach "gewöhnlichen" Anweisungen ausführen).
    Master of the EDH ;)
    Hallo,

    danke für die erschöpfende Antwort.
    Irgendwo habe ich noch ein paar Grundsatzprobleme im Verständnis.
    Das Ganze findet zur Zeit in Freepascal unter Raspbian statt. Nun ist der Pi ziemlich schwach auf der Brust.
    Ich habe bis jetzt Synchronisationen mit Synchronize (@Prozedur) gemacht, das kommt mir aber etwas überdimansioniert vor...

    ism
    Morgen ist Heute schon Gestern
    Pfff... Das ändert natürlich einiges. Zum ersten habe ich keine Ahnung vom Raspberry und kann deshalb kaum etwas dazu sagen. Aber Wiki sagt mir, dass es inzwischen Version 3 gibt und die natürlich im verwendeten Prozessor auch Fortschritte gemacht haben. Der RPI3 verfügt wohl über NEON SIMD-Instructions und kann damit bis zu 128 Bit gleichzeitig laden. Wie man NEON verwendet, steht wohl im Guide, der aber nur für registrierte ARM-Kunden zugänglich ist. Vielleicht kann man sich bei OpenSource-Software was abgucken.
    Die grundsätzliche Botschaft ändert sich aber nicht:
    • Wie viele Bits Daten sind in einem Datenpaket, das der Workertherad lesen will?
    • Synchronize ist die einfache und schnelle Lösung, aber nicht immer optimal. Gehen wir davon aus, dass deine Daten zu groß sind, um sie in einem Rutsch zu lesen. Dann heißt das nicht unbedingt, dass jeglicher Zugriff nur in einem Synchronize stattfinden darf. Manches geht auch ohne oder mit leichtgewichtigerer Synchronisierung, vielleicht unter Verwendung von Messages, .... Aber das ist wirklich sehr problemspezifisch. Das heißt, um weiterzuhelfen musst du mal die genaue Datenstruktur posten, wie wird auf diese lesend/schreibend zugegriffen (auf welche Teile davon evtl. nur); was passiert, wenn neue Daten hinzukommen; wenn alte Daten entfernt werden (falls das vorkommt - wenn etwa der Buffer nicht ausreicht).
    • Und schließlich ist die Frage, ob es so viele Threads überhaupt bringen. Auch wenn der Prozessor mehrere Kerne hat, ist es nicht unbedingt vorteilhaft, mehr Threads als Kerne zu erstellen. Hältst du dich an diese Obergrenze? Sonst könntest du auch überlegen, das OnIdle-Ereignis zu nutzen manches dorthin auszulagern.
    Master of the EDH ;)
    Hallo,

    ich habe drei Lauschthreads, die je eine COM abfragen.
    Davon stellen zwei je einen Transpondercode (Reader) zusammen und speichern in einem eigenen Objekt einen String (10 Zeichen);
    Der dritte Lauscher hört eine Schrittmotorkarte ab und speichert einen Int (enthält 4 Bit für Endlagenschalter) sowie den Zustand eines Sensors, welcher über eine Handshake - Leitung abgefragt wird (1 Bit).
    Die Lauschthreads schreiben ihre Daten ungebremst in ihre Objekte.
    Der Wörkerthread soll auf allen 3 Objekten lesen.
    Seitdem ich das synchronisiert mache knallts hier nicht mehr.
    Trotzdem mußte ich schon bei sen grafischen Indikatoren sehr abspecken, eigentlich soll jeder Lauschthread bei jedem Durchlauf einen Indikator bont färben (nat. Synchroisiert).
    Kannst Du mir Lektüre empfehlen, mit der ich meine Wissenslücken füllen kann ?
    Dieses Rumgestümper kotzt mich selber langsam an...
    Gruß ism
    Morgen ist Heute schon Gestern
    Lektüreempfehlung habe ich leider keine; ich habe mir das über Jahre hinweg auch durch Probieren und die verschiedensten Beiträge in Foren, Tutorials, der Hilfe, RTL-Quellcode, ... angelesen. Für die Geschichte was die Nutzung der SIMD (Single Instruction, Multiple Data, d.h. SSE, AVX, ...) angeht, verwende ich den Intel Intrinsics Guide, schaue mir da das Assembler-Kommando an und google evtl. noch nach der nähere Syntax, falls die nicht selbsterklärend ist. Weiter ist die Optimizing Manual (Parts 1 & 2) von Agner Fog eine gute Referenz (beachte aber wie gesagt, dass das mit Delphi so leicht nicht geht), allerdings geht die ein bisschen in eine andere Richtung als deine Frage.

    Den dritten Thread, der nur 5 Bits schreibt, musst du nicht synchronisieren, sofern sich diese fünf Bits einen Integer teilen. Das Schreiben geht auf jeden Fall in einem Zug und es kann kann kein inkonsistenter Zustand auftauchen. Da die beiden anderen Threads entkoppelt sind vom dritten, gehst du wohl eh kaum davon aus, dass die Strings mit den anderen fünf Bits in irgendeinem gemeinsamen Zustand sind.
    Die anderen beiden Threads schreiben 10 Zeichen, also gehe ich mal von 10 Bytes aus. Das sind auch auf einem 64-Bit-System zwei Bytes mehr, als ohne Vektorregister in einem Rutsch gehen. Das heißt, wenn du keine Vektorregister verwendest, müsstest du synchronisieren.
    Andererseits hast du mit Vektoren bis zu 16 Bytes, die du gleichzeitig schreiben kannst. Vielleicht mal ein kleines Codebeispiel für Windows, lauffähig unter D7:

    Delphi-Code

    1. unit Unit1;
    2. interface
    3. uses
    4. Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
    5. Dialogs, StdCtrls, Unit2, ExtCtrls;
    6. type
    7. TMyData = Packed Array[0..9] Of Byte;
    8. TWorker = class(TThread)
    9. protected
    10. procedure Execute; override;
    11. end;
    12. TForm1 = class(TForm)
    13. Timer1: TTimer;
    14. ListBox1: TListBox;
    15. procedure FormCreate(Sender: TObject);
    16. procedure FormDestroy(Sender: TObject);
    17. procedure Timer1Timer(Sender: TObject);
    18. private
    19. { Private-Deklarationen }
    20. public
    21. { Public-Deklarationen }
    22. WT: TWorker;
    23. end;
    24. var
    25. Form1: TForm1;
    26. GlobalData: TMyData;
    27. implementation
    28. {$R *.dfm}
    29. procedure TForm1.FormCreate(Sender: TObject);
    30. begin
    31. WT := TWorker.Create(True);
    32. WT.FreeOnTerminate := True;
    33. WT.Resume;
    34. end;
    35. procedure TForm1.FormDestroy(Sender: TObject);
    36. begin
    37. WT.Terminate;
    38. end;
    39. procedure TForm1.Timer1Timer(Sender: TObject);
    40. var
    41. I: Integer;
    42. ThreadData: TMyData;
    43. D: Byte;
    44. begin
    45. ThreadData := GlobalData;
    46. D := ThreadData[0];
    47. For I := 1 To 9 Do
    48. If ThreadData[I] <> D Then
    49. ListBox1.Items.Add('Fail');
    50. end;
    51. { Worker }
    52. procedure TWorker.Execute;
    53. var
    54. ThreadData: TMyData;
    55. I: Byte;
    56. begin
    57. I := 0;
    58. while Not Terminated do begin
    59. FillChar(ThreadData, SizeOf(ThreadData), Ord(I));
    60. Inc(I);
    61. GlobalData := ThreadData;
    62. end;
    63. end;
    64. end.
    Master of the EDH ;)
    Der Timer hat ein Intervall von 10. Bei mir füllt sich die ListBox sehr rasch mit einer Menge Fails. Ich habe eigentlich alles richtig gemacht - im Timer-Event kopiere ich zunächst die globalen Daten, die vom Thread beschrieben werden, in ein lokales Array, um einen konsistenten Zustand sicherzustellen. Auch im Thread arbeite ich bis zum Schluss auf einem lokalen Array und kopiere erst zum Schluss. Aber der Disassembler enthüllt, was schief geht: z.B. GlobalData := ThreadData im Thread wird wie folgt bei mir übersetzt:

    Quellcode

    1. mov eax, [$00452154]
    2. mov edx, [esp]
    3. mov [eax], edx
    4. mov edx, [esp+$04]
    5. mov [eax+$04],edx
    6. mov dx,[esp+$08]
    7. mov [eax+$08],dx

    Ich gebe mal einen "Pseudopascalcode" (Delphi wird die Typen bemängeln) dazu, was das hier bedeutet. Die Register enthalten grundsätzlich Pointer und sind 32 Bit breit:

    Delphi-Code

    1. var
    2. EAX, EDX, ESP: Pointer;
    3. begin
    4. ESP := @ThreadData; // das ist implizit, weil ESP auf die Stack-Variable zeigt
    5. EAX := @GlobalData; // mov eax, [$00452154]
    6. EDX := ESP^; // mov edx, [esp] - hier wird EDX mit den ersten vier Bytes aus ThreadData gefüllt
    7. EAX^ := EDX; // mov [eax], edx - kopiere diese ersten vier Bytes also an die Stelle der ersten vier Bytes von GlobalData ***
    8. EDX := (ESP +4)^; // mov edx, [esp+$04] - fülle EDX mit den nächsten vier Bytes aus ThreadData
    9. (EAX +4)^ := EDX; // mov [eax+$04],edx - kopiere diese zweiten vier Bytes an die Stelle der zweiten vier Bytes von GlobalData ***
    10. EDX := Word(ESP +8)^; // mov dx,[esp+$08] - fülle die letzten zwei Bytes aus ThreadData nach DX (habe ich also nicht ganz richtig transkribiert,
    11. // DX sind die unteren zwei Bytes von EDX, und die oberen würden nicht geändert).
    12. Word(EAX +8)^ := EDX; // mov [eax+$08],dx - kopiere die letzten zwei Bytes nach GlobalData

    Die Stellen, die ich mit *** markiert habe, sind kritisch. In diesem Moment ist GlobalData nämlich in einem inkonsistenten Zustand.
    Auf einem 64-Bit-Rechner würden die ersten beiden Operationen zusammengefasst, weil dort 64-Bit breite Register zur Verfügung stehen. Es verbleiben aber immer noch die fehlenden zwei Bytes.
    Jetzt das ganze mit Vektorregistern. Wie gesagt, ich weiß nicht, wie du die auf dem Raspberry ansprichst, aber dort gibt es sie auch. Zunächst mal ist wichtig, dass dein Vektorregister 2x64 Bytes groß ist. Wenn du daraus liest, wirst du also immer 128 Bits lesen und musst entsprechenden Platz bereithalten. Das heißt, dass dein Array größer werden muss:

    Delphi-Code

    1. Packed Array[0..15] Of Byte

    Alternativ könntest du auch, um klar zu machen, dass es sich hier um reservierten Platz handelt,

    Delphi-Code

    1. TMyData = Packed Record
    2. Data: Packed Array[0..9] Of Byte;
    3. Reserved: Packed Array[10..15] Of Byte;
    4. End;

    schreiben. Ich verwende immer das Schlüsselwort "Packed", um absolut sicherzugehen, dass der Compiler nicht mit irgendwelchen Alignment-Einstellungen dazwischenpfuscht. Du kannst dich natürlich auch auf die {$A}-Anweisung von Delphi verlassen, dann würde {$A8} reichen (ohne packed natürlich) - aber ich bevorzuge es, Sachen, die alternativlos sind auch so zu fordern, dass der Compiler nicht anders kann.
    Als nächstes muss ich zwei Routinen ändern: Das Schreiben des Threads in die Daten wird zu

    Delphi-Code

    1. procedure TWorker.Execute;
    2. var
    3. ThreadData: TMyData;
    4. I: Byte;
    5. begin
    6. I := 0;
    7. while Not Terminated do begin
    8. FillChar(ThreadData, SizeOf(ThreadData), Ord(I));
    9. Inc(I);
    10. asm
    11. movups xmm0, dqword ptr ThreadData
    12. movups dqword ptr GlobalData, xmm0
    13. end;
    14. end;
    15. end;

    und das Lesen wird zu

    Delphi-Code

    1. procedure TForm1.Timer1Timer(Sender: TObject);
    2. var
    3. I: Integer;
    4. ThreadData: TMyData;
    5. D: Byte;
    6. begin
    7. asm
    8. movups xmm0, dqword ptr GlobalData
    9. movups dqword ptr ThreadData, xmm0
    10. end;
    11. D := ThreadData[0];
    12. For I := 1 To 9 Do
    13. If ThreadData[i] <> D Then
    14. ListBox1.Items.Add('Fail');
    15. end;

    Wenn du jetzt das Programm laufen lässt, wird es keinerlei Fails mehr geben. Ich habe nichts synchronisiert, im Assembler ist der Code sogar kürzer und effizienter geworden (der Intrinsics-Guide verrät, dass movups eine Latenz von 1 hat). So einfach kann es gehen, wenn man sich die Architekturfeatures zunutze macht. Und wie gesagt, das compiliert mit D7 und läuft auch auf Rechnern, die zehn Jahre als sind.
    Was tue ich? movups ist die Anweisung dafür, einen Vektor von single-Precision-Werten, die nicht ausgerichtet sind, zwischen einem Vektorregister und dem Speicher hin- und herzuschaufeln. Der erste Parameter ist das Ziel, der zweite der Ursprung. xmm0 ist der Name des Vektorregisters. Ich kann nach Belieben nach xmm0 schreiben, weil das ein volatile-Register ist, d.h. die Aufrufkonvention sagt mir nicht, dass ich dieses Register nicht verändern darf (oder zumindest nach dem Verändern wiederherstellen müsste).
    Dann schiebe ich einen Single-Precision-Vektor hin- und her. Das wäre für Delphi folgender Typ:

    Delphi-Code

    1. Packed Array[0..3] Of Single;

    Meine Daten sind aber überhaupt keine Singles, sondern zusammenhanglose Bytes. Das spielt keine Rolle, denn die CPU führt keinen Test durch, ob die Werte tatsächlich gültig sind. Sie kopiert einfach nur das Bitmuster - und vier Singles haben nun mal 128 Bit, genauso wie 12 Bytes. Es gibt auch eine Anweisung movupd, die zwei Doubles kopiert. Da mich das alles nicht interessiert, verwende ich die Anweisung, die das kürzeste Bitmuster enthält, und das ist die für Singles. movupd wäre ein Byte länger. (Solche Tricks findet man in der oben erwähnten Optimization Manual).
    Alternativ - und semantisch eigentlich am sinnvollsten - wäre der movdqu-Befehl, der nämlich einen Integer-Vektor kopiert. Aber auch der ist ein Byte länger, als der Single-Befehl. (Kürzer heißt nicht unbedingt besser, manchmal wenn es um die Ausrichtung des Codes geht, ist man auch froh, wenn man eine Anweisung künstlich um ein Byte verlängern kann.) Und abgesehen davon wird der nicht schon vom SSE-Befehlssatz unterstützt, sondern braucht SSE2. Damit schließt man also ältere Rechner aus. Delphi 7 kennt den Befehl aber bereits.
    Dann gäbe es noch eine schnellere Varianten als movups, nämlich movaps. Aber movaps verlässt sich darauf, dass der übergebene Pointer an einer 16-Byte-Grenze ausgerichtet ist (das ist im Intrisics Guide beschrieben). Das ist aber unter 32 Bit nicht gewährleistet, weil der Stack nur auf 4 Bytes ausgerichtet ist. In einer 64-Bit-Anwendung könnte man diese Anweisung verwenden, weil laut Aufrufkonvention der Stack - und damit, wenn man sie richtig positioniert, die lokale Variable - an 16 Bytes ausgerichtet sein muss.
    Schließlich steht da noch "dqword ptr". Das ist der Intel-Assembler-Syntax geschuldet, die Delphi verwendet. Assembler ist im Prinzip untypisiert, weil alles nur Bitmuster sind. Aber so ein "bisschen" Typtreue bringt die Syntax hinein: Du musst dem Assembler sagen, dass GlobalData bzw. ThreadData hier verwendet werden soll als ein Pointer of einen double-quad-word (d.h. 2 x 4 x 16 = 128 Bit). Du wirst intuitiv vielleicht erwarten, dass du @GlobalData verwenden musst, weil ja schließlich die Adresse von GlobalData gefragt wird. Aber im Assembler setzt Delphi standardmäßig für alle Variablen deren Adresse ein. Wenn du den Inhalt haben willst, musst du explizit [GlobalData] schreiben.

    Bei jedem Durchlauf etwas färben - da sehe ich keinen Grund zur Synchronisation. Mit Farben werden keine großartigen Berechnungen angestellt, also würde ich sogar eine Zuweisung der Farbe in einem anderen Thread als unproblematisch ansehen. Neuzeichnen dann über Invalidate.
    Aber wenn du die GUI schön getrennt von der Datenverarbeitung halten willst, dann würde ich mit Messages arbeiten: Der Thread sendet ein PostMessage, sobald er die Daten aktualisiert hat und ein Neuzeichnen wünscht, und der GUI-Thread reagiert entsprechend auf diese Message. Synchronisation quasi for free.

    Nur falls du jetzt bei meiner Erwähnung von Messages auf die Idee kommst, dass der Thread ja der Message als Parameter die Daten übergeben könnte und du dir so Assembler ersparen könntest: Das funktioniert nicht für 10 Bytes. Denn du kannst maximal acht Bytes als Parameter übergeben (wobei du das mit Trickserei des Message-Codes vielleicht sogar noch erweitern kannst). Das heißt, dass du dynamisch Speicherplatz im Thread anfordern müsstest; der enthält die Daten; der GUI-Thread bekommt die Message mit einem Pointer auf die Daten, verarbeitet sie und gibt sie frei. Das funktioniert, ist aber unschön und auch ziemlich langsam, weil dynamische Speicherallokation relativ kostspielig ist.
    Schließlich: Du könntest statt PostMessage auch SendMessage verwenden. Dann wartet der Worker-Thread, bis der GUI-Thread die Nachricht bearbeitet hat. Das erspart dir natürlich auch die Synchronisation, weil keine neuen Daten reinkommen können, bevor die alten nicht abgearbeitet wurden. Aber dadurch verlierst du die Asynchronität des Threads und bist vom Flaschenhals der GUI abhängig.
    Ganz abschließend: Wenn extrem viele Messages rausgehen (was das auf einem Raspberry bedeutet, weiß ich nicht, auf einem PC wären das ein paar Tausend bis Zehntausend die Sekunde), dann wird die Message-Queue überfüllt und du legst dein Programm lahm. In einem solchen Fall soll lieber die GUI in regelmäßigen Intervallen auf neue Daten prüfen.


    @Thomas: Die 10000 Zeichen sind ziemlich lästig...
    Master of the EDH ;)

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „projekter“ ()