Neben Wahrheitswerten lassen sich atomare Datetypen zu Zeigern, integralen Typen und eigenen Datentypen erzeugen. Für eigene Datentypen gelten besondere Regeln.
Sowohl die atomare Wrapper um einen Zeiger T* std::atomic<T*> als auch um einen integralen Typ integ std::atomic<integ> unterstützen die CAS-Operationen.
std::atomic<T*>
Der atomare Zeiger std::atomic<T*> als einfacher atomarer Wrapper um einen Zeiger T* verhält sich wie ein gewöhnlicher Zeiger. std::atomic<T*> unterstützt Zeiger Arithmetik und Pre- und Postinkrement bzw. Pre- und Postdekremet Operationen. Das zeigt das Codebeispiel.
int intArray[5];
std::atomic<int*> p(intArray);
p++;
assert(p.load() == &intArray[1]);
p+=1;
assert(p.load() == &intArray[2]);
--p;
assert(p.load() == &intArray[1]);
std::atomic<Integraler Typ>
Zu den bekannten integralen Datentypen gibt es in C++11 die entsprechenden atomaren integralen Datentypen. Wie immer sind die atomaren integralen Datentypen zusammen mit ihren Operationen schön auf der Webseite en.cppreference.com beschrieben. Ein std::atomic<Integraler Typ> kann alles, was ein std::atomic_flag, ein std::atomic<bool> und ein std::atomic<T*> kann, aber noch viel mehr.
Am interessantesten sind die zusammengesetzten Zuweisungsoperatoren +=, -=, &=, |= und ^= und ihre Pedants std::atomic<>::fetch_add(), std::atomic<>::fetch_sub(), std::atomic<>::fetch_and(), std::atomic<>::fetch_or() und std::atomic<>::fetch_xor(). Die atomaren Lese- und Schreibeoperationen unterscheiden sich im Detail. Während die zusammengesetzten Zuweisungsoperatoren den neuen Wert zurückgeben, geben die fetch-Variationen den alten Wert zurück. Ein zweiter Blick verrät aber noch mehr. Es gibt keine Multiplikation, Division und Shift-Operation als atomare Lese-und Schreiboperation. Das ist aber keine Einschränkung, da diese Operationen relativ selten benötigt werden und sie aus den bestehenden Operationen leicht zu implementieren sind. Wie? Das stellt das Codebeispiel vor.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// fetch_mult.cpp
#include <atomic>
#include <iostream>
template <typename T>
T fetch_mult(std::atomic<T>& shared, T mult){
T oldValue= shared.load();
while (!shared.compare_exchange_strong(oldValue, oldValue * mult));
return oldValue;
}
int main(){
std::atomic<int> myInt{5};
std::cout << myInt << std::endl;
fetch_mult(myInt,5);
std::cout << myInt << std::endl;
}
|
Das bisschen Ausgabe zeigt der Screenshot
Die Implementierung des Funktions-Templates fetch_mult besitzt ein konzeptionelles Problem. Sie ist vollkommen generisch und kann daher mit einem beliebigen Typ aufgerufen werden. Ersetze ich dem Aufruf die ganze Zahl 5 in dem Ausdruck fetch_mult(myInt,5) durch einen C-String "5" in dem Ausdruck fetch_mult(myInt,"5"), kann der Microsoft Compiler die Zweideutigkeit nicht auflösen.
Zum einen lässt sich "5" als const char* interpretieren, zum anderen als int. Das ist aber nicht in meinem Sinn. Tatsächlich sollen nur integrale Typen als Template-Argument T verwendet werden. Das lässt sich mit Concepts Lite elegant lösen. Concepts Lite erlauben es, Bedingungen an Template-Parameter zu stellen. Sie werden aller Voraussicht nach Bestandteil des neuen C++20-Standards sein.
1
2
3
4
5
6
7
|
template <typename T>
requires std::is_integral<T>::value
T fetch_mult(std::atomic<T>& shared, T mult){
T oldValue= shared.load();
shared.compare_exchange_strong(oldValue, oldValue * mult);
return oldValue;
}
|
Die Funktion std::is_integral<T>::value wertet der Compiler aus. Ist T kein integraler Typ, moniert dies der Compiler unmissverständlich. std::is_integral ist eine Funktion der in C++11 neuen Type-Traits Bibliothek. In der requires Bedingung in Zeile 2 definiert der Programmierer die Anforderung an den Typ T. Diesen Vertrag stellt der Compiler zur Compilezeit sicher.
Aber auch eigene atomare Datentypen lassen sich in C++11 definieren.
std::atomic<Eigener Typ>
Für einen eigenen Typ MyType gelten starke Einschränkungen, damit dieser zum atomaren Typ std::atomic<MyType> wird. Diese Einschränkungen betreffen zum einen den Typ MyType selbst, diese betreffen zum anderen die Operationen, die std::atomic<MyType> zur Verfügung stehen.
Für MyType gelten die folgenden Einschränkungen:
- Der Copy-Zuweisungsoperator für MyType, für alle Basisklassen von MyType und alle nicht statischen Mitglieder von MyType muss trivial sein. Nur ein vom Compiler automatisch erzeugter Copy-Zuweisungsoperator ist trivial. Benutzerdefinierte Copy-Zuweisungsoperatoren sind im Umkehrschluss nicht trivial.
- MyType kann keine virtuelle Methoden und Basisklassen enthalten.
- MyType muss bitweise vergleichbar sein, so dass die C-Funktionen memcpy und memcmp angewandt werden können.
Die Zusicherungen an den Typ MyType lassen sich mit den Funktionen std::is_trivially_copy_constructible, std::is_polymophic und std::is_trivial der neuen Type-Traits Bibliothek zur Compilezeit prüfen.
Für den eigenen atomaren Typ std::atomic<MyType> steht nur ein eigeschränktes Interface zur Verfügung. Dies zeigt der nächste Abschnitt.
Um den Überblick zu wahren, sind in der Tabelle die atomaren Operationen abhängig vom atomaren Datentype dargestellt.
Freie, atomare Funktionen und Smart Pointer
Die Methoden des Klassen-Templates std::atomic und des Flags std::atomic_flag stehen auch als freie Funktionen zur Verfügung. Da die freien, atomaren Funktionen Zeiger anstelle von Referenzen verwenden, sind sie C-compatible. Natürlich unterstützen sie die entsprechenden Typen, aber auch den Smart Pointer std::shared_ptr. Dies ist ein Novum, ist der std::shared_ptr doch kein atomarer Datentyp. Das C++-Standardisierungskomitee sah es als notwendig an, das sich Instanzen von Smarter Pointern, die sich unter der Decke ihren Referenzzähler und ihr Objekt teilen, in einer atomaren Operation modifizieren lassen.
std::shared_ptr<MyData> p;
std::shared_ptr<MyData> p2= std::atomic_load(&p);
std::shared_ptr<MyData> p3(new MyData);
std::atomic_store(&p, p3);
Diese Atomizität betrifft aber nur den Referenzzähler. Dies gilt nicht für das Objekt. Aus diesem Grund wird C++17 um atomare std::shared_ptr erweitert: std::atomic_shared_ptr. Diese Argumentation gilt natürlich auch für den kleinen Bruder des std::shared_ptr: std::weak_ptr. std::weak_ptr, der helfen soll, zyklische Referenzen von std::shared_ptr zu brechen, erhält mit C++17 einen atomaren Pendant: std::atomic_weak_ptr.
Wie geht's weiter?
Nun sind die Grundlagen zu atomaren Datentypen gelegt. Im nächsten Artikel geht es mit den Synchronisations- und Ordnungsbedingungen auf atomaren Datentypen los.
Was sie schon immer wissen wollten
- Type-Traits Biibliothek
- Die neue Type-Traits Bibliothek erlaubt es, zur Compilezeit Typen abzufragen, zu vergleichen oder auch zu verändern. In meinem Linux-Magazin Artikel Statisch geprüft 02/2015 lässt sich das alles nachlesen.
- Smart Pointer
- Die Smart Pointer std::unique_ptr, std::shared_ptr und std::weak_ptr ermöglichen in C++11 explizites, automatisches Speichermanagement. In meinem Linux-Magazin Artikel Räumkommando und Klug aufgeräumt in 02/2013 und 04/2013 gehe ich genauer auf sie ein.
-
Go to Leanpub/cpplibrary "What every professional C++ programmer should know about the C++ standard library". Hole dir dein E-Book. Unterstütze meinen Blog.
Weiterlesen...