Atomic
« | 10 Oct 2018 | »… war ein Spiel, in dem man Atome zu Molekülen zusammenführen musste.
In jener Zeit waren auf dem guten alten PC noch alle Instruktionen “atomar” und Programmierer mussten sich um den Zugriff auf Variablen nicht weiter kümmern.
Nicht einmal als präemptives Multitasking aufkam, wurde das Thema besonders populär, obwohl damit sowohl durch Linux wie auch unter Windows der unterbrochene Zugriff auf Speicherzellen möglich wurde.
Was sind atomare Zugriffe?
Wollen wir eine Variable um 1 erhöhen, finden 3 Operationen statt.
- Die Variable wird in ein CPU-Register gelesen,
- der Wert wird im Register erhöht,
- der Register-Inhalt wird in den Variablenspeicher geschrieben.
Schreibt nun ein anderer Thread zur gleichen Zeit ebenso in die Variable, kommt es zur Korruption. Unter Linux/POSIX könnten wir auch noch Signale als Szenario sehen.
Selbst auf Single-Core System passiert das, weil ein Thread während der Lese/Schreibeoperation unterbrochen werden kann. Und auf unseren heutigen Multi-Core Systemen ist das noch schlimmer. Denn alle CPU-Kerne arbeiten entkoppelt, haben ihre eigenen Caches und es ist nicht garantiert, in welcher Reihenfolge CPU-Caches mit dem Speicher synchronisiert werden.
Betriebssysteme stellen uns Mutex- und Critical-Section-Strukturen bereit, mit denen bei gemeinsamen Zugriffen nur eine CPU Code ausführen kann, während die anderen angehalten werden (Sie werden natürlich nicht angehalten, sondern dürfen was anderes rechnen ;) )
Prozessoren unterstützen aber auch eine interne Möglichkeit mit diesem Phänomen umzugehen: Atomare Zugriffe.
Ein atomarer Lese- oder Schreibzugriff besteht aus einer etwas anderen Codesequenz, die alle Kerne im System synchronisiert und Unterbrechungen (Interrupts) während der Zugriffsoperation unterbindet.
Eine atomare Operation garantiert also, dass der Wert einer Variable als ganzes gelesen und geschrieben wird, und dass alle anderen CPUs sofort ihren Cache aktualisieren müssen, wenn sie ebenfalls darauf zugreifen.
Die Windows-API stellt uns die Interlocked Funktionen zur Verfügung. Der MSVC lässt uns diese API aber auch direkt in CPU Instruktionen übersetzen.
Linux und POSIX definieren selbst meines Wissens keine eigene API, doch der GCC stellt mit seinen _sync* Funktionen alles bereit, was wir brauchen, um atomare Zugriffe zu realisieren.
Erst der C11 Standard und der C++11 Standard nehmen sich des Themas an … und das war meiner Meinung nach 10 Jahre zu spät.
Das Compare-and-swap (CAS) Schema ist die elementarste atomare Anweisung. Man kann alle Formen von Synchronisierungen und Rechenoperationen mittels CAS abbilden. In Hochsprachen manifestiert sich CAS meist so, dass der Inhalt einer Speicherstelle nur dann verändert wird, wenn ihr aktueller Wert einem bestimmten Wert entspricht.
1long pseudo_cas(long volatile* atom, 2 long compare_with, long new_value) 3{ 4 long old_value = *atom; 5 if(old_value == compare_with) 6 { 7 *atom = new_value; 8 } 9 return old_value; 10} 11 12long win_cas(long volatile* atom, 13 long compare_with, long new_value) 14{ 15 return InterlockedCompareExchange( 16 atom, new_value, compare_with); 17} 18 19long gcc_cas(long volatile* atom, 20 long compare_with, long new_value) 21{ 22 return __sync_val_compare_and_swap( 23 atom, compare_with, new_value); 24}
Im GATE Projekt bilden die gate_atomic_[TYPE]_xchg_if()
Funktionen
das CAS Feature ab.
Ich setze “Atome” gerne als Statusvariablen ein, um auf
Mutexe verzichten zu können.
1#define DISCONNECTED 0 2#define CONNECTED 1 3#define BUSY 2 4static gate_atomic_int32_t state = DISCONNECTED; 5 6bool connect_something() 7{ 8 if(DISCONNECTED != 9 gate_atomic_int32_xchg_if(&state, DISCONNECTED, BUSY)) 10 { 11 /* Alter Zustand war NICHT DISCONNECTED -> Abbruch */ 12 return false; 13 } 14 15 /* Neuer Zustand ist jetzt BUSY */ 16 17 ... 18 19 gate_atomic_int32_set(&state, CONNECTED); 20 /* Neuer Zustand ist jetzt CONNECTED */ 21 22 return true; 23} 24 25bool do_something_with_connection() 26{ 27 if(CONNECTED != 28 gate_atomic_int32_xchg_if(&state, CONNECTED, BUSY)) 29 { 30 /* Alter Zustand war NICHT CONNECTED -> Abbruch */ 31 return false; 32 } 33 /* Zustand ist jetzt BUSY */ 34 35 ... 36 37 gate_atomic_int32_set(&state, CONNECTED); 38 /* Neuer Zustand ist jetzt wieder CONNECTED */ 39 40 return true; 41} 42 43bool disconnect_something() 44{ 45 if(CONNECTED != 46 gate_atomic_int32_xchg_if(&state, CONNECTED, BUSY)) 47 { 48 /* Alter Zustand war NICHT CONNECTED -> Abbruch */ 49 return false; 50 } 51 52 /* Zustand ist jetzt BUSY */ 53 54 ... 55 56 gate_atomic_int32_set(&state, DISCONNECTED); 57 /* Neuer Zustand ist jetzt DISCONNECTED */ 58 59 return true; 60}