Gérer les accès concurrents en écriture – Quelques exemples

Quelques exercices pour apprivoiser la gestion des accès concurrents avec C#. Notez que ces exemples accompagnent un cours et que je n'ai pas encore eu le temps de les documenter par écrit.

Le programme de test est trivial.

using System;
using System.Collections.Generic;
using System.Threading;
using System.Diagnostics;

TestExact();
TestInexact();
TestAtomique();
TestAtomiqueMeilleur();

Nous mesurerons le temps d'exécution de chaque test à l'aide de la fonction Tester<T> visible à droite (voir ../../../Sujets/Divers--cdiese/Mesurer-Temps.html pour des explications)

static (T rés, long dt) Tester<T>(Func<T> f)
{
   var sw = new Stopwatch();
   sw.Start();
   T rés = f();
   sw.Stop();
   return (rés, sw.ElapsedMilliseconds);
}

Le test nommé TestInexact ne donne pas les bons résultats dû à une Data Race sur l'opération ++n.

static void TestInexact()
{
   const int N = 1_000_000;
   var (rés, dt) = Tester(() =>
   {
      const int NTHREADS = 2;
      int n = 0;
      var th = new Thread[NTHREADS];
      for (int i = 0; i != th.Length; ++i)
         th[i] = new (() =>
         {
            for (int j = 0; j != N; ++j)
               ++n;
         });
      foreach (var thr in th) thr.Start();
      foreach (var thr in th) thr.Join();
      return n;
   });
   Console.WriteLine($"À la fin, n vaut {rés}, obtenu en {dt} ms");
}

Le test nommé TestExact donne les bons résultats, et utilise (sagement) une variable locale pour l'essentiel du calcul, ne synchronisant l'accès à n qu'une seule fois.

static void TestExact()
{
   const int N = 1_000_000;
   object monMutex = new ();
   var (rés, dt) = Tester(() =>
   {
      const int NTHREADS = 2;
      int n = 0;
      var th = new Thread[NTHREADS];
      for (int i = 0; i != th.Length; ++i)
         th[i] = new (() =>
         {
            int m = 0;
            for (int j = 0; j != N; ++j)
               ++m;
            lock (monMutex)
               n += m;
         });
      foreach (var thr in th) thr.Start();
      foreach (var thr in th) thr.Join();
      return n;
   });
   Console.WriteLine($"À la fin, n vaut {rés}, obtenu en {dt} ms");
}

Le test nommé TestAtomique donne les bons résultats, et n'incrémente n que de manière synchronisée. Notez que c'est beaucoup plus lent qu'une version avec variable locale, mais nous avons un « pire cas » ici en écrivant N fois dans n (dans un programme plus raisonnable, ça pourrait être une bonne option; mesurez!)

static void TestAtomique()
{
   const int N = 1_000_000;
   var (rés, dt) = Tester(() =>
   {
      const int NTHREADS = 2;
      int n = 0;
      var th = new Thread[NTHREADS];
      for (int i = 0; i != th.Length; ++i)
         th[i] = new (() =>
         {
            for (int j = 0; j != N; ++j)
               Interlocked.Increment(ref n);
         });
      foreach (var thr in th) thr.Start();
      foreach (var thr in th) thr.Join();
      return n;
   });
   Console.WriteLine($"À la fin, n vaut {rés}, obtenu en {dt} ms");
}

Le test nommé TestAtomiqueMeilleur est probablement le meilleur du lot, mais il est subtil (il utilise un Compare-and-Swap, opération fondamentale mais pas évidente à comprendre). Ceci est super pertinent, mais plus de niveau universitaire que de niveau collégial (et ce ne sera pas à l'examen!)

static void TestAtomiqueMeilleur()
{
   const int N = 1_000_000;
   var (rés, dt) = Tester(() =>
   {
      const int NTHREADS = 2;
      int n = 0;
      var th = new Thread[NTHREADS];
      for (int i = 0; i != th.Length; ++i)
         th[i] = new (() =>
         {
            int m = 0;
            for (int j = 0; j != N; ++j)
               ++m;
            // Ok, «boutte subtil»... n+=m
            int attendu = n;
            int souhaité = attendu + m;
            while(Interlocked.CompareExchange(ref n, souhaité, attendu) != attendu)
            {
               attendu = n;
               souhaité = attendu + m;
            }
         });
      foreach (var thr in th) thr.Start();
      foreach (var thr in th) thr.Join();
      return n;
   });
   Console.WriteLine($"À la fin, n vaut {rés}, obtenu en {dt} ms");
}

Voilà. J'ajouterai des explications écrites éventuellement, mais ça vous fait un point de référence.


Valid XHTML 1.0 Transitional

CSS Valide !