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.

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)

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

namespace z
{
   class Program
   {
      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;
            Thread[] th = new Thread[NTHREADS];
            for (int i = 0; i != th.Length; ++i)
               th[i] = new Thread(() =>
               {
                  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 object();
         var (rés, dt) = Tester(() =>
         {
            const int NTHREADS = 2;
            int n = 0;
            Thread[] th = new Thread[NTHREADS];
            for (int i = 0; i != th.Length; ++i)
               th[i] = new Thread(() =>
               {
                  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;
            Thread[] th = new Thread[NTHREADS];
            for (int i = 0; i != th.Length; ++i)
               th[i] = new Thread(() =>
               {
                  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;
            Thread[] th = new Thread[NTHREADS];
            for (int i = 0; i != th.Length; ++i)
               th[i] = new Thread(() =>
               {
                  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");
      }

Le programme de test est trivial.

      static void Main(string[] args)
      {
         TestExact();
         TestInexact();
         TestAtomique();
         TestAtomiqueMeilleur();
      }
   }
}

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


Valid XHTML 1.0 Transitional

CSS Valide !