A magas szintű nyelvek, mint a C# elÅ‘nye, hogy elfedik a program alatt lévÅ‘ operációs rendszerek és hardverek különbségeit, egészen addig, amÃg nem akarunk több szálon programozni. Ebben az esetben ismernünk kell valamilyen szinten az operációs rendszert és a hardver működését is, amire fejlesztünk.
Mégpedig azért, mert bizonyos utasÃtások nem biztos, hogy atomiak. Vegyük például az i++ utasÃtást. A JIT fordÃtó X86 és X64 processzorok esetén dönthet úgy, hogy ha nincs számára megfigyelhetÅ‘ szálon belüli mellékhatás, akkor a szokásos i = i + 1 helyett ++i művelettel helyettesÃti, ami ezen architektúrák esetén gyorsabb, hiszen van rá külön assembly utasÃtás.
Azonban ez más platform és processzor architektúra esetén nem garantálható, valamint arról se felejtkezzünk meg, hogy a .NET esetén az ilyen működések egy részét maga a C# fordÃtó fedi el, más részét pedig a futtatókörnyezet. Ezért ha atomi műveletekre van szükségünk, akkor azt explicit jeleznünk kell a kódunkban, hogy az hardverfüggetlenül is megfelelÅ‘en működjön.
Ez a jelzés az Interlocked osztály metódusaival történik.
Az alábbi program az Interlocked osztály néhány metódusát szemlélteti:
using System.Threading;
internal static class Program
{
private static void Main(string[] args)
{
int i = 0;
int j = 15;
Interlocked.Increment(ref i); // inkrementálás i: 1
Interlocked.Add(ref i, 5); // hozzáadás, i: 6
Interlocked.Decrement(ref i); // dekrementálás i: 5
Interlocked.Exchange(ref i, j); //j átmozgatása i-be, i: 15
Interlocked.CompareExchange(ref i, 10, 15); // ha i értéke 15, akkor 10-re változtatja, i: 10
Interlocked.And(ref j, 10); // bitenkénti és
Interlocked.Or(ref j, 5); // bitenkénti vagy
}
}
MemoryBarrier
A modern processzorok több szintű cache memóriával rendelkeznek, hogy egyszerre több utasÃtás végrehajtását tudják megkezdeni, ezért nem biztos, hogy a programunk sorrendileg úgy fog végrehajtódni, mint ahogy azt megÃrtjuk. Egyes utasÃtásokat átrendezhet belül a processzor. Természetesen igyekszik ezt úgy elvégezni, hogy ebbÅ‘l mi semmit se vegyünk észre mellékhatások formájában.
Azonban többszálú környezetben ez nem biztos, hogy minden esetben sikerrel jár. Nézzünk egy példát:
using System;
using System.Threading;
class Program
{
static int x, y;
static int r1, r2;
static void Main()
{
Console.WriteLine("Teszt MemoryBarrier nélkül...");
RunTest(useBarrier: false);
Console.WriteLine("\nTeszt MemoryBarrier-rel...");
RunTest(useBarrier: true);
}
static void RunTest(bool useBarrier)
{
int iter = 0;
while (true)
{
iter++;
x = 0;
y = 0;
r1 = 0;
r2 = 0;
Thread t1 = new Thread(() =>
{
if (useBarrier) Interlocked.MemoryBarrier();
x = 1;
if (useBarrier) Interlocked.MemoryBarrier();
r1 = y;
});
Thread t2 = new Thread(() =>
{
if (useBarrier) Interlocked.MemoryBarrier();
y = 1;
if (useBarrier) Interlocked.MemoryBarrier();
r2 = x;
});
t1.Start();
t2.Start();
t1.Join();
t2.Join();
// Ha mindkét szál 0-t látott, akkor reorder történt!
if (r1 == 0 && r2 == 0)
{
Console.WriteLine($"Inkonzisztens állapot {iter}. iterációban: r1={r1}, r2={r2}");
break;
}
// Ha túl sokáig tart, jelezzünk, hogy stabil
if (iter % 10_000 == 0)
{
Console.WriteLine($"{iter} iteráció után sem volt hiba...");
break;
}
}
}
}
A program egy lehetséges kimenete:
Teszt MemoryBarrier nélkül...
Inkonzisztens állapot 431. iterációban: r1=0, r2=0
Teszt MemoryBarrier-rel...
10000 iteráció után sem volt hiba...
A programban két változó kereszt cseréje történik két szálon. A kódból jól látható, hogy elméletileg nem fordulhat elÅ‘ olyan eset a csere után, hogy r1 és r2 értéke is 0 legyen. Azonban a saját gépemen futtatva a 431. iterációban ez mégis megtörtént, mivel ebben futásban a processzor a belsÅ‘ cache-ben tévesen megÃtélve átrendezte az utasÃtások végrehajtási sorrendjét és nem azt az eredményt kapom, amit elvárnék.
Ezen jelenséget küszöböli ki az Interlocked osztály MemoryBarrier() metódusa. Ez szinkronizálja a memória-hozzáférést úgy, hogy a jelenlegi szálat futtató processzor mag nem rendezheti át az utasÃtásokat úgy, hogy a MemoryBarrier() hÃvása elÅ‘tti memória-hozzáférések a hÃvást követÅ‘ memória-hozzáférések után hajtódjanak végre.