Le qualificateur volatile en C++

Jinku Hu 12 octobre 2023
Le qualificateur volatile en C++

Cet article présentera le qualificatif volatile en C++.

Utilisez le qualificateur volatile pour désigner l’objet qui est modifié par un autre thread ou une action externe en C++

Le comportement du mot-clé volatile doit généralement être considéré comme dépendant du matériel, et les développeurs d’applications en espace utilisateur doivent toujours consulter les manuels spécifiques du compilateur pour savoir comment le qualificatif peut être interprété dans divers scénarios. Habituellement, le mot-clé volatile notifie au compilateur que l’objet donné ne doit pas être optimisé pour les opérations de chargement et toujours récupéré à partir de la mémoire principale au lieu des registres ou des caches. Notez qu’il existe plusieurs hiérarchies de caches qui sont pour la plupart inaccessibles au logiciel et gérées uniquement dans le matériel, mais lorsque le compilateur essaie de charger l’emplacement mémoire dans le registre, il est automatiquement mis en cache. Ainsi, un accès conséquent au même emplacement mémoire peut être effectué à partir des lignes de cache proches du CPU et plusieurs fois plus rapide que la RAM.

Pendant ce temps, si l’objet est modifié par un signal externe ou une routine de type interruption, la valeur modifiée doit être accessible à partir de la RAM car la valeur mise en cache n’est plus valide. Ainsi, les accès aux objets volatile sont gérés par le compilateur en conséquence. Pour démontrer le scénario possible, nous implémentons une fonction qui modifie la variable entière globale volatile et une autre fonction qui évalue le même entier dans l’instruction de boucle while. Notez que la boucle while peut avoir un corps vide pour que cet exemple fonctionne. Au début, la fonction main crée un thread séparé qui exécute la fonction IncrementSeconds. Juste après, le thread principal invoque la fonction DelayTenSeconds, qui entre dans la boucle qui ne retournera pas si la variable seconds ne dépasse pas la valeur de 10. Étant donné que l’autre thread a commencé à incrémenter la variable secondes déjà simultanément, le thread principal observera bientôt la valeur modifiée et reviendra de la fonction.

#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;
}

Production:

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

En conséquence, nous avons essentiellement implémenté une fonction de délai conditionnel qui attend que l’objet volatile soit modifié par une action extérieure. Maintenant, on peut remarquer que ce code se comportera exactement de la même manière si le qualificateur volatile est supprimé et une variable globale régulière est utilisée, mais il ne faut pas oublier que le bloc de code de modification peut provenir d’une unité de traduction différente ou d’une action de signal externe. Ces derniers scénarios obligeraient le compilateur à optimiser la boucle DelayTenSeconds puisque la variable n’est pas modifiée dans l’unité de traduction courante.

#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;
}
Auteur: 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