Der flüchtige Qualifier in C++

Jinku Hu 12 Oktober 2023
Der flüchtige Qualifier in C++

In diesem Artikel wird der Qualifier volatile in C++ vorgestellt.

Verwenden Sie den Qualifier volatile, um das Objekt zu kennzeichnen, das durch einen anderen Thread oder eine externe Aktion in C++ geändert wird

Das Verhalten des Schlüsselworts volatile sollte im Allgemeinen als hardwareabhängig angesehen werden, und Entwickler von User-Space-Anwendungen sollten immer die spezifischen Compiler-Handbücher konsultieren, um zu erfahren, wie der Qualifizierer in verschiedenen Szenarien interpretiert werden kann. Normalerweise teilt das Schlüsselwort volatile dem Compiler mit, dass das angegebene Objekt nicht für Ladeoperationen optimiert und immer aus dem Hauptspeicher statt aus den Registern oder Caches abgerufen werden soll. Beachten Sie, dass es mehrere Hierarchien von Caches gibt, auf die die Software meistens keinen Zugriff hat und die ausschließlich in der Hardware verwaltet werden. Wenn der Compiler jedoch versucht, den Speicherort in das Register zu laden, wird er automatisch zwischengespeichert. Somit kann ein konsequenter Zugriff auf dieselbe Speicherstelle von den Cache-Zeilen in der Nähe der CPU und um ein Vielfaches schneller als beim RAM erfolgen.

Wenn das Objekt durch ein externes Signal oder eine Unterbrechungs-ähnliche Routine modifiziert wird, sollte auf den geänderten Wert aus dem RAM zugegriffen werden, da der zwischengespeicherte Wert nicht mehr gültig ist. So werden Zugriffe auf volatile Objekte vom Compiler entsprechend gehandhabt. Um das mögliche Szenario zu demonstrieren, implementieren wir eine Funktion, die die globale Variable volatile Integer modifiziert, und eine weitere Funktion, die dieselbe Integer in der Schleifenanweisung while auswertet. Beachten Sie, dass die Schleife while einen leeren Körper haben kann, damit dieses Beispiel funktioniert. Zunächst erstellt die Funktion main einen separaten Thread, der die Funktion IncrementSeconds ausführt. Direkt danach ruft der Hauptthread die Funktion DelayTenSeconds auf, die in die Schleife geht, die nicht zurückkehrt, wenn die Variable seconds den Wert 10 nicht überschreitet. Da der andere Thread bereits gleichzeitig damit begonnen hat, die Variable seconds zu inkrementieren, wird der Hauptthread bald den geänderten Wert beobachten und von der Funktion zurückkehren.

#include <unistd.h>

#include <iostream>
#include <thread>

using std::cerr;
using std::cin;
using std::cout;
using std::endl;

volatile int seconds = 0;

void DelayTenSeconds() {
  while (seconds < 10) {
    usleep(500000);
    cerr << "waiting..." << endl;
  }
}

void IncrementSeconds() {
  for (int i = 0; i < 10; ++i) {
    sleep(1);
    cerr << "incremented " << endl;
    seconds = seconds + 1;
  }
}

int main() {
  struct timeval start {};
  struct timeval end {};
  std::thread th1;

  th1 = std::thread(IncrementSeconds);

  DelayTenSeconds();

  th1.join();
  return EXIT_SUCCESS;
}

Ausgabe:

waiting...
incremented
waiting...
....
waiting...
10.002481 sec

Als Ergebnis haben wir im Wesentlichen eine bedingte Verzögerungsfunktion implementiert, die wartet, bis das volatile Objekt durch externe Einwirkung verändert wird. Nun könnte man feststellen, dass sich dieser Code genau gleich verhält, wenn der volatile Qualifier entfernt und eine reguläre globale Variable verwendet wird, aber man sollte nicht vergessen, dass der Modifikationscodeblock von einer anderen Übersetzungseinheit oder einer externen Signalaktion stammen kann. Letztere Szenarien würden den Compiler zwingen, die DelayTenSeconds-Schleife zu optimieren, da die Variable in der aktuellen Übersetzungseinheit nicht verändert wird.

#include <sys/time.h>
#include <unistd.h>

#include <iostream>
#include <thread>

using std::cerr;
using std::cin;
using std::cout;
using std::endl;

volatile int seconds = 0;

void DelayTenSeconds() {
  while (seconds < 10) {
    usleep(500000);
    cerr << "waiting..." << endl;
  }
}

float TimeDiff(struct timeval *start, struct timeval *end) {
  return (end->tv_sec - start->tv_sec) + 1e-6 * (end->tv_usec - start->tv_usec);
}

void IncrementSeconds() {
  for (int i = 0; i < 10; ++i) {
    sleep(1);
    cerr << "incremented " << endl;
    seconds = seconds + 1;
  }
}

int main() {
  struct timeval start {};
  struct timeval end {};
  std::thread th1;

  th1 = std::thread(IncrementSeconds);

  gettimeofday(&start, nullptr);
  DelayTenSeconds();
  gettimeofday(&end, nullptr);

  printf("%0.6f sec\n", TimeDiff(&start, &end));

  th1.join();
  return EXIT_SUCCESS;
}
Autor: Jinku Hu
Jinku Hu avatar Jinku Hu avatar

Founder of DelftStack.com. Jinku has worked in the robotics and automotive industries for over 8 years. He sharpened his coding skills when he needed to do the automatic testing, data collection from remote servers and report creation from the endurance test. He is from an electrical/electronics engineering background but has expanded his interest to embedded electronics, embedded programming and front-/back-end programming.

LinkedIn Facebook