Forum: Mikrocontroller und Digitale Elektronik Atomic-Variable etc. - ich verstehe fast nur Bahnhof


von Alex (haidanai)


Lesenswert?

Hallo,
ich habe ein Problem mit Software-Timern:
In einem Interrupt incrementiere ich den Zählerstand.
Dann gibt es Routinen, die den Zählerstand auslesen und andere Routinen, 
die den Zählerstand und / oder die Variable für die Referenzzeit 
verändern.
Eine Timeout-Routine vergleicht Zählerstand mit Referenzzeit.'
Die Variable ist uint16_t auf einem 32Bit-SAM.Die Kollision ist zwischen 
Lesen, incrementieren und zurückschreiben.
Nach Recherche fand ich den Hinweis man soll die Bibliothek stdatomic
verwenden. Da gibt es die atomic-Variablen, denen man aber eine 
memory_order angeben muss. Nur verstehe ich die Erklärungen überhaupt 
nicht. Was brauche ich?

von Oliver S. (oliverso)


Lesenswert?

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

Eigentlich reicht es, deine Zählvariable als std::atomic_uint16_t
zu deklarieren.

Oliver

: Bearbeitet durch User
von Sebastian V. (sebi_s)


Lesenswert?

Du musst keinen Memory Order angeben. Der Standard ist erstmal 
std::memory_order_seq_cst.

von Alex (haidanai)


Lesenswert?

@Oliver:
Danke für die schnelle Antwort.
ich programmiere in C unter Visual Studio.
In Beipielen finde ich fast immer die atomic Lib.
ist die stdatomic vorzuziehen? gibt es in der atomic Lib auch 
atomic-Variable?

von Alex (haidanai)


Lesenswert?

In meiner stdatomic gibt es _Atomic short, _Atomic int, _Atomic 
_INT_FAST16_TYPE_
Weis jemand, was es damit auf sich hat?
Wie ist die richtige Syntax?

von Adam P. (adamap)


Lesenswert?

Alex schrieb:
> In einem Interrupt incrementiere ich den Zählerstand.
> Dann gibt es Routinen, die den Zählerstand auslesen und andere Routinen,
> die den Zählerstand und / oder die Variable für die Referenzzeit
> verändern.
> Eine Timeout-Routine vergleicht Zählerstand mit Referenzzeit.'
> Die Variable ist uint16_t auf einem 32Bit-SAM.Die Kollision ist zwischen
> Lesen, incrementieren und zurückschreiben.

Vielleicht schätze ich die Situation falsch ein, aber irgendwie hört 
sich das für mich nach einem komischen Design an.
Bei einem 8-bit System würde ich atomic Zugriffe auf eine uint16 
Variable verstehen.
Ich habe bei meinen Projekten auf dem SAM lediglich volatile verwendet 
und würde nie auf die Idee kommen, eine Variable zu verändern, die vom 
Interrupt inkrementiert wird.
Ich würde dann eher eine Offset Variable nutzen die die Differenz zum 
Systemzähler enthält.

Aber vielleicht habe ich da auch zu wenig Einblick in das Problem / den 
Aufbau.

von Alex (haidanai)


Lesenswert?

>> ...und würde nie auf die Idee kommen, eine Variable zu verändern, die vom 
Interrupt inkrementiert wird
Ich habe ein Array aus vielen Timern ("Eieruhren"), die mit 10kHz 
Inkrementiert werden.
Mit Offset kann man machen, ist halt umständlicher. Und irgendwann muss 
man noch Überlauf behandeln...
Deshalb würde ich es so gerade nicht machen. Und atomic ist doch genau 
für solche Sachen da.

von Oliver S. (oliverso)


Lesenswert?

Alex schrieb:
> ich programmiere in C

Ok, dann gehts hier weiter:

https://en.cppreference.com/w/c/language/atomic

Da wärs dann der Datentyp atomic_ushort.

Oliver

von Alex (haidanai)


Lesenswert?

Danke! Probiere ich mal.

von Peter D. (peda)


Lesenswert?

Alex schrieb:
> Die Kollision ist zwischen
> Lesen, incrementieren und zurückschreiben.

Das würde ich vermeiden wollen. Es sollte immer nur eine Instanz geben, 
die die eine Variable schreiben darf.
Ansonsten mußt Du außerhalb des Interrupts die gesamte Sequenz (Lesen, 
incrementieren und zurückschreiben) atomar kapseln.

Ich rufe meine Softwaretimer in der Mainloop im 1ms Tick auf. Damit ist 
alles kollisionsfrei.
Der Timerinterrupt setzt also nur das 1ms Flag für die Mainloop.

von Klaus (feelfree)


Lesenswert?

Peter D. schrieb:
> Ansonsten mußt Du außerhalb des Interrupts die gesamte Sequenz (Lesen,
> incrementieren und zurückschreiben) atomar kapseln.

Und genau für diesen Zweck gibt es die atomic-Variablen...

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Das Ganze funktioniert übrigens nur ab Cortex-M3, die M0 unterstützen 
das nicht. Es basiert auf den LDREX/STREX  Instruktionen. CLREX wird 
tatsächlich nicht gebraucht, das machen die Cortex-M automatisch bei 
Interrupt-Eintritt.

Atomics sind etwas fummelig, es ist natürlich nur der eine Zugriff (z.B. 
atomic_fetch_add()) in sich atomisch, weitere Zugriffe sind immer nur 
für sich zu betrachten.

von Alex (haidanai)


Lesenswert?

>> Das Ganze funktioniert übrigens nur ab Cortex-M3, die M0 unterstützen
das nicht.
Vielen Dank für den Hinweis! Ich habe das gleiche Problem bei einem 
SAMD21, der ja ein M0 ist.
Wie kann man das beim M0 und atomic hinbekommen?

von Alex (haidanai)


Lesenswert?

Und ich hätte noch die Frage, ob das auch mit Array-Variablen 
funktioniert?
Geht das so?

volatile atomic_ushort  eggtmr_ch_ticks[HELPER_EGGTIMER_MAX_CH_COUNT];
volatile atomic_ushort  eggtmr_ch_times[HELPER_EGGTIMER_MAX_CH_COUNT];

von Peter D. (peda)


Lesenswert?

Alex schrieb:
> Wie kann man das beim M0 und atomic hinbekommen?

Ganz klassisch mit ATOMIC_BLOCK(ATOMIC_RESTORESTATE){}

von Adam P. (adamap)


Lesenswert?

Alex schrieb:
> Wie kann man das beim M0 und atomic hinbekommen?

Interrupt disable
...do something...
Interrupt enable

Alex schrieb:
> volatile atomic_ushort  eggtmr_ch_times[HELPER_EGGTIMER_MAX_CH_COUNT];

Bin mir nicht sicher ob das bei dem M0 wirklich was bringt, da er ja 
keine exclusive lade/speicher Befehle hat.

von Oliver S. (oliverso)


Lesenswert?

Adam P. schrieb:
> Bin mir nicht sicher ob das bei dem M0 wirklich was bringt, da er ja
> keine exclusive lade/speicher Befehle hat.

Da der Sinn einer Hochsprache für den Anwender u.a. darin besteht, die 
genaue Funktionalität der unterlagerten Hardware nicht kennen zu müssen, 
ist das ein Problem der Compilerbauer. Wenn es da gar keine 
Hardwareunterstützung für die Funktionalität gibt, muß es halt in 
Software passieren. So in der Art:

Adam P. schrieb:
> Interrupt disable
> ...do something...
> Interrupt enable

Oliver

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Alex schrieb:
> ob das auch mit Array-Variablen
> funktioniert?

Ja, natürlich sind nur die einzelnen Variablen atomic. Also bei so 
etwas:
1
volatile atomic_ushort  eggtmr_ch_ticks[HELPER_EGGTIMER_MAX_CH_COUNT];
2
3
void myISR (void) {
4
  assert (eggtmr_ch_ticks [0] == eggtmr_ch_ticks[1]);
5
}
6
7
int main () {
8
  while (1) {
9
    atomic_fetch_add (& eggtmr_ch_ticks[0], 1);
10
    atomic_fetch_add (& eggtmr_ch_ticks[1], 1);
11
  }
12
}

Kann es schief gehen, d.h. die ISR kann ganz genau zwischen die beiden 
Operationen dazwischen funken. Ist ja auch logisch, es steht nirgendwo 
dass diese Operationen irgendwie verknüpft sein sollen.

Oliver S. schrieb:
> Da der Sinn einer Hochsprache für den Anwender u.a. darin besteht, die
> genaue Funktionalität der unterlagerten Hardware nicht kennen zu müssen,

Grundsätzlich ja, aber für Embedded-Systemsoftware auf dem Cortex-M0 
gibt es für den Compiler keine vernünftige Möglichkeit dies umzusetzen. 
Der GCC generiert da ganz lapidar einen Aufruf an __atomic_fetch_add_4 
o.ä., zu dem man dann eine undefined reference bekommt. Man könnte das 
selbst implementieren indem man die Interrupts sperrt, aber man kann 
nicht alle Exceptions sperren, z.B. den NMI (sagt ja schon der Name). 
Man müsste dann sicher stellen dass man in den nicht-sperrbaren 
Exception-Handlern bloß nicht auf atomics zugreift.

Adam P. schrieb:
> Interrupt disable
> ...do something...
> Interrupt enable

Das ist grundsätzlich auch nicht schlecht, denn für einzelne 
Integer-Operationen ist das auch ziemlich effizient. Wenn der Interrupt 
erst 5 Takte später kommen kann ist das nicht unbedingt so tragisch. 
Etwas besser begreiflich ist es so allemal. Sobald aber ein RTOS im 
Spiel ist sollte man dann vermutlich doch besser atomics oder mutexe 
nutzen.

Peter D. schrieb:
> Ganz klassisch mit ATOMIC_BLOCK(ATOMIC_RESTORESTATE){}

Das stammt aus der AVR-Libc und bei den üblich ARM-Umbegungen gibt es 
kein Äquivalent.

Für die ARMs wurde das ganze Thema hier schon ausführlich diskutiert:
Beitrag "atomic-lib für stm32"

Der letzte Vorschlag dort, um ein ATOMIC_BLOCK für Cortex-M umzusetzen, 
ist schon sehr schön:
Beitrag "Re: atomic-lib für stm32"

Ich würde den noch ein kleines bisschen aufpolieren:
1
#define ATOMIC_BLOCK(type) for( uint32_t __prim = (type) == 0 ? 0 : __get_PRIMASK(), \
2
  __cond = (__disable_irq(),1); __cond != 0; \
3
  (type) == 0 ? (__enable_irq(),0):(__set_PRIMASK(__prim),0),__cond=0)
4
5
6
#define ATOMIC_FORCEON          0
7
#define ATOMIC_RESTORESTATE     1

Dadurch erspart man sich den bedingten Sprung am Ende. Der Compiler 
erzeugt dann aus
1
void testForceOn (void) {
2
  ATOMIC_BLOCK (ATOMIC_FORCEON) {
3
    __NOP ();
4
  }
5
}
6
7
void testRestore (void) {
8
  ATOMIC_BLOCK (ATOMIC_RESTORESTATE) {
9
    __NOP ();
10
  }
11
}

diesen Assemblercode:
1
00000014 <testForceOn>:
2
  14:   b672            cpsid   i
3
  16:   46c0            nop                     @ (mov r8, r8)
4
  18:   b662            cpsie   i
5
  1a:   4770            bx      lr
6
7
0000001c <testRestore>:
8
  1c:   f3ef 8310       mrs     r3, PRIMASK
9
  20:   b672            cpsid   i
10
  22:   46c0            nop                     @ (mov r8, r8)
11
  24:   f383 8810       msr     PRIMASK, r3
12
  28:   4770            bx      lr

Sowohl für Cortex-M0 als auch -M4 (bis auf das NOP...).

von Oliver S. (oliverso)


Lesenswert?

Niklas G. schrieb:
> Sobald aber ein RTOS im
> Spiel ist sollte man dann vermutlich doch besser atomics oder mutexe
> nutzen.

Du hast die Wahl zwischen zyklengenauen Interrupts, oder atomaren 
Variablenzugriff. Beides zusammen geht halt nicht. Weder vermutlich noch 
vielleicht.

Sinnvoll ist es natürlich, bei einem RTOS die dafür vorgesehenen 
Mechanismen zu nutzen. Ändern tut das an der Tatsachen aber auch nichts.

Oliver

: Bearbeitet durch User
von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Oliver S. schrieb:
> Beides zusammen geht halt nicht.

Bei Cortex-M0 (ARMv6M) nicht, genau. Beim Cortex-M3/4/7 (ARMv7M) schon, 
aber halt nur bei einzelnen 8/16/32-Bit-Variablen, wobei allerdings der 
(unterbrechbare) Variablenzugriff selbst etwas langsamer wird, die 
Interruptlatenz aber nicht. Keine Arme, keine Kekse 😉🍪

Just for fun:
Aus
1
atomic_fetch_add (&acnt, 42);
wird auf dem Cortex-M4 dann
1
   2:   f3bf 8f5b       dmb     ish
2
   6:   e853 1f00       ldrex   r1, [r3]
3
   a:   312a            adds    r1, #42 @ 0x2a
4
   c:   e843 1200       strex   r2, r1, [r3]
5
  10:   2a00            cmp     r2, #0
6
  12:   d1f8            bne.n   6 <test+0x6>
7
  14:   f3bf 8f5b       dmb     ish

Das CMP und BNE brauchen auch im "Gut"-Fall (kein Interrupt dazwischen 
gekommen) 2-3 zusätzliche Takte. Die beiden DMB-Instruktionen können 
mehrere Takte brauchen, ARM sagt dass man sie auf Single-Core Systemen 
ohne Cache (also Cortex-M3/4) weglassen könnte, das weiß der GCC nur 
anscheinend nicht.

: Bearbeitet durch User
von P. S. (namnyef)


Lesenswert?

Zuallererst sollte die Variable, die in der ISR inkrementiert wird, 
"volatile" qualifiziert werden. Dann ist für den Compiler klar, dass 
sich die Variable jederzeit ändern kann und er kommt nicht auf die Idee 
Optimierungen durchzuführen, die zu beschädigten Daten führen.

"Volatile" garantiert aber keinen atomaren Zugriff. Ob der Zugriff 
atomar ist, hängt von einigen Randbedingungen ab. Am besten schaut man 
ins Disassembly. Wenn für das Lesen mehr als eine Instruction benötigt 
wird, ist der Zugriff nicht atomar.

Um daher auf Nummer sicher zu gehen, sollten Interrupts vor dem Zugriff 
auf die Variable deaktiviert und nach dem Zugriff wieder zu aktiviert 
werden.

von Andreas M. (amesser)


Lesenswert?

Peter D. schrieb:
> Das würde ich vermeiden wollen. Es sollte immer nur eine Instanz geben,
> die die eine Variable schreiben darf.
> Ansonsten mußt Du außerhalb des Interrupts die gesamte Sequenz (Lesen,
> incrementieren und zurückschreiben) atomar kapseln.

Nicht unbedingt. Es gibt verschiedene Möglichkeiten wie man sowas 
umsetzen kann. Es kommt ganz auf die Zugriffsmuster an. Es gibt ja auch 
nicht nur Read-Modify-Write sondern auch nur "Write". Manchmal kann man 
z.b. es auch vom Wert abhängig machen, ala Zähler > 0 -> Wird im 
Interrupt runtergezählt. Zähler = 0 -> Userspace darf neuen Wert setzen. 
In anderen Fällen kann man den Zugriff aus dem Userspace auch solange 
wiederholen bis der gewünschte Wert drinnen steht (z.B. bei Timern) also 
ein Read-Modify gefolgt von Schleife aus Write-Read solange bis Read das 
gleiche wie Write ergibt. Da spart man sich Interruptlocks und macht 
sich somit die Interruptlatenz nicht kaputt. So werden oft Spinlocks 
implementiert.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

P. S. schrieb:
> Zuallererst sollte die Variable, die in der ISR inkrementiert wird,
> "volatile" qualifiziert werden.

Das ist bei Verwendung der "atomic" Typen unnötig.

P. S. schrieb:
> Dann ist für den Compiler klar, dass
> sich die Variable jederzeit ändern kann und er kommt nicht auf die Idee
> Optimierungen durchzuführen, die zu beschädigten Daten führen.

Das weiß er bei Atomics auch, sofern man die richtige Memory-Order 
angibt, was standardmäßig der Fall ist. Sonst würde das auf großen 
Prozessoren überhaupt nicht funktionieren, und da braucht man auch kein 
volatile.

P. S. schrieb:
> "Volatile" garantiert aber keinen atomaren Zugriff. Ob der Zugriff
> atomar ist, hängt von einigen Randbedingungen ab.

Bei volatile alleine ist da gar nichts garantiert. Daher benutzt man 
atomics.

P. S. schrieb:
> Am besten schaut man
> ins Disassembly.

Das hilft aber nur bis zu dem Moment wo man neu kompiliert, und das 
möchte man sicherlich nicht jedes Mal prüfen. Besser man schaut in das 
Architecture Reference Manual welche Garantien es gibt, und in den 
Sprachstandard, wie man diese nutzt. Eben mit Atomics.

P. S. schrieb:
> Um daher auf Nummer sicher zu gehen, sollten Interrupts vor dem Zugriff
> auf die Variable deaktiviert

Nur auf Architekturen welche keine Atomics bieten, wie Cortex-M0.

Andreas M. schrieb:
> Read-Modify gefolgt von Schleife aus Write-Read solange bis Read das
> gleiche wie Write ergibt. Da spart man sich Interruptlocks und macht
> sich somit die Interruptlatenz nicht kaputt. So werden oft Spinlocks
> implementiert.

Atomics implementieren genau das, da ist automatisch eine Schleife drin.

von Alex (haidanai)


Lesenswert?

Hallo an Alle,
vielen Dank für eure ausführliche Diskussion zum Thema.
Es gibt viele Wege das Problem zu lösen. Es kommt ganz auf die 
Erfordernisse an, welche Lösung man benutzen muss.
Bei mir ist ein Timerjitter tolerierbar, solange der Timer nicht falsch 
geht.

@Andreas M.
Ja tatsächlich, die Technik mit dem Probelesen hatte ich auch schon 
umgesetzt.

Ich habe es jetzt so umgesetzt, dass ich vor der Modifikation der 
Zählvariablen den Increment-Interrupt deaktiviere und danach wieder 
einschalte. Diese Routinen sind bei mir nicht sehr zeitkritisch.
Aber ich werde auch die aufpolierte Version mal ausprobieren.

von Michi S. (mista_s)


Lesenswert?

Andreas M. schrieb:
> Manchmal kann man z.b. es auch vom Wert abhängig machen,
> ala Zähler > 0 -> Wird im Interrupt runtergezählt.
> Zähler = 0 -> Userspace darf neuen Wert setzen.

Wie garantiert man hier dann, daß beim nicht atomaren Neusetzen das 
niederwertigste Byte (Word) als erstes geschrieben wird? Denn 
andernfalls könnte nach dem schreiben des höherwertigen Bytes (Word) der 
Interrupt dazwischen grätschen, mit runterzählen beginnen und das 
anschließende neu schreiben des niederwertigen Bytes macht daraus u.U. 
ein decrement um 256 statt um 1.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Michi S. schrieb:
> Wie garantiert man hier dann, daß beim nicht atomaren Neusetzen das
> niederwertigste Byte (Word) als erstes geschrieben wird?

Der ARM liest bzw. schreibt Halfwords (16bit) und Words (32bit) immer 
als ganzes atomisch. Das ist von der Architektur garantiert. Ist auch 
logisch für einen 32bit-Prozessor. Problematisch ist das 
Read-Modify-Write, bei diesen 3 Schritten kann ein Interrupt dazwischen 
kommen.

von Uwe B. (Firma: TU Darmstadt) (uwebonnes)


Lesenswert?

Niklas G. schrieb:
> Michi S. schrieb:
>> Wie garantiert man hier dann, daß beim nicht atomaren Neusetzen das
>> niederwertigste Byte (Word) als erstes geschrieben wird?
>
> Der ARM liest bzw. schreibt Halfwords (16bit) und Words (32bit) immer
> als ganzes atomisch. Das ist von der Architektur garantiert. Ist auch
> logisch für einen 32bit-Prozessor. Problematisch ist das
> Read-Modify-Write, bei diesen 3 Schritten kann ein Interrupt dazwischen
> kommen.

Gilt das auch bei non-aligned access?

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Uwe B. schrieb:
> Gilt das auch bei non-aligned access?

Nö. Unaligned Zugriffe sind bei ARMv6M (Cortex-M0) gar nicht möglich, 
bei ARMv7M (Cortex-M3 und höher) sind sie unterstützt (können per 
Software abgeschaltet werden), sind aber langsam. Der Prozessor setzt 
sie als 2 einzelne Zugriffe um, und damit sind sie nicht atomar, d.h. 
ein Interrupt kann dazwischen funken. Das gilt so für einzelne 
Lese/Schreibzugriffe.

Versucht man einen exklusiven Zugriff (also für eine read-modify-write 
Operation, nicht nur einzelner Zugriff) auf unaligned Daten zu machen 
mittels der LDREX/STREX-Instruktionen (also über die "atomic" APIs in 
C/C++), stürzt der Controller ab ("alignment fault"). Eine unaligned 
atomic Variable in C/C++ anzulegen ist aber eigentlich sowieso nicht 
möglich/erlaubt.

Eigentlich sind in C/C++ alle unaligned Zugriffe nicht erlaubt 
(undefined behavior), egal ob "atomic" oder nicht. Ich vermeide sie 
jedenfalls. Der Compiler darf davon ausgehen dass undefined behavior 
niemals auftritt, und könnte Codeteile die so etwas machen komplett 
wegoptimieren oder sonstwie verunstalten.

von (prx) A. K. (prx)


Lesenswert?

Niklas G. schrieb:
> sind sie nicht atomar, d.h.
> ein Interrupt kann dazwischen funken. Das gilt so für einzelne
> Lese/Schreibzugriffe.

DMA und 2. Core ja, aber wirklich auch Interrupts? Eine Unterbrechung 
innerhalb eines Befehls mit Fortsetzung gibt es bei ARM bei skalaren 
Befehlen nicht. Das gab es m.W. nur bei 68K.

> Eigentlich sind in C/C++ alle unaligned Zugriffe nicht erlaubt
> (undefined behavior),

Per C/C++ allein ist das erlaubt. Und wenn Ereignisse wie Interrupts 
oder DMA reinspuken, interessiert das die Sprachdefinition nicht.

: Bearbeitet durch User
von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

(prx) A. K. schrieb:
> Eine Unterbrechung innerhalb eines Befehls mit Fortsetzung gibt es bei
> ARM bei skalaren Befehlen nicht

Load/Store auf mehrere Adressen ist halt nicht skalar, daraus werden 2 
Einzelzugriffe.

(prx) A. K. schrieb:
> Per C/C++ allein ist das erlaubt

Nö. Dies verletzt die aliasing rules.

von Andreas M. (amesser)


Lesenswert?

Michi S. schrieb:
> Wie garantiert man hier dann, daß beim nicht atomaren Neusetzen das
> niederwertigste Byte (Word) als erstes geschrieben wird? Denn
> andernfalls könnte nach dem schreiben des höherwertigen Bytes (Word) der
> Interrupt dazwischen grätschen, mit runterzählen beginnen und das
> anschließende neu schreiben des niederwertigen Bytes macht daraus u.U.
> ein decrement um 256 statt um 1

Ja, vor allem bei 8-Bittern muss man dass im Hinterkopf haben. Da klappt 
das nur mit 8-Bit Variablen. Generell sollte man bei solchen Konstrukten 
die Architektur im Hinterkopf behalten. Bei 32 Bit CPUs kann man davon 
ausgehen, das Einzelzugriffe auf "normale" 8/16/32 Bit Variablen atomar 
sind. (Unaligned sei hier mal ausgeschlossen)

Aber auch auf 8 Bit CPUs kann man sowas anwenden, ja man kann im Prinzip 
sogar ganze Strukturen gegen Inkonsistente Zugriffe absichern indem man 
"Guards" einsetzt. D.h. es gibt nicht eine Variable, sondern zwei. 
(typischerweise als volatile gekennzeichnet) Der Userspace schreibt 
beide mit dem selben Wert, die ISR liest beide und tut nichts, wenn sie 
sich unterscheiden, oder dekrementiert halt beide. Und so funktioniert 
das auch mit Strukturen: Erste Variable verändern, Struktur 
aktualisieren, zweite Variable gleichziehen.

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.