Das atomare Flag

Atomare Datentypen sichern zwei Eigenschaften zu. Zum einen sind sie atomar, zu anderen setzen die Synchronisations- und Ordungsbedingungen an die Programmausführung.

 

Im letzten Artikel habe ich die sequenzielle Konsistenz vorgestellt als das Default-Verhalten für atomare Operationen. Was heißt das? Für jede atomare Operation lässt sich explizit die Speicherordnung setzen. Wird die Speicherordnung nicht explizit gesetzt, kommt implizit das Flag std:: memory_order_seq_cst zum Einsatz.

Daher ist der Codeschnipsel

x.store(1);
res= x.load();

äquivalent zum folgenden Codeschnipsel.

x.store(1,std::memory_order_seq_cst);
res= x.load(std::memory_order_seq_cst);

 

Der Einfachheit wegen werde ich natürlich im Verlauf des Artikels die erste Schreibweise vorziehen.

std::atomic_flag

std::atomic_flag bietet ein sehr einfaches Interface an. Mit der Methode clear lässt sich sein Wert auf false setzen, mit der Methode test_and_set lässt sich sein Wert auf true setzen. Dabei gibt test_and_set den alten Wert zurück. Um std::atomic_flag zu verwenden, muss er mit der Konstante ATOMIC_FLAG_INIT auf false initialisiert werden. Hört sich erst mal nicht besonders spannend an. std::atomic_flag besitzt aber zwei sehr interessanten Eigenschaften. Zum einen ist er der einzige lockfrei atomare Datentyp, zum anderen wird er gern als Baustein für höhere Threadabstraktionen verwendet.

Der einzige lockfrei atomare Datentyp. Was soll das heißen? Die komplexeren verbleibenden atomaren Datentypen können laut C++-Standard ihre atomaren Operationen durch ein Mutex implementieren. Daher bieten sie die Methode is_lock_free an, die genau die Frage beantwortet, ob bei der Implementierung des atomaren Datentyps ein Mutex verwendet wurde. Auf den populären Plattformen gibt der Methodenaufruf is_lock_free in der Regel false zurück.

Mit Hilfe des Baustein std::atomic_flag lässt sich ein Spinlock bauen. Ein Spinlock erlaubt es, ähnlich wie ein Mutex, einen kritischen Bereich zu schützen. Im Gegensatz zum Mutex wartet ein Spinlock aber nicht passiv darauf, bis er den Lock erhält, sondern fordert das Lock fortwährend aktiv an. Das spart zwar den teueren Contextwechsel in den wait-Zustand, verbraucht aber die volle Power der CPU.

Im Beispiel ist ein Splinlock mit Hilfe von std::atomic_flag implementiert.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <atomic>
#include <thread>

class Spinlock{
  std::atomic_flag flag;
public:
  Spinlock(): flag(ATOMIC_FLAG_INIT) {}

  void lock(){
    while( flag.test_and_set() );
  }

  void unlock(){
    flag.clear();
  }
};

Spinlock spin;

void workOnResource(){
  spin.lock();
  // shared resource
  spin.unlock();
}


int main(){

  std::thread t(workOnResource);
  std::thread t2(workOnResource);

  t.join();
  t2.join();

}

 

In dem Beispiel konkurrieren die zwei Threads t und t2 (Zeile 29 und 30) um den kritischen Bereich, der in Zeile 22 durch einen Kommentar nur angedeutet ist. Wie funktioniert das ganze? Die Klasse Spinlock bietet - ähnlich einem Mutex - die zwei Methoden lock und unlock an. Darüber hinaus wird im Konstruktor des Spinlock (Zeile 7) das std::atomic_flag Flag auf false initialisiert. Versucht nun Thread t die Funktion workOnRessource auszuführen, sind zwei Szenarien möglich.

Im ersten Szeanarium erhält der Thread t das Lock. Das bedeutet, das der lock-Aufruf erfolgreich war. Der lock-Aufruf ist dann erfolgreich, wenn der initiale Wert des Flags in Zeile 10 false ist, so dass ihn Thread t in einer atomaren Operation auf true setzen kann. Genau diesen Wert true gibt die while-Schleife für den anderen Thread t2 zurück, falls dieser später versucht, den Lock zu erhalten. Damit ist er im Hamsterrad gefangen. Da er selbst nicht den Wert des Flags auf false setzen kann, muss er aktiv warten, bis der Thread t die unlock-Methode ausführt und das Flag auf false zurücksetzt (Zeile 13 - 15). 

Im zweiten Szenarium erhält der Thread t nicht das Lock. Dann besitz der Thraed t2 betreits das Lock und wir sind in Szenarium 1 mit vertauschten Rollen.

Besonders interessant ist es, das aktive Warten des Spinlocks mit dem passiven Warten des Mutex zu vergleichen.

Spinlock versus Mutex

Was passiert mit der CPU-Last, wenn die Funktion workOnRessource den Spinlock für 2 Sekunden in Zeile 21 - 23 lockt?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <atomic>
#include <thread>

class Spinlock{
  std::atomic_flag flag;
public:
  Spinlock(): flag(ATOMIC_FLAG_INIT) {}

  void lock(){
    while( flag.test_and_set() );
  }

  void unlock(){
    flag.clear();
  }
};

Spinlock spin;

void workOnResource(){
  spin.lock();
  std::this_thread::sleep_for(std::chrono::milliseconds(2000));
  spin.unlock();
}


int main(){

  std::thread t(workOnResource);
  std::thread t2(workOnResource);

  t.join();
  t2.join();

}

 

Stimmt die Theorie, dann muss in diesem Augenblick einer meiner vier Kerne voll ausgelastet sein. Genau das Ergebnis zeigt der Screenshot.

 spinLockSleep

Schön ist in dem Screenshot zu sehen, dass durch das Ausführen des Programms spinLockSleep die Last eines Kernes auf 100% steigt. Interessanterweise kommt auch immer ein anderer meiner Kerne zum Zuge.

Statt einem Spinlock kommt im nächsten Programm ein Mutex zum Einsatz. Das Programm ist dadurch kurz und knapp.

 

#include <mutex>
#include <thread>

std::mutex mut;

void workOnResource(){
  mut.lock();
  std::this_thread::sleep_for(std::chrono::milliseconds(5000));
  mut.unlock();
}

int main(){

  std::thread t(workOnResource);
  std::thread t2(workOnResource);

  t.join();
  t2.join();

}

 

Führe ich das Programm mehrfach aus, ist keine erhöhte Last der Kerne zu beobachten.

mutex

 

Wie geht's weiter?

Weiter geht es im nächsten Artikel mit dem Klassen-Template std::atomic. Die verschiedenen Spezialierungen für boolesche Werte, integrale Typen und Zeiger besitzen ein deutlich reichhaltigeres Interface als das std::atomic_flag.

 

 

 

 

 

 

 

title page smalltitle page small Go to Leanpub/cpplibrary "What every professional C++ programmer should know about the C++ standard library".   Hole dir dein E-Book. Unterstütze meinen Blog.

 

Kommentare   

0 #1 Oliver S. 2021-01-11 19:51
Spinlocks ergeben im Userland keinen Sinn da, wenn der Thread der das Spinlock hält weg-schedult wird, andere Threads lange warten können bis die das Spinlock locken können. Daher machen Spinlocks nur im Kernel Sinn wo ein Thread für den eigenen Kern den Scheduler abschalten kann.
Zitieren
0 #2 Sandro S, 2021-02-15 10:03
Spinlocks mit "low contention" sind besser als Mutex & Co. Zumindest hat das Herb Sutter in einen seiner Talks gemeint. Yumal die Implementierung hier sehr basic ist. Um weiteren Overhead zu vermeiden, hilft vielleicht auch https://software.intel.com/en-us/articles/benefitting-power-and-performance-sleep-loops
Zitieren

Mentoring

Stay Informed about my Mentoring

 

Rezensionen

Tutorial

Besucher

Heute 639

Gestern 1497

Woche 8182

Monat 33041

Insgesamt 3331953

Aktuell sind 57 Gäste und keine Mitglieder online

Kubik-Rubik Joomla! Extensions

Abonniere den Newsletter (+ pdf Päckchen)

Beiträge-Archiv

Sourcecode

Neuste Kommentare