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.