webentwicklung-frage-antwort-db.com.de

Was genau ist std :: atomic?

Ich verstehe, dass std::atomic<> Ein atomares Objekt ist. Aber inwieweit atomar? Nach meinem Verständnis kann eine Operation atomar sein. Was genau bedeutet es, ein Objekt atomar zu machen? Wenn beispielsweise zwei Threads gleichzeitig den folgenden Code ausführen:

a = a + 12;

Ist dann die gesamte Operation (sagen wir add_twelve_to(int)) atomar? Oder werden Änderungen an der Variablen atomic vorgenommen (also operator=())?

113
user4386938

Jede Instanziierung und vollständige Spezialisierung von std :: atomic <> stellt einen Typ dar, mit dem verschiedene Threads gleichzeitig (ihre Instanzen) arbeiten können, ohne undefiniertes Verhalten auszulösen:

Objekte atomarer Typen sind die einzigen C++ - Objekte, die frei von Datenrassen sind. Das heißt, wenn ein Thread auf ein atomares Objekt schreibt, während ein anderer Thread davon liest, ist das Verhalten genau definiert.

Außerdem können Zugriffe auf atomare Objekte die Synchronisation zwischen Threads herstellen und nichtatomare Speicherzugriffe anordnen, wie in std::memory_order Angegeben.

std::atomic<> Bricht Vorgänge ab, die in Vorgängerversionen von C++ 11 mit (z. B.) verschachtelten Funktionen mit MSVC oder ausgeführt werden mussten. Atombultine bei GCC.

Außerdem können Sie mit std::atomic<> Die Steuerung verbessern, indem Sie verschiedene Speicherreihenfolgen zulassen, die Synchronisations- und Sortierungsbeschränkungen festlegen. Wenn Sie mehr über C++ 11 Atomics und das Speichermodell erfahren möchten, können diese Links hilfreich sein:

Beachten Sie, dass Sie für typische Anwendungsfälle wahrscheinlich überladene arithmetische Operatoren oder eine andere Menge von ihnen verwenden würden:

std::atomic<long> value(0);
value++; //This is an atomic op
value += 5; //And so is this

Da Sie in der Operatorsyntax die Speicherreihenfolge nicht angeben können, werden diese Operationen mit std::memory_order_seq_cst ausgeführt, da dies die Standardreihenfolge für alle atomaren Operationen in C++ ist 11. Es garantiert sequentielle Konsistenz (globale Gesamtordnung) zwischen allen atomaren Operationen.

In einigen Fällen ist dies jedoch möglicherweise nicht erforderlich (und nichts ist kostenlos), sodass Sie möglicherweise ein expliziteres Formular verwenden möchten:

std::atomic<long> value {0};
value.fetch_add(1, std::memory_order_relaxed); // Atomic, but there are no synchronization or ordering constraints
value.fetch_add(5, std::memory_order_release); // Atomic, performs 'release' operation

Nun, dein Beispiel:

a = a + 12;

wird nicht zu einer einzelnen atomaren Operation ausgewertet: Es wird a.load() (die selbst atomar ist) ergeben, dann Addition zwischen diesem Wert und 12 und a.store() (auch atomar) ) des Endergebnisses. Wie bereits erwähnt, wird hier std::memory_order_seq_cst Verwendet.

Wenn Sie jedoch a += 12 Schreiben, ist dies eine atomare Operation (wie bereits erwähnt) und entspricht in etwa a.fetch_add(12, std::memory_order_seq_cst).

Wie für Ihren Kommentar:

Ein reguläres int hat atomare Lasten und Speicher. Was bringt es, es mit atomic<> Zu verpacken?

Ihre Aussage gilt nur für Architekturen, die eine solche Atomitätsgarantie für Geschäfte und/oder Lasten bieten. Es gibt Architekturen, die dies nicht tun. Außerdem ist es normalerweise erforderlich, dass Operationen an einer mit Word/Dword ausgerichteten Adresse ausgeführt werden müssen, um atomar zu sein. std::atomic<> Ist etwas, das auf every Plattformen garantiert atomar ist, ohne zusätzliche Anforderungen. Darüber hinaus können Sie Code wie folgt schreiben:

void* sharedData = nullptr;
std::atomic<int> ready_flag = 0;

// Thread 1
void produce()
{
    sharedData = generateData();
    ready_flag.store(1, std::memory_order_release);
}

// Thread 2
void consume()
{
    while (ready_flag.load(std::memory_order_acquire) == 0)
    {
        std::this_thread::yield();
    }

    assert(sharedData != nullptr); // will never trigger
    processData(sharedData);
}

Beachten Sie, dass die Assertionsbedingung immer wahr ist (und daher niemals ausgelöst wird), sodass Sie immer sicher sein können, dass die Daten nach dem Beenden der while -Schleife bereit sind. Das ist, weil:

  • store() für das Flag wird ausgeführt, nachdem sharedData gesetzt wurde (wir gehen davon aus, dass generateData() immer etwas Nützliches zurückgibt, insbesondere niemals NULL) und benutzt die std::memory_order_release Reihenfolge:

memory_order_release

Eine Speicheroperation mit dieser Speicherreihenfolge führt die Operation release aus: Es können keine Lese- oder Schreibvorgänge im aktuellen Thread neu angeordnet werden after this store. Alle Schreibvorgänge im aktuellen Thread sind in anderen Threads sichtbar, die dieselbe atomare Variable erhalten

  • sharedData wird verwendet, nachdem die while - Schleife beendet wurde, und daher wird nach dem load() from-Flag ein Wert ungleich Null zurückgegeben. load() verwendet die Reihenfolge std::memory_order_acquire:

std::memory_order_acquire

Eine Ladeoperation mit dieser Speicherreihenfolge führt die Operation Erfassung am betroffenen Speicherort aus: Es können keine Lese- oder Schreibvorgänge im aktuellen Thread neu angeordnet werden vor diesem Laden. Alle Schreibvorgänge in anderen Threads, die dieselbe atomare Variable freigeben, sind im aktuellen Thread sichtbar.

Dies gibt Ihnen eine genaue Kontrolle über die Synchronisation und ermöglicht es Ihnen, explizit anzugeben, wie sich Ihr Code verhalten darf/darf nicht/wird/wird. Dies wäre nicht möglich, wenn nur die Atomizität selbst gewährleistet wäre. Vor allem, wenn es um sehr interessante Synchronisationsmodelle wie die Release-Consume-Bestellung geht.

122
Mateusz Grzejek

Ich verstehe, dass std::atomic<> Ein Objekt atomar macht.

Das ist eine Frage der Perspektive ... Sie können es nicht auf beliebige Objekte anwenden und deren Operationen werden atomar, aber die bereitgestellten Spezialisierungen für (die meisten) integralen Typen und Zeiger können verwendet werden.

a = a + 12;

std::atomic<> Vereinfacht dies nicht (verwendet Vorlagenausdrücke) zu einer einzelnen atomaren Operation, stattdessen führt das operator T() const volatile noexcept-Mitglied eine atomare load() von a aus. Dann wird zwölf hinzugefügt und operator=(T t) noexcept führt eine store(t) aus.

17
Tony Delroy