Transaktionaler Speicher (TM TS)
Transaktionaler Speicher ist ein Synchronisationsmechanismus für Nebenläufigkeit, der Gruppen von Anweisungen in Transaktionen zusammenfasst, die
- atomar sind (entweder alle Anweisungen werden ausgeführt oder keine)
- isoliert sind (Anweisungen in einer Transaktion dürfen keine halb geschriebenen Änderungen einer anderen Transaktion beobachten, selbst wenn sie parallel ausgeführt werden)
Typische Implementierungen verwenden Hardware-transaktionalen Speicher, wo unterstützt und bis zu den Grenzen, in denen er verfügbar ist (z.B. bis der Änderungssatz gesättigt ist), und greifen auf Software-transaktionalen Speicher zurück, der üblicherweise mit optimistischem Nebenlauf implementiert wird: 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 und außerhalb einer Transaktion ohne weitere externe Synchronisation ein Datenrennen 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 201505 angezeigt.
Inhalt |
[bearbeiten] Synchronisierte Blöcke
synchronized compound-statement
Führt die zusammengesetzte Anweisung aus, als ob sie unter einer globalen Sperre stünde: Alle äußersten synchronisierten Blöcke im Programm werden in einer einzigen totalen Reihenfolge ausgeführt. Das Ende jedes synchronisierten Blocks synchronisiert sich mit dem Anfang des nächsten synchronisierten Blocks in dieser Reihenfolge. Synchronisierte Blöcke, die innerhalb anderer synchronisierter Blöcke verschachtelt sind, haben keine spezielle Semantik.
Synchronisierte Blöcke sind keine Transaktionen (im Gegensatz zu den nachfolgenden atomaren Blöcken) und dürfen 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 (Erreichen des Endes, Ausführen von goto, break, continue oder return oder Auslösen einer Ausnahme) beendet den Block und synchronisiert sich mit dem nächsten Block in der einzigen totalen Reihenfolge, 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 gestattet.
Obwohl synchronisierte Blöcke so ausgeführt werden, als ob sie unter einer globalen Sperre stünden, wird erwartet, dass die Implementierungen den Code innerhalb jedes Blocks untersuchen und optimistischen Nebenlauf (unterstützt durch Hardware-transaktionalen 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-inlined Funktion macht, muss der Compiler möglicherweise die spekulative Ausführung abbrechen und eine Sperre um 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.
[bearbeiten] Atomare Blöcke
| Dieser Abschnitt ist unvollständig |
atomic_noexcept compound-statement
atomic_cancel compound-statement
atomic_commit compound-statement
Die für die Transaktionsabbrechung in atomic_cancel-Blöcken verwendeten Ausnahmen sind std::bad_alloc, std::bad_array_new_length, std::bad_cast, std::bad_typeid, std::bad_exception, std::exception und alle von dieser abgeleiteten Standardbibliotheksausnahmen sowie der spezielle Ausnahmetyp std::tx_exception<T>.
Die zusammengesetzte Anweisung in einem atomaren Block darf keinen Ausdruck oder Anweisung ausführen oder eine Funktion aufrufen, die nicht transaction_safe ist (dies ist ein Kompilierungsfehler).
// each call to f() retrieves a unique value of i, even when done in parallel int f() { static int i = 0; atomic_noexcept { // begin transaction // printf("before %d\n", i); // error: cannot call a non transaction-safe function ++i; return i; // commit transaction } }
Das Verlassen eines atomaren Blocks auf eine andere Weise als durch eine Ausnahme (Erreichen des Endes, goto, break, continue, return) committet die Transaktion. Das Verhalten ist undefiniert, wenn std::longjmp verwendet wird, um einen atomaren Block zu verlassen.
[bearbeiten] 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 direkt nach der Capture-Liste oder direkt nach dem (optionalen) Schlüsselwort mutable.
| Dieser Abschnitt ist unvollständig |
extern volatile int * p = 0; struct S { virtual ~S(); }; int f() transaction_safe { int x = 0; // ok: not volatile p = &x; // ok: the pointer is not volatile int i = *p; // error: read through volatile glvalue S s; // error: invocation of unsafe destructor }
int f(int x) { // implicitly transaction-safe 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 in Zeiger auf Funktionen bzw. Zeiger auf Memberfunktionen konvertierbar. Es ist nicht spezifiziert, ob der resultierende Zeiger mit dem ursprünglichen übereinstimmt.
[bearbeiten] Transaktionssichere virtuelle Funktionen
| Dieser Abschnitt ist unvollständig |
Wenn der finale Überreiter einer transaction_safe_dynamic-Funktion nicht als transaction_safe deklariert ist, ist deren Aufruf in einem atomaren Block undefiniertes Verhalten.
[bearbeiten] Standardbibliothek
Neben der Einführung der neuen Ausnahmemuster std::tx_exception nimmt die Transactional Memory Technical Specification 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, wenn der aufgerufene Konstruktor transaktionssicher ist, std::allocator::destroy, wenn 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 Transaktionsabbrechung 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, wenn der aufgerufene Konstruktor transaktionssicher ist, std::allocator::destroy, wenn 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 Transaktionsabbrechung unterstützen (siehe
- macht die folgenden Funktionen explizit
transaction_safe_dynamic
- jede virtuelle Memberfunktion aller Ausnahmetypen, die Transaktionsabbrechung unterstützen (siehe
atomic_canceloben)
- jede virtuelle Memberfunktion aller Ausnahmetypen, die Transaktionsabbrechung unterstützen (siehe
- fordert, dass alle Operationen, die auf einem Allocator X transaktionssicher sind, auch auf
X::rebind<>::othertransaktionssicher sind
[bearbeiten] Attribute
Das Attribut [[optimize_for_synchronized]] kann auf einen Deklarator in einer Funktionsdeklaration angewendet werden und muss auf der ersten Deklaration der Funktion erscheinen.
Wenn eine Funktion in einer Translation Unit als [[optimize_for_synchronized]] deklariert wird und dieselbe Funktion in einer anderen Translation Unit ohne [[optimize_for_synchronized]] deklariert wird, ist das Programm schlecht geformt; keine Diagnose erforderlich.
Es zeigt an, dass eine Funktionsdefinition für die Ausführung in einem synchronized-Statement optimiert werden sollte. Insbesondere vermeidet es die Serialisierung von synchronisierten Blöcken, die einen Aufruf an eine Funktion machen, die für die Mehrheit der Aufrufe transaktionssicher ist, aber nicht für alle Aufrufe (z.B. Einfügen in eine Hashtabelle, das möglicherweise ein Rehashing erfordert, ein Allocator, der möglicherweise einen neuen Block anfordern muss, eine einfache Funktion, die selten protokolliert).
std::atomic<bool> rehash{false}; // maintenance thread runs this loop void maintenance_thread(void*) { while (!shutdown) { synchronized { if (rehash) { hash.rehash(); rehash = false; } } } } // worker threads execute hundreds of thousands of calls to this function // every second. Calls to insert_key() from synchronized blocks in other // translation units will cause those blocks to serialize, unless insert_key() // is marked [[optimize_for_synchronized]] [[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-Assembly 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 # Note: this is the serialization point movb $1, rehash(%rip) mfence .L27: addq $8, %rsp ret
| Dieser Abschnitt ist unvollständig Grund: Assembly mit Trunk prüfen, auch Änderungen auf der Aufruferseite zeigen |
[bearbeiten] Anmerkungen
| Dieser Abschnitt ist unvollständig Grund: Erfahrungen aus Wyatt-Papier/Vortrag |
[bearbeiten] Schlüsselwörter
atomic_cancel, atomic_commit, atomic_noexcept, synchronized, transaction_safe, transaction_safe_dynamic
[bearbeiten] Compiler-Unterstützung
Diese technische Spezifikation wird von GCC ab Version 6.1 unterstützt (erfordert -fgnu-tm zum Aktivieren). Eine ältere Variante dieser Spezifikation wurde in GCC ab 4.7 unterstützt.