« Algorithme » sans synchronisation

Intuition générale : entrer dans une section critique de manière telle qu'une même donnée soit accédée par au moins deux unités d'exécution, dont au moins une en écriture, et ce sans synchronisation est une chose périlleuse, une Data Race, qui mène directement au comportement indéfini.

Le code qui suit est une implémentation où les accès en zone critique se font sans aucune synchronisation. Les résultats, dû à des conditions de course, sont indéfinis.

Je vous propose ceci à titre comparatif seulement. N'utilisez pas cet « algorithme » en pratique.

L'article suppose que vous avez lu la présentation générale des algorithmes de synchronisation classiques, de même que Algos-Section-Critique-General.html qui explique la démarche d'ensemble, présente les risques et décrit les classes Producteur et Consommateur qui sont réinvesties ici.

Détails spécifiques à cette « approche »

Par souci de « symétrie » ou par mimétisme partiel, j'ai utilisé un type nommé etat_synchro dans cette version, mais il ne sert, comme vous pouvez le voir, à presque rien – aucune synchronisation ne sera faite, après tout.

#include "Incopiable.h"
#include "Producteur.h"
#include "Consommateur.h"
#include <cassert>
#include <algorithm>
#include <iostream>
#include <string>
#include <fstream>
#include <thread>
#include <random>
using namespace std;
struct etats_synchro
{
   enum { NB_THREADS = 2 };
};

L'algorithme, si on peut le nommer ainsi, est à droite. On remarquera que l'accès à la section critique se fait sans aucune protection.

On le comprendra, cette pratique n'est pas des plus recommandables (c'est le moins qu'on puisse dire). Ce code n'a de sens que dans un contexte monoprogrammé, là où un seul thread opère à la fois... Ici, c'est un désastre pur et simple.

template <class T>
   void sans_synchro(T && oper, etats_synchro&, bool &fin)
   {
      auto id = oper.id;
      while (!fin)
      {
         oper.avant_partie_critique();
         oper.partie_critique(); // oups!
         oper.apres_partie_critique();
      }
   }

Sans grande surprise, le programme principal lance les diverses unités de traitement souhaitées, assure leur identification correcte et leur supplée le booléen qu'elles partageront et qui jouera pour elles le rôle d'un signal de fin, puis attend la fin des threads et nettoie les ressources attribuées au préalable.

int main()
{
   etats_synchro etats;
   random_device rd;
   mt19937 prng{ rd() };
   uniform_int_distribution<int> de(1,50);
   ofstream sortie{"out.txt"};
   clog.rdbuf(sortie.rdbuf());
   bool fin = {};
   int id = {};
   string transit;
   Producteur prod{ id++, transit, de(prng) };
   Consommateur cons{ id++, transit, sortie };
   thread th [] =
   {
      thread{[&] {
         sans_synchro(prod, etats, fin);
      }},
      thread{[&] {
         sans_synchro(cons, etats, fin);
      }}
   };
   char c; cin >> c; fin = true;
   for(auto &thr : th)
      thr.join();
   clog.rdbuf(nullptr);
}

À vos risques et périls, cependant.


Valid XHTML 1.0 Transitional

CSS Valide !