Kezdőlap > Programming > A .NET 4.0 párhuzamosok a végtelenben találkoznak…

A .NET 4.0 párhuzamosok a végtelenben találkoznak…

2010. április 10. szombat
Egy héttel ezelőtt egy másik ügy miatt nagyon röviden megemlítettem a .NET 4.0-ben meglévő, többszálú, párhuzamos kódvégrehajtás lehetőségeit. Egyes elemeit picit tovább vizsgálva, érdekes dolgora bukkantam.
 
A többszálú működés kelléke (a korábban vázolt lehetőségek mellett) a for és foreach ciklusok végrehajtásának párhuzamosítása, mellyel akár fele annyi időre is lecsökkenhet a ciklus feldolgozási ideje (vagy… néha épp fordítva). Ehhez nyújt hatékony segítséget a Parallel.For és Parallel.ForEach statikus metódusok valamelyike.
 
Egyébként ez egy tök jó dolog, hiszen borzasztó kényelmes módon lehet rettentő sok időt nyerni, de… (bizony, mindig van egy "de…"). Abban az esetben, ha a programozó rendelkezik egy jó adag lustasággal vagy épp tudáshiánnyal (melyekre néha megvan az esély), sajnos bizony érhetik meglepetések. Vegyük az alábbi példát. 
int szamlalo = 0;
// Klasszikus for ciklus, szekvenciális utasítás-végrehajtással
for(int i = 0; i < 1000; i++)
{
  szamlalo++;
};

Console.WriteLine("Számláló = {0}", szamlalo);  // A számláló értéke = 1000
A fenti példában látható kód úgy viselkedik és addig tart, ahogy azt az elmúlt évtizedekben már megszokhattuk. Semmi meglepetés, semmi izgalom.
int szamlalo = 0;
// .NET 4.0 TPL – Task Parallel Library for ciklus, párhuzamos utasítás-végrehajtással
Parallel.For(0, 1000, () =>
{
  szamlalo++;
});

Console.WriteLine("Számláló = {0}", szamlalo);  // A számláló értéke = ? (nem tudjuk mennyi)


Megjegyzés:A Parallel.For utolsó paramétere egy
lambda kifejezés.
Ez a példakód tartalmát tekintve pontosan ugyanazt csinálja, mint az előző annyi apró különbséggel, hogy a párhuzamosításnak köszönhetően összességében kevesebb idő alatt hajtódik végre. Wow! De hiszen ez kell nekünk, ugyi!? Igen, csak… ahogy a megjegyzésben is látszik sajnos a végeredmény mégse az, mint amire számítottunk. Vagyis elképzelhető, hogy a szamlalo értéke 1000, de az is lehet, hogy 998 vagy 997 vagy bármi ehhhez közel álló lehet. Óóóóó!? Na, de miért? A Microsoft tervezői alaposan benéztek valamit? Nos, ha Java vagy Linux-hívő lennék, akkor ez utóbbi magyarázat felettébb szimpatikus lenne, de nem, fájdalom nem erről van szó. Hanem arról, hogy ez a dolgok rendje. Bármilyen furcsa ennek így kellett történnie. Ok, hagyok egy kis időt ennek a gondolatnak a megemésztésére jó? … [idő][idő][idő][idő][idő][idő][idő][idő][idő] … Jó, ennyi elég is volt. Jöjjön a magyarázat.
 
A magyarázat rém egyszerű: konkurrens feladatvégrehajtás miatt kialakult versenyhelyzet, ami a legelső, szevenciális esetben nem alakult ki, de a párhuzamosítás során viszont igen. Milyen helyzet? Versenyhezet. De hisz ez nem Forma-1! Nem, azaz majdnem. A Parallel.For metódus saját magának csinált versenyheztetet azáltal, hogy szétbontotta a szamlalo nevű változó értékének növelését (szamlalo++) több, párhuzamosan futó szálra. Csakhogy ezek a szálak mit sem tudnak egymásról. Mindegyik ugyanazt az egy szerencsétlen szamlalo-t szeretné növelni eggyel, ami meg pont ilyen problémát generál. Az "A" szál fogja a szamlalo-t, kiolvassa az aktuális értékét (mondjuk 990), majd megnöveli eggyel (991) és visszaírja a szamlalo-ba. Csakhogy, miután ő kiolvasta lehet, hogy ezalatt "B" szál is megtette ugyanezt, azaz kiolvasta a 990-et, megnövelte eggyel, így lett 991 és visszaírta ugyanoda. Tehát mindketten dolgoztak, de a nagy buzgólkodás vége az lett, hogy az elvárt 992 helyett mégis 991 lett a szamlalo érteke. Ismétlés: mindketten kiolvasták az aktuális értéket, mindketten ugyanazt a számot (a 990-et) találták benne, és mindketten boldogan megnövelték 991-re, majd végül mindketten visszaírták a szamlalo-ba. Mondom én, hogy hülye a Microsoft! Nem képes ilyenre figyelni? – mondaná a szorgos ellenfél. Képesnek képes lenne, de nem teszi, mert ez bizony a programozó dolga (nem biztos hogy az automatizmus minden esetben jó). Van megoldás? Van hát! Íme:
int szamlalo = 0;
Parallel.For(0, 1000, () =>
{
  Interlocked.Increment(ref szamlalo);  // Eggyel növeli a paraméterben kapott int változót
});

Console.WriteLine("Számláló = {0}", szamlalo);  // A számláló értéke = 1000

Az Intelocked osztály statikus metódusai (mint amilyen az Increment is) sokat segíthetnek ezen a helyzeten, hiszen a kiolvasás és érték növelés idejére zárolják a paraméterben kapott változót, vagyis megakadályozzák, hogy a konkurrens szálak egyidőben hozzáférjenek. Persze az Interlocked osztály se old meg minden problémát, de hogy mit nem, az legyen inkább házi feladat…
 
A nevezett Parallel.For és ForEach utasításokról részleteket itt meg itt lehet olvasni.
Kategóriák:Programming