Namensräume
Varianten
Aktionen

Transaktionaler Speicher (TM TS)

Von cppreference.com
< cpp‎ | Sprache
 
 
C++ Sprache
Allgemeine Themen
Kontrollfluss
Bedingte Ausführungsaussagen
if
Iterationsanweisungen (Schleifen)
for
Bereichs-for (C++11)
Sprunganweisungen
Funktionen
Funktionsdeklaration
Lambda-Funktionsausdruck
inline-Spezifizierer
Dynamische Ausnahmespezifikationen (bis C++17*)
noexcept-Spezifizierer (C++11)
Ausnahmen
Namensräume
Typen
Spezifizierer
const/volatile
decltype (C++11)
auto (C++11)
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Speicherdauer-Spezifizierer
Initialisierung
Ausdrücke
Alternative Darstellungen
Literale
Boolesch - Ganzzahl - Gleitkommazahl
Zeichen - String - nullptr (C++11)
Benutzerdefinierte (C++11)
Dienstprogramme
Attribute (C++11)
Typen
typedef-Deklaration
Typalias-Deklaration (C++11)
Umwandlungen
Speicherzuweisung
Klassen
Klassenspezifische Funktionseigenschaften
explicit (C++11)
static

Spezielle Member-Funktionen
Templates
Sonstiges
 
 

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

atomic_noexcept compound-statement

atomic_cancel compound-statement

atomic_commit compound-statement

1) Wenn eine Ausnahme ausgelöst wird, wird std::abort aufgerufen.
2) Wenn eine Ausnahme ausgelöst wird, wird std::abort aufgerufen, es sei denn, die Ausnahme ist eine der für die Transaktionsabbrechung verwendeten Ausnahmen (siehe unten). In diesem Fall wird die Transaktion *abgebrochen*: Die Werte aller Speicherorte im Programm, die durch Nebeneffekte der Operationen des atomaren Blocks modifiziert wurden, werden auf die Werte zurückgesetzt, die sie zum Zeitpunkt der Ausführung des atomaren Blocks hatten, und die Ausnahme wird wie gewohnt fortgesetzt (Stack-Unwinding).
3) Wenn eine Ausnahme ausgelöst wird, wird die Transaktion normal committet.

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

Eine Funktion kann explizit als transaktionssicher deklariert werden, indem das Schlüsselwort transaction_safe in ihrer Deklaration verwendet wird.

In einer Lambda-Deklaration erscheint es entweder direkt nach der Capture-Liste oder direkt nach dem (optionalen) Schlüsselwort mutable.

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

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
  • macht die folgenden Funktionen explizit transaction_safe_dynamic
  • jede virtuelle Memberfunktion aller Ausnahmetypen, die Transaktionsabbrechung unterstützen (siehe atomic_cancel oben)
  • fordert, dass alle Operationen, die auf einem Allocator X transaktionssicher sind, auch auf X::rebind<>::other transaktionssicher 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

[bearbeiten] Anmerkungen

[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.