Transactional memory (TM TS)
Transaktionales Speichermodell ist ein Nebenläufigkeits-Synchronisationsmechanismus, der Gruppen von Anweisungen in Transaktionen zusammenfasst, die
- atomar (entweder alle Anweisungen werden ausgeführt oder nichts geschieht)
- isoliert (Anweisungen in einer Transaktion dürfen keine halbfertigen Schreibvorgänge einer anderen Transaktion beobachten, selbst wenn sie parallel ausgeführt werden)
Typische Implementierungen verwenden Hardware Transactional Memory, wo unterstützt und innerhalb der verfügbaren Grenzen (z.B. bis das Changeset gesättigt ist), und greifen auf Software Transactional Memory zurück, üblicherweise implementiert mit optimistischer Nebenläufigkeit: Wenn eine andere Transaktion einige der von einer Transaktion verwendeten Variablen aktualisiert hat, wird sie stillschweigend wiederholt. Aus diesem Grund können wiederholbare Transaktionen ("atomare Blöcke") nur transaktionssichere Funktionen aufrufen.
Beachten Sie, dass der Zugriff auf eine Variable innerhalb einer Transaktion und außerhalb einer Transaktion ohne weitere externe Synchronisierung einen Datenwettlauf darstellt.
Wenn Feature-Testing unterstützt wird, werden die hier beschriebenen Features durch die Makrokonstante __cpp_transactional_memory mit einem Wert gleich oder größer als 201505 angezeigt.
Inhaltsverzeichnis |
Synchronisierte Blöcke
synchronized
compound-statement
Führt die compound statement aus, als ob sie unter einem globalen Lock stünde: Alle äußersten synchronized-Blöcke im Programm werden in einer einzigen Gesamtordnung ausgeführt. Das Ende jedes synchronized-Blocks synchronisiert mit dem Beginn des nächsten synchronized-Blocks in dieser Reihenfolge. Synchronized-Blöcke, die innerhalb anderer synchronized-Blöcke verschachtelt sind, haben keine spezielle Semantik.
Synchronisierte Blöcke sind keine Transaktionen (im Gegensatz zu den atomaren Blöcken unten) und können transaktionsunsichere Funktionen aufrufen.
#include <iostream> #include <thread> #include <vector> int f() { static int i = 0; synchronized { // begin synchronized block std::cout << i << " -> "; ++i; // each call to f() obtains a unique value of i std::cout << i << '\n'; return i; // end synchronized block } } int main() { std::vector<std::thread> v(10); for (auto& t : v) t = std::thread([] { for (int n = 0; n < 10; ++n) f(); }); for (auto& t : v) t.join(); }
Ausgabe:
0 -> 1 1 -> 2 2 -> 3 ... 99 -> 100
Das Verlassen eines synchronisierten Blocks auf beliebige Weise (durch Erreichen des Endes, Ausführen von goto, break, continue oder return, oder durch Werfen einer Exception) verlässt den Block und synchronisiert-mit dem nächsten Block in der einzelnen Gesamtordnung, wenn der verlassene Block ein äußerer Block war. Das Verhalten ist undefiniert, wenn std::longjmp verwendet wird, um einen synchronisierten Block zu verlassen.
Das Betreten eines synchronisierten Blocks durch goto oder switch ist nicht erlaubt.
Obwohl synchronisierte Blöcke ausgeführt werden, als ob sie unter einer globalen Sperre laufen, wird von den Implementierungen erwartet, dass sie den Code innerhalb jedes Blocks untersuchen und optimistische Nebenläufigkeit (gestützt durch hardwaretransaktionale Speicher, wo verfügbar) für transaktionssicheren Code und minimale Sperrung für nicht transaktionssicheren Code verwenden. Wenn ein synchronisierter Block einen Aufruf an eine nicht eingebettete Funktion tätigt, muss der Compiler möglicherweise die spekulative Ausführung beenden und eine Sperre für den gesamten Aufruf halten, es sei denn, die Funktion ist als
transaction_safe
deklariert (siehe unten) oder das Attribut
[[optimize_for_synchronized]]
(siehe unten) wird verwendet.
Atomare Blöcke
| Dieser Abschnitt ist unvollständig |
atomic_noexcept
compound-statement
atomic_cancel
compound-statement
atomic_commit
zusammengesetzte Anweisung
Die für Transaktionsabbruch verwendeten Ausnahmen in
atomic_cancel
-Blöcken sind
std::bad_alloc
,
std::bad_array_new_length
,
std::bad_cast
,
std::bad_typeid
,
std::bad_exception
,
std::exception
und alle Standardbibliothek-Ausnahmen, die davon abgeleitet sind, sowie der spezielle Ausnahmetyp
std::tx_exception<T>
.
Die
compound-statement
in einem atomaren Block darf keinen Ausdruck oder Anweisung ausführen und keine Funktion aufrufen, die nicht
transaction_safe
ist (dies ist ein Kompilierzeitfehler).
// jeder Aufruf von f() ruft einen eindeutigen Wert von i ab, auch bei paralleler Ausführung int f() { static int i = 0; atomic_noexcept { // Transaktion beginnen // printf("before %d\n", i); // Fehler: Aufruf einer nicht transaktionssicheren Funktion nicht möglich ++i; return i; // Transaktion committen } }
Das Verlassen eines atomaren Blocks auf andere Weise als durch eine Exception (Erreichen des Endes, goto, break, continue, return) führt zum Commit der Transaktion. Das Verhalten ist undefiniert, wenn std::longjmp verwendet wird, um einen atomaren Block zu verlassen.
Transaktionssichere Funktionen
| Dieser Abschnitt ist unvollständig |
Eine Funktion kann explizit als transaktionssicher deklariert werden, indem das Schlüsselwort transaction_safe in ihrer Deklaration verwendet wird.
| Dieser Abschnitt ist unvollständig |
In einer
Lambda
-Deklaration erscheint es entweder unmittelbar nach der Capture-Liste oder unmittelbar nach dem (Schlüsselwort
mutable
(falls verwendet).
| Dieser Abschnitt ist unvollständig |
extern volatile int * p = 0; struct S { virtual ~S(); }; int f() transaction_safe { int x = 0; // ok: nicht volatile p = &x; // ok: der Zeiger ist nicht volatile int i = *p; // Fehler: Lesezugriff durch volatile Glvalue S s; // Fehler: Aufruf eines unsicheren Destruktors }
int f(int x) { // implizit transaktionssicher if (x <= 0) return 0; return x + f(x - 1); }
Wenn eine Funktion, die nicht transaktionssicher ist, über eine Referenz oder einen Zeiger auf eine transaktionssichere Funktion aufgerufen wird, ist das Verhalten undefiniert.
Zeiger auf transaktionssichere Funktionen und Zeiger auf transaktionssichere Memberfunktionen sind implizit konvertierbar zu Zeigern auf Funktionen beziehungsweise Zeigern auf Memberfunktionen. Es ist nicht spezifiziert, ob der resultierende Zeiger gleich dem ursprünglichen vergleicht.
Transaktionssichere virtuelle Funktionen
| Dieser Abschnitt ist unvollständig |
Wenn der endgültige Überschreiber einer
transaction_safe_dynamic
-Funktion nicht als
transaction_safe
deklariert ist, ist ihr Aufruf in einem atomaren Block undefiniertes Verhalten.
Standardbibliothek
Neben der Einführung der neuen Ausnahmeklasse std::tx_exception nimmt die Transaktionsspeicher-Technische Spezifikation folgende Änderungen an der Standardbibliothek vor:
-
macht die folgenden Funktionen explizit
transaction_safe:
-
-
std::forward
,
std::move
,
std::move_if_noexcept
,
std::align
,
std::abort
, globaler Standard-
operator new
, globaler Standard-
operator delete
,
std::allocator::construct
falls der aufgerufene Konstruktor transaktionssicher ist,
std::allocator::destroy
falls der aufgerufene Destruktor transaktionssicher ist,
std::get_temporary_buffer
,
std::return_temporary_buffer
,
std::addressof
,
std::pointer_traits::pointer_to
, jede nicht-virtuelle Memberfunktion aller Ausnahmetypen, die Transaktionsabbruch unterstützen (siehe
atomic_canceloben)Dieser Abschnitt ist unvollständig
Grund: es gibt mehr
-
std::forward
,
std::move
,
std::move_if_noexcept
,
std::align
,
std::abort
, globaler Standard-
operator new
, globaler Standard-
operator delete
,
std::allocator::construct
falls der aufgerufene Konstruktor transaktionssicher ist,
std::allocator::destroy
falls der aufgerufene Destruktor transaktionssicher ist,
std::get_temporary_buffer
,
std::return_temporary_buffer
,
std::addressof
,
std::pointer_traits::pointer_to
, jede nicht-virtuelle Memberfunktion aller Ausnahmetypen, die Transaktionsabbruch unterstützen (siehe
-
macht die folgenden Funktionen explizit
transaction_safe_dynamic
-
-
jede virtuelle Memberfunktion aller Ausnahmetypen, die Transaktionsabbruch unterstützen (siehe
atomic_canceloben)
-
jede virtuelle Memberfunktion aller Ausnahmetypen, die Transaktionsabbruch unterstützen (siehe
-
erfordert, dass alle Operationen, die transaktionssicher für einen
Allocator
X sind, auch transaktionssicher für
X::rebind<>::othersind
Attribute
Das Attribut
[[
optimize_for_synchronized
]]
kann auf einen Deklarator in einer Funktionsdeklaration angewendet werden und muss bei der ersten Deklaration der Funktion erscheinen.
Wenn eine Funktion in einer Übersetzungseinheit als
[[optimize_for_synchronized]]
deklariert wird und dieselbe Funktion in einer anderen Übersetzungseinheit ohne
[[optimize_for_synchronized]]
deklariert wird, ist das Programm fehlerhaft; keine Diagnose erforderlich.
Es zeigt an, dass eine Funktionsdefinition für den Aufruf aus einer synchronized Anweisung optimiert werden sollte. Insbesondere vermeidet es die Serialisierung von synchronisierten Blöcken, die einen Aufruf an eine Funktion durchführen, die für die Mehrheit der Aufrufe transaktionssicher ist, aber nicht für alle Aufrufe (z.B. Hash-Tabellen-Einfügung, die neu hashen muss, Allokator, der einen neuen Block anfordern muss, eine einfache Funktion, die selten protokolliert).
std::atomic<bool> rehash{false}; // Wartungsthread führt diese Schleife aus void maintenance_thread(void*) { while (!shutdown) { synchronized { if (rehash) { hash.rehash(); rehash = false; } } } } // Worker-Threads führen hunderttausende Aufrufe dieser Funktion // pro Sekunde aus. Aufrufe von insert_key() aus synchronisierten Blöcken in anderen // Übersetzungseinheiten führen zur Serialisierung dieser Blöcke, es sei denn insert_key() // ist mit [[optimize_for_synchronized]] markiert [[optimize_for_synchronized]] void insert_key(char* key, char* value) { bool concern = hash.insert(key, value); if (concern) rehash = true; }
GCC-Assembly ohne das Attribut: Die gesamte Funktion wird serialisiert
insert_key(char*, char*): subq $8, %rsp movq %rsi, %rdx movq %rdi, %rsi movl $hash, %edi call Hash::insert(char*, char*) testb %al, %al je .L20 movb $1, rehash(%rip) mfence .L20: addq $8, %rsp ret
GCC-Assembler mit dem Attribut:
transaction clone for insert_key(char*, char*): subq $8, %rsp movq %rsi, %rdx movq %rdi, %rsi movl $hash, %edi call transaction clone for Hash::insert(char*, char*) testb %al, %al je .L27 xorl %edi, %edi call _ITM_changeTransactionMode # Hinweis: dies ist der Serialisierungspunkt movb $1, rehash(%rip) mfence .L27: addq $8, %rsp ret
|
Dieser Abschnitt ist unvollständig
Grund: Assembly mit Trunk überprüfen, auch Änderungen auf der Aufruferseite anzeigen |
Hinweise
|
Dieser Abschnitt ist unvollständig
Grund: Erfahrungsberichte aus Wyatt-Papier/Vortrag |
Schlüsselwörter
atomic_cancel , atomic_commit , atomic_noexcept , synchronized , transaction_safe , transaction_safe_dynamic
Compiler-Unterstützung
Diese technische Spezifikation wird von GCC ab Version 6.1 unterstützt (erfordert - fgnu - tm zur Aktivierung). Eine ältere Variante dieser Spezifikation wurde in GCC ab Version 4.7 unterstützt .