Forum: Mikrocontroller und Digitale Elektronik Threadsichere, nicht-blockierende Variablen


von Walter T. (nicolas)


Lesenswert?

Guten Morgen,

es ist Freitagmorgen und ich habe gerade wieder eine Denkblockade bei 
einem Problem, das wohl im ersten Semesters eines Informatikstudiums 
verordnet sein sollte.

Ich will eine große, nicht-atomar änderbare Variable nicht-blockierend 
von einer langsamen Hauptschleife konsistent in eine schnelle ISR 
bringen.

Mein Ansatz:
1
/* Globale Variablen */
2
typedef struct InputBuffer_s
3
{
4
    int64_t nonAtomicVariable0;
5
    int64_t nonAtomicVariable1;
6
}
7
InputBuffer_t;
8
9
InputBuffer_t InputBuffer[2] = {[0] = {.nonAtomicVariable0 = 0, .nonAtomicVariable1 = 1}};
10
InputBuffer_t *ValidBuffer = &InputBuffer[0];
11
12
13
14
/* Funktionsdeklarationen */
15
void doSomeFastStuffWith(int64_t var);
16
void doOtherFastStuffWith(int64_t var);
17
int64_t doALittleBitSimplerCalculation(void);
18
int64_t doSomeMassiveCalculation(void);
19
20
21
22
/* Langsame Hauptschleifenfunktion */
23
void mainloop(void)
24
{
25
    InputBuffer_t LocalBuffer;
26
27
    LocalBuffer.nonAtomicVariable0 = doSomeMassiveCalculation();
28
    LocalBuffer.nonAtomicVariable1 = doALittleBitSimplerCalculation();
29
30
31
    /* Puffer-Tausch */
32
    if( ValidBuffer == &InputBuffer[0] )
33
    {
34
        InputBuffer[1] = LocalBuffer;
35
        ValidBuffer = &InputBuffer[1];
36
    }
37
    else if( ValidBuffer == &InputBuffer[1] )
38
    {
39
        InputBuffer[0] = LocalBuffer;
40
        ValidBuffer = &InputBuffer[0];
41
    }
42
    else
43
    {
44
        assert( false );
45
    }
46
}
47
48
49
50
/* ISR wird pro Hauptschleifendurchlauf viele Male aufgerufen */
51
void ISR(void)
52
{
53
    InputBuffer_t LocalBuffer = *ValidBuffer;
54
55
    doSomeFastStuffWith(LocalBuffer.nonAtomicVariable1);
56
    doOtherFastStuffWith(LocalBuffer.nonAtomicVariable0);
57
}
Habe ich etwas wichtiges übersehen, oder benötige ich wirklich vier 
Kopien davon, wenn ich nicht-blockierend arbeiten will und die ISR immer 
konsistente Daten benötigt?

von Dunno.. (Gast)


Lesenswert?

Wie fängst du denn bei deinem vorgehen ab dass die ist Eintritt in dem 
Moment wo valid Buffer geändert werden soll?

Meiner Meinung nach kann's dir da passieren dass die isr trotzdem noch 
die alten Daten verarbeitet.

von Walter T. (nicolas)


Lesenswert?

Dunno.. schrieb:
> Meiner Meinung nach kann's dir da passieren dass die isr trotzdem noch
> die alten Daten verarbeitet.

Alt ist ja nicht schlimm. Hauptsache die Daten sind nicht inkonsistent, 
d.h.

1. alle Daten, die die ISR bekommt passen zueinander und
2. es werden nie neuere Daten durch alte überschrieben.

von Oliver S. (oliverso)


Lesenswert?

dosomeFastStuffWith(LocalBuffer);

call by value->noch eine Kopie ;)


Ich verstehe deine Frage so:

doSomeMassiveCalculation und doALittleBitSimplerCalculation erzeugen die 
Daten in der mainloop, unabhängig von einem alten Datensatz (!!!).

dosomeFastStuffWith benutzt die Daten, ändert sie aber nicht.

In dem Fall reicht doch InputBuffer mit zwei Feldern, mit atomarem swap 
von ValidBuffer und WorkBuffer - pointern, ganz ohne Kopie der 
Datensätze.

Oliver

von Thomas W. (goaty)


Lesenswert?

Du brauchst auf jeden Fall eine Mutex um den Puffertausch zu locken, 
denke ich.

Oder mal sehen was std::atomic library zu bieten hat.

von Walter T. (nicolas)


Lesenswert?

Oliver S. schrieb:
> dosomeFastStuffWith(LocalBuffer);
>
> call by value->noch eine Kopie ;)

Ich habe das Beispiel im Eröffnungsbeitrag mal angepasst, um es weniger 
missverständlich zu machen.

Oliver S. schrieb:
> Ich verstehe deine Frage so:
>
> doSomeMassiveCalculation und doALittleBitSimplerCalculation erzeugen die
> Daten in der mainloop, unabhängig von einem alten Datensatz (!!!).
>
> dosomeFastStuffWith benutzt die Daten, ändert sie aber nicht.

Genau.

Oliver S. schrieb:
> In dem Fall reicht doch InputBuffer mit zwei Feldern, mit atomarem swap
> von ValidBuffer und WorkBuffer - pointern, ganz ohne Kopie der
> Datensätze.

Stimmt. mainloop() kann mir ja den aktiven Puffer nicht überschreiben, 
solange die ISR läuft. Es ist also nur eine Frage der Geschwindigkeit, 
ob es schneller ist, eine Kopie des Structs zu machen oder in einem 
veränderlichen Speicherbereich mit festen Offsets herumzuzeigern.

Gibt es darüber Daumenregeln, oder hilft nur profilen?

So sähe das aus:
1
/* Langsame Hauptschleifenfunktion */
2
void mainloop1(void)
3
{
4
    static InputBuffer_t *OutBuffer = &InputBuffer[1];
5
6
    OutBuffer->nonAtomicVariable0 = doSomeMassiveCalculation();
7
    OutBuffer->nonAtomicVariable1 = doALittleBitSimplerCalculation();
8
9
10
    /* Puffer-Tausch */
11
    if( ValidBuffer == &InputBuffer[0] )
12
    {
13
        OutBuffer =   &InputBuffer[0];
14
        ValidBuffer = &InputBuffer[1];
15
    }
16
    else if( ValidBuffer == &InputBuffer[1] )
17
    {
18
        OutBuffer =   &InputBuffer[1];
19
        ValidBuffer = &InputBuffer[0];
20
    }
21
    else
22
    {
23
        assert( false );
24
    }
25
}
26
27
28
29
/* ISR wird pro Hauptschleifendurchlauf viele Male aufgerufen */
30
void ISR1(void)
31
{
32
    InputBuffer_t *Buffer = ValidBuffer;
33
34
    doSomeFastStuffWith(Buffer->nonAtomicVariable1);
35
    doOtherFastStuffWith(Buffer->nonAtomicVariable0);
36
}

von Thomas W. (goaty)


Lesenswert?

Du mußt aber sicher sein daß

  OutBuffer =   &InputBuffer[0];
  ValidBuffer = &InputBuffer[1];

nicht unterbrochen wird, sonst hast du da einen halben Zustand.
Selbst

  ValidBuffer = &InputBuffer[1];

ist ja erstmal unterbrechbar, da du ja nicht weißt aus welchen 
Instructions sich das im Maschinencode zusammensetzt.

von Walter T. (nicolas)


Lesenswert?

Thomas W. schrieb:
> Du mußt aber sicher sein daß
>
>   OutBuffer =   &InputBuffer[0];
>   ValidBuffer = &InputBuffer[1];
>
> nicht unterbrochen wird, sonst hast du da einen halben Zustand.

Verstehe ich nicht. mainloop() wird in Outbuffer nichts mehr 
hineinschreiben, bevor nicht Validbuffer geändert wurde.

Thomas W. schrieb:
> welbst
>
>   ValidBuffer = &InputBuffer[1];
>
> ist ja erstmal unterbrechbar, da du ja nicht weißt aus welchen
> Instructions sich das im Maschinencode zusammensetzt.

Stimmt. In meinem Fall ist aber praktischerweise Maschinenwortbreite == 
Zeigerbreite (Cortex M4). Ich gehe also davon aus, dass ValidBuffer nie 
auf etwas Ungültiges zeigt.

von Thomas W. (goaty)


Lesenswert?

Ah ok, ich dachte da sind evtl noch mehr Threads die da arbeiten, dann 
wirds schon passen.

von Walter T. (nicolas)


Lesenswert?

Thomas W. schrieb:
> Ah ok, ich dachte da sind evtl noch mehr Threads die da arbeiten, dann
> wirds schon passen.

Nanu? Ich hätte gesagt: Für jede Kommunikation zwischen je zwei 
"Threads" brauche ich je eine unabhängige Austauschvariable pro 
Richtung. Wobei letztere unterschiedliche behandelt werden müssen, je 
nachdem wer wen unterbrechen kann.





Walter T. schrieb:
> Es ist also nur eine Frage der Geschwindigkeit,
> ob es schneller ist, eine Kopie des Structs zu machen oder in einem
> veränderlichen Speicherbereich mit festen Offsets herumzuzeigern.
>
> Gibt es darüber Daumenregeln, oder hilft nur profilen?

Gibt es Daumenregeln, ob das Kopieren eines structs oder der Zugriff auf 
jedes Element per Zeiger schneller ist, wenn die Adresse nicht fest und 
das Ziel volatile ist?

(Edit: letzteres habe ich im Eröffnungspost tatsächlich vergessen)

von Heiko L. (zer0)


Lesenswert?

Ich will noch anmerken: Das geht alles nicht.
Ohne atomics (oder zumindest Speicherbarrieren) gibt es keine Garantie, 
in welcher Reihenfolge da irgendetwas geschrieben wird. Interrupts kennt 
der Compiler nicht - interessieren also auch nicht.
Also ob der Pointer-Write echt vor dem Datenwrite kommt entscheidet so 
irgendein Optimierungsalgorithmus.

von Walter T. (nicolas)


Lesenswert?

Heiko L. schrieb:
> Pointer-Write echt vor dem Datenwrite kommt entscheidet so
> irgendein Optimierungsalgorithmus.

Wie ich schon schrieb: Im Eröffnungsthread das "volatile" vergessen. 
Zwei "volatile"-Schreibzugriffe dürfen nicht vertauscht werden.
1
volatile InputBuffer_t InputBuffer[2] = {[0] = {.nonAtomicVariable0 = 0, .nonAtomicVariable1 = 1}};
2
volatile InputBuffer_t *ValidBuffer = &InputBuffer[0];

von Heiko L. (zer0)


Lesenswert?

Walter T. schrieb:
> Heiko L. schrieb:
>> Pointer-Write echt vor dem Datenwrite kommt entscheidet so
>> irgendein Optimierungsalgorithmus.
>
> Wie ich schon schrieb: Im Eröffnungsthread das "volatile" vergessen.
> Zwei "volatile"-Schreibzugriffe dürfen nicht vertauscht werden.

Das nicht, aber non-volatile writes können daran vorbei wandern. Das 
müsste schon eine direkt Daten-Dependency verhindern.

Edit: ach so - ich sehe.
In dem Fall fiele mir noch ein, dass man sichergehen müsste, dass der 
Pointer-Write atomar ist.

von Peter D. (peda)


Lesenswert?

Wozu vor der Zuweisung erst noch vergleichen?
Das kostet doch ein Lesen, einen Vergleich und einen Sprung zusätzlich.

Wie schon geschrieben wurde, kapsele es atomar und gut is.
Sachen sollte man nicht unnötig verkomplizieren.

von Heiko L. (zer0)


Lesenswert?

Sehr interessanter Vortrag übrigens rund um solche Problematiken sogar 
noch mit atomics:
https://www.youtube.com/watch?v=IB57wIf9W1k

These: Der Compiler könnte auch erstmal zwei Schleifendurchläufe 
durchrechnen und dann am Schluss ein paar mehr volatile writes in Folge 
machen oder die Berechnung des lokalen Buffers mit den volatilen writes 
interleaven.

von Walter T. (nicolas)


Lesenswert?

Peter D. schrieb:
> Wozu vor der Zuweisung erst noch vergleichen?
> Das kostet doch ein Lesen, einen Vergleich und einen Sprung zusätzlich.
>
> Wie schon geschrieben wurde, kapsele es atomar und gut is.
> Sachen sollte man nicht unnötig verkomplizieren.

Das verstehe ich nicht. Wie würdest Du den Puffer-Tausch realisieren? 
Oder meinst Du beide Puffer hintereinander mit einem Flag?
1
#define BUFFERDEFAULT {.nonAtomicVariable0 = 0, .nonAtomicVariable1 = 1}
2
volatile InputBuffer_t InputBuffer2 = BUFFERDEFAULT;
3
volatile bool bufferValid2 = false;
4
5
6
7
/* Langsame Hauptschleifenfunktion */
8
void mainloop2(void)
9
{
10
    InputBuffer_t OutBuffer;
11
12
    OutBuffer.nonAtomicVariable0 = doSomeMassiveCalculation();
13
    OutBuffer.nonAtomicVariable1 = doALittleBitSimplerCalculation();
14
15
    /* Puffer schreiben */
16
    bufferValid2 = false;
17
    InputBuffer2 = OutBuffer;
18
    bufferValid2 = true;
19
}
20
21
22
23
/* ISR wird pro Hauptschleifendurchlauf viele Male aufgerufen */
24
void ISR2(void)
25
{
26
    static InputBuffer_t Buffer = BUFFERDEFAULT;
27
28
    if( bufferValid2 )
29
    {
30
        InputBuffer_t Buffer = InputBuffer2;
31
    }
32
33
    doSomeFastStuffWith(Buffer.nonAtomicVariable1);
34
    doOtherFastStuffWith(Buffer.nonAtomicVariable0);
35
}

Dann käme die ISR ja nur zu neuen Daten, wenn die Hauptschleife langsam 
genug ist, dass die Schreibfunktion nur einen kleinen Anteil hat.

von Peter D. (peda)


Lesenswert?

Walter T. schrieb:
> Das verstehe ich nicht. Wie würdest Du den Puffer-Tausch realisieren?
> Oder meinst beide Puffer hintereinander mit einem Flag?

Vergiß es, ich hatte das mit den Pointern übersehen.

von W.S. (Gast)


Lesenswert?

Walter T. schrieb:
> Ich will eine große, nicht-atomar änderbare Variable nicht-blockierend
> von einer langsamen Hauptschleife konsistent in eine schnelle ISR
> bringen.

Eine Variable kannst du nicht irgendwo hin bringen. So etwas geht 
grundsätzlich NICHT.

Was du kannst, ist dir ein Agreement ausdenken, mit dessen Hilfe du den 
Zugriff mehrerer unabhängiger Programmteile auf besagte Variable 
geregelt kriegst.

Wie du das im einzelnen tust, ist deine Sache und hängt vom dahinter 
steckenden Problem ab.

Aber im Allgemeinen tut man das so, daß es irgend einen Kenner gibt, der 
atomar änderbar ist und den nur eine einzige "Master"-Instanz schreiben 
darf. Zum Beispiel ein "IsValid" Bit oder bool im Datensatz.

Alle anderen Instanzen dürfen den Kenner nur lesend zur Kenntnis nehmen 
und allenfalls in einem anderen Antwortkenner (der dann ihnen gehört) 
mitteilen, wie sie auf den gelesenen Kenner reagiert haben. Sowas kann 
auch in Form von System-Botschaften erfolgen (ereignisgsteuerte Abläufe)

W.S.

von Walter T. (nicolas)


Lesenswert?

W.S. schrieb:
> Eine Variable kannst du nicht irgendwo hin bringen.

Danke für die präzise Klarstellung.

Beitrag #6102020 wurde von einem Moderator gelöscht.
von Wilhelm M. (wimalopaan)


Lesenswert?

Dein Problem ist doch das atomare Vertauschen zweier Zeigervariablen, 
und zwar ausschließlich von einem "thread" (main). Der andere (ISR) 
vertausch nicht. Und nur der vertauschende kann unterbrochen werden.

Dann brauchst Du nur ein atomares swap der Zeigervariablen (in main).

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.