Forum: Mikrocontroller und Digitale Elektronik Double core: Mutex nötig oder nicht?


von Fritz G. (fritz65)


Lesenswert?

Ich habe einen ESP32 mit zwei Kernen, den ich zum Erfassen von 
Sensordaten nutze. Für jeden Sensor gibt es einen Task der auf das 
Eintreffen wartet und den Wert in einer globalen 32bit Variablen ablegt. 
Ein anderer Task liest die Sensorwerte periodisch aus. Normalerweise 
müsste der Zugriff auf 32bit-Variablen atomar sein, denn ein Core kann 
ja nicht gleichzeitig mehrfach auf das selbe Speicherwort zugreifen. 
Aber gilt das auch in einem Multiprozessorsytem? Muss ich die Zugriffe 
mittels Mutex schützen?

von Arduino F. (Firma: Gast) (arduinof)


Lesenswert?

Fritz G. schrieb:
> Muss ich die Zugriffe
> mittels Mutex schützen?
Ja, da musst du etwas Sorgfalt walten lassen.

FreeRTOS dürfte auch für dich die passende Queue, oder so, haben.
Auch den ganzen anderen Kram, den man für Multicore/Multitasking 
Angelegenheiten braucht.

: Bearbeitet durch User
von Sebastian W. (wangnick)


Lesenswert?

Fritz G. schrieb:
> Muss ich die Zugriffe mittels Mutex schützen?

Ich glaube nein. Auf dem ESP32 sind anscheinend nicht nur 32-Bit 
Registeroperationen atomar, sondern auch 32-Bit Speicheroperationen. 
Aber ich kann mich irren.

LG, Sebastian

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Das _Atomic / std::atomic API hilft auch bei sowas. Im Endeffekt genau 
wie beim PC.

von Arduino F. (Firma: Gast) (arduinof)


Lesenswert?

Sebastian W. schrieb:
> sondern auch 32-Bit Speicheroperationen.

Sind sie!
Aber eine Memory Barrier oder volatile wird dennoch nötig sein, damit 
das Datum auch wirklich im RAM landet, bzw. daraus gelesen werden kann.

von Foobar (asdfasd)


Lesenswert?

Ich kenne den ESP32 nicht genauer.  Im Linux-Kernel werden zur 
Synchronisation von echt parallelen Threads (i.e. 
Hyper-Threading/Multi-Core) Spinlocks eingesetzt.  Die wiederum werden 
zur Implementation von Atomics und/oder Mutexes eingesetzt.  Man darf 
nicht vergessen, dass der Speicher bei solchen Systemen eine relativ 
asynchron angeschlossene Peripherie ist (so muß ja z.B. beim Schreiben 
ins RAM der dazugehörige Cache-Eintrag in allen anderen Cores gelöscht 
werden, etc) und da gibt es viele Fallstricke und sie sind alle extrem 
CPU-spezifisch.  Da es nicht "den" ESP32 gibt, sondern viele 
verschiedene Modelle mit unterschiedlichen CPUs, ist das ein Minenfeld.

Wie arduinof schon schrieb, wenn du schon Multithreading machen willst, 
benutzt die Mittel des Betriebssystems.  Das selbst zu 
implementieren/optimieren ist Strong Stuff© und nichts für jemanden, der 
fragt, ob er Mutexe benutzen muß.

Die nächste Frage wäre, ob du wirklich Multithreading brauchst?  Anfangs 
scheint es die Sache sehr einfach zu machen, erst später merkt: mit 
Multithreading fängt man sich auch eine Latte an Problemen ein, die man 
single threaded gar nicht hätte.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Foobar schrieb:
> Die nächste Frage wäre, ob du wirklich Multithreading brauchst?

Beim ESP32 unumgänglich, das IDF setzt FreeRTOS zwingend voraus.

Foobar schrieb:
> Das selbst zu implementieren/optimieren ist Strong Stuff©

Tja, und die existierende Implementation von FreeRTOS ist auch teilweise 
buggy...

Praktischerweise hat das ESP32 FreeRTOS eine sehr nützliche RingBuffer 
Implementation mitgeliefert.

von Ob S. (Firma: 1984now) (observer)


Lesenswert?

Fritz G. schrieb:
> Ich habe einen ESP32 mit zwei Kernen, den ich zum Erfassen von
> Sensordaten nutze. Für jeden Sensor gibt es einen Task der auf das
> Eintreffen wartet und den Wert in einer globalen 32bit Variablen ablegt.

Also eine Variable pro Sensor, richtig?

> Ein anderer Task liest die Sensorwerte periodisch aus.

Woher weiß dieser Task, ob und welcher Sensorwert gültig bzw. aktuell 
ist? Woher wissen die Sensortasks, dass der Sammlertask den letzten Wert 
gelesen hat und es demzufolge sinnvoll wäre, den nächsten zu lesen?

Das ist der eigentliche Knackpunkt bei der Synchronisation.

> Normalerweise
> müsste der Zugriff auf 32bit-Variablen atomar sein

Ist er. Aber nicht unbedingt kohärent. Bedeutet: wenn wenigstens einmal 
ein gültiges Datum in den Variablen abgelegt wurde, wirst du auch ein 
gültiges Datum lesen. Aber nicht unbedingt das aktuelle...

von Fritz G. (fritz65)


Lesenswert?

Ob S. schrieb:
> Also eine Variable pro Sensor, richtig?
Genau, jeder Sensor hat seine eigene Variable.
>> Ein anderer Task liest die Sensorwerte periodisch aus.
>
> Woher weiß dieser Task, ob und welcher Sensorwert gültig bzw. aktuell
> ist? Woher wissen die Sensortasks, dass der Sammlertask den letzten Wert
> gelesen hat und es demzufolge sinnvoll wäre, den nächsten zu lesen?

Meine ursprüngliche Idee war, jedem Sensor ein Event-Flag zuzuordnen, 
die alle zur gleichen Event-Group gehören. Trudelt ein Messwert ein, 
wird das Flag gesetzt. Der Auswerte-Task wartet dann darauf, dass ein 
Bit der Gruppe auf 1 gesetzt wird (geht mit xEventGroupWaitBits(), wobei 
xWaitForAllBits auf false gesetzt ist). dann liest er den neuen 
Sensorwert und setzt das bit zurück.
Alternativ könnte man das Eventbit in der Sensorvariablen selbst 
unterbringen - für die Sensorwerte brauche ich nicht die vollen 32bit. 
Allerdings gibt es dann nicht eine so schöne Wartefunktion und der 
Auswertetask müsste periodisch die Bits pollen.

>> Normalerweise
>> müsste der Zugriff auf 32bit-Variablen atomar sein
>
> Ist er. Aber nicht unbedingt kohärent. Bedeutet: wenn wenigstens einmal
> ein gültiges Datum in den Variablen abgelegt wurde, wirst du auch ein
> gültiges Datum lesen. Aber nicht unbedingt das aktuelle...

Ein Problem hätte ich dann, wenn ein Core einen neuen Sensorwert in 
seinen lokalen Cache schreibt, der andere in seinem Cache aber noch den 
alten Wert hat. Kann man das Writethrough irgendwie erzwingen?
Sonst käme nur die Signalisierung mittels Eventbit in der 
Sensorvariablen in Frage, mit dem oben erwähnten Nachteil.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Fritz G. schrieb:
> Kann man das Writethrough irgendwie erzwingen?

Laut C/C++ Standards geschieht das automatisch wenn man einen Mutex 
freigibt, ein Atomic beschreibt etc. D.h. die normalen Schutzmechanismen 
bewirken das automatisch. Es gibt aber auch explizite Memory Fence 
Operationen, die sind aber normalerweise nicht nötig.

Allerdings haben die meisten Multicore-Prozessoren mit Cache auch 
integrierte Cache Kohärenz, sodass das gar nicht nötig ist (x86, und die 
normalen ARM Cortex-A Multicores). Es kann allerdings ARM-Prozessoren 
geben die das nicht haben, weiß aber nicht ob es sowas in der echten 
welt gibt. Bei Nutzung von DMA o.ä., was nicht auf die Cache Kohärenz 
zugreifen kann, braucht man aber die Cache Maintenance Operationen um 
den Cache zu leeren (DCCMVAC etc).

Allerdings: Der ESP32 hat überhaupt keinen Cache. Ich würde trotzdem 
immer einen Synchronisationsmechanismus wie eben Atomics nutzen, das 
macht es auch portabel und vermeidet Überraschungen.

: Bearbeitet durch User
von Andreas M. (amesser)


Lesenswert?

Fritz G. schrieb:
> Normalerweise
> müsste der Zugriff auf 32bit-Variablen atomar sein,

Ja, das ist normalerweise auch bei mehreren Cores so, sonst wäre es gar 
nicht möglich so Dinge wie Spinlocks zu implementieren. Im Prinzip kommt 
es auf die Breite der RAM Anbindung an welche Zugriffe bei Multicore 
atomar sind.

Fritz G. schrieb:
> Meine ursprüngliche Idee war, jedem Sensor ein Event-Flag zuzuordnen,
> die alle zur gleichen Event-Group gehören. Trudelt ein Messwert ein,
> wird das Flag gesetzt. Der Auswerte-Task wartet dann darauf, dass ein
> Bit der Gruppe auf 1 gesetzt wird (geht mit xEventGroupWaitBits(), wobei
> xWaitForAllBits auf false gesetzt ist). dann liest er den neuen
> Sensorwert und setzt das bit zurück.
> Alternativ könnte man das Eventbit in der Sensorvariablen selbst
> unterbringen - für die Sensorwerte brauche ich nicht die vollen 32bit.
> Allerdings gibt es dann nicht eine so schöne Wartefunktion und der
> Auswertetask müsste periodisch die Bits pollen.

Kann man so machen, wenn Dich immer nur der letzte Sensorwert 
interessiert.

von A. B. (funky)


Lesenswert?

Jeder Task schreibt in die selbe Queue und ein Task liest aus dieser 
Queue und wertet aus.

am besten die Daten in ein Struct und noch eine ID dazu

von John P. (brushlesspower)


Lesenswert?

was passiert denn wenn man das alles nicht beachtet und mit beiden 
kernen auf eine variable zugreift?

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

John P. schrieb:
> was passiert denn wenn man das alles nicht beachtet und mit beiden
> kernen auf eine variable zugreift?

https://www.google.com/search?q=race+condition 😉

von Norbert (der_norbert)


Lesenswert?

John P. schrieb:
> was passiert denn wenn man das alles nicht beachtet und mit beiden
> kernen auf eine variable zugreift?

Bei einem zwei-Kern RP2040 passiert gar nichts (also schon, aber nichts 
Schlechtes)

Auf der technischen Ebene (bei einem und nur einem 32bit Zugriff):

Das interne RAM ist wird nicht durch einen cache angesprochen.
Das heißt der arbiter der AHB-Lite crossbar regelt den Zugriff nach 
Proritäten. Sollten wirklich beide Kerne präzise zum gleichen Zeitpunkt 
zugreifen, dann bekommt der Kern mit der höheren Priorität den 
sofortigen Zugriff. Der Andere wird um einen einzelnen Zyklus angehalten 
und dann versorgt.
Der gelesene/geschriebene Wert ist immer und zu jeder Zeit gültig.

Aber, da können bei einem präzise synchronen Zugriff durchaus mal 8ns 
Wartezeit anfallen (beim Standard-Takt).

PS. Es würde mich wundern wenn es bei STM32 und anderen Mikrocontrollern 
anders wäre. Da muss man das jeweilige Datenblatt studieren.

: Bearbeitet durch User
von Philipp Klaus K. (pkk)


Lesenswert?

Fritz G. schrieb:
> Ich habe einen ESP32 mit zwei Kernen, den ich zum Erfassen von
> Sensordaten nutze. Für jeden Sensor gibt es einen Task der auf das
> Eintreffen wartet und den Wert in einer globalen 32bit Variablen ablegt.
> Ein anderer Task liest die Sensorwerte periodisch aus. Normalerweise
> müsste der Zugriff auf 32bit-Variablen atomar sein, denn ein Core kann
> ja nicht gleichzeitig mehrfach auf das selbe Speicherwort zugreifen.
> Aber gilt das auch in einem Multiprozessorsytem? Muss ich die Zugriffe
> mittels Mutex schützen?

In der Annahme, das du in C, oder etwas ähnlichem programmierst:

Wenn du einen atomaren Zugriff willst, verwende _Atomic. Eine Mutex 
würde sicher auch funktionieren, aber _Atomic ist genau das, was du 
willst, und das kann der Compiler wahrscheinlich deutlich effizienter 
als die Mutex umsetzen (je nach Zielarchitektur).

: Bearbeitet durch User
von Arduino F. (Firma: Gast) (arduinof)


Lesenswert?

Norbert schrieb:
> Das interne RAM ist wird nicht durch einen cache angesprochen.

Das nicht!
Aber:
Die im C/C++ Compiler eingebaute Optimierung versucht Daten in den 
Registern zu halten. Es neigt nicht dazu diese Daten ins RAM zu 
schreiben, oder auch zu lesen, wenn/wann man sich das wünscht, sondern 
eher wenn es unvermeidlich ist.

von John P. (brushlesspower)


Lesenswert?

Norbert schrieb:
> John P. schrieb:
>> was passiert denn wenn man das alles nicht beachtet und mit beiden
>> kernen auf eine variable zugreift?
>
> Bei einem zwei-Kern RP2040 passiert gar nichts (also schon, aber nichts
> Schlechtes)
>
> Auf der technischen Ebene (bei einem und nur einem 32bit Zugriff):
>
> Das interne RAM ist wird nicht durch einen cache angesprochen.
> Das heißt der arbiter der AHB-Lite crossbar regelt den Zugriff nach
> Proritäten. Sollten wirklich beide Kerne präzise zum gleichen Zeitpunkt
> zugreifen, dann bekommt der Kern mit der höheren Priorität den
> sofortigen Zugriff. Der Andere wird um einen einzelnen Zyklus angehalten
> und dann versorgt.
> Der gelesene/geschriebene Wert ist immer und zu jeder Zeit gültig.
>
> Aber, da können bei einem präzise synchronen Zugriff durchaus mal 8ns
> Wartezeit anfallen (beim Standard-Takt).
>
> PS. Es würde mich wundern wenn es bei STM32 und anderen Mikrocontrollern
> anders wäre. Da muss man das jeweilige Datenblatt studieren.

Darauf wollte ich etwa hinaus.
Ich habe beim ESP32  beide Kerne genutzt uns lasse diese auf gemeinsame 
variablen zugreifen.
Ohne Mutex ohne atomic.
Und habe bisher noch keine Probleme dabei festgestellt.
Wenn mal ein Kern ein paar ns wartet ist es bei mir nicht schlimm

von Norbert (der_norbert)


Lesenswert?

Philipp Klaus K. schrieb:
> In der Annahme, das du in C, oder etwas ähnlichem programmierst:

Nein. Darum schrieb ich: Auf der technischen Ebene …
Es ging explizit nicht um irgendwelche höheren Programmiersprachen.

Das heißt Maschinencode, oder für diejenigen welche sich in den Besitz 
eines Assemblers gebracht haben, Assemblercode.

von Norbert (der_norbert)


Lesenswert?

John P. schrieb:
> Darauf wollte ich etwa hinaus.
> Ich habe beim ESP32  beide Kerne genutzt uns lasse diese auf gemeinsame
> variablen zugreifen.
> Ohne Mutex ohne atomic.
> Und habe bisher noch keine Probleme dabei festgestellt.

Ja genau, sobald der (Hardware)Buszugriff 32bit breit erfolgt, kann es 
ohne cache gar nicht anders sein. (Bei externem RAM jedoch durchaus 
möglich)

Das ESP32 Datasheet lässt da durchaus eine Menge zu wünschen übrig.

von Norbert (der_norbert)


Lesenswert?

Arduino F. schrieb:
> Die im C/C++ Compiler eingebaute Optimierung…

Schon klar. Wenn ich den µC in Cobol programmiere vermutlich auch noch 
schlimmer.

Darum schrieb ich explizit: Auf der technischen Ebene …

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.