Namespaces
Variants

Transactional memory (TM TS)

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

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

atomic_noexcept compound-statement

atomic_cancel compound-statement

atomic_commit zusammengesetzte Anweisung

1) Wenn eine Exception ausgelöst wird, std:: abort wird aufgerufen.
2) Wenn eine Exception ausgelöst wird, std:: abort wird aufgerufen, es sei denn, die Exception ist eine der für Transaktionsabbruch verwendeten Exceptions (siehe unten). In diesem Fall wird die Transaktion abgebrochen : Die Werte aller Speicherstellen im Programm, die durch Nebeneffekte der Operationen des atomaren Blocks verändert wurden, werden auf die Werte zurückgesetzt, die sie zum Zeitpunkt der Ausführung des Starts des atomaren Blocks hatten, und die Exception setzt das Stack Unwinding wie üblich fort.
3) Wenn eine Exception ausgelöst wird, wird die Transaktion normal committet.

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

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 unmittelbar nach der Capture-Liste oder unmittelbar nach dem (Schlüsselwort mutable (falls verwendet).

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

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

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

Hinweise

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 .