(Oké, hogy programozói magyarsággal mondjuk: anonymous methods and captured local variables. :) )
Nemrég egyik ismerősöm egy furcsa hibával keresett meg. A programot, amit írt, többszálúvá kellett tennie; 4-5 szál is futhatott egyszerre, és egy ciklusban indította el őket. Minden teljesen jól működött – habár én próbáltam rábeszélni, hogy készüljön már most a Taskokra való átállásra :) –, egészen addig, amíg rá nem jött, hogy C# 2.0 óta van egy gyönyörűséges lehetőség a nyelvben a kód egyszerűsítésére, ez pedig a névtelen metódusok intézménye. Ha egy rövid kódot csak egy helyen kell felhasználnia a programban, miért szemetelje tele az osztályait kis metódusokkal?
Ha az embernek kalapácsa van, akkor
1. hajlamos minden problémát szögnek nézni,
2. felteheti a kérdést, hogy minek a kalapot ácsolni.
Hogy kicsit megfoghatóbb legyen nekünk is, leegyszerűsítve, kódban mondom el a történetet:
Adott tehát egy metódus, amit több szálon is futtatni szeretnénk,
private static void WriteNumber(object o)
{
Console.WriteLine("Something: {0} on Thread no {1}", o, Thread.CurrentThread.ManagedThreadId);
}
(bocs az angol elnevezésekért) és adott a Main metódus, ami majd futtatja:
static void Main()
{
for (int i = 0; i < 25; i++)
{
Thread t = new Thread(WriteNumber);
t.Start(i);
}
Console.Read();
}
Eddig minden szép és jó tehát; ha futtatjuk, valami ilyesmit kellene látnunk:
Valószínűleg nem sorban lesznek az eredmények, de alapvetően 0-tól 24-ig más-más szálak kiírnak 1-1 számot. Ez az elvárt működés, jegyezzük meg. :)
Akkor ugye jön az igény, hogy kicsit változtassunk a metóduson: ne fixen írja ki a stringet, ami bele van égetve (“Something: … on thread no …”), hanem azt is lehessen szabályozni a hívó metódus egy belső változójával. De lesznek olyan helyzetek is, amikor megelégszünk az alapfunkcionalitással, nem kell a testreszabott string.
Mit lehet tenni? Írhatunk még egy metódust, ami két paramétert fogad… Esetleg hívhatja egyik a másikat, és akkor legalább a logika valamivel szebb lesz. De akkor is néhány sornyi valódi kód kedvéért írunk húsz sort. Vaaaaaagy… Hatékonyan oldjuk meg! Kalapács, szög, névtelen metódus.
A probléma szempontjából ez a plusz string nem fontos, ezért kihagyhatjuk. A lényeg, hogy normál metódusról áttérünk névtelenre. Most lambda kifejezésként deklaráljuk, de az majdnem ugyanaz. Tehát:
Thread t = new Thread(() => Console.WriteLine("Something: {0} on Thread no {1}", i, Thread.CurrentThread.ManagedThreadId));
Vegyük észre, hogy innentől már a Thread indításánál megússzuk a paraméter átadását is. Elvégre az i-t adtuk át, de az is belső változó, tehát a “belső” függvény már eléri. Minden szép és jó! Leegyszerűsítettük a metódust, sőt, eltüntettük. Kevesebb, átláthatóbb kód. (Utóbbival azért szoktak vitatkozni.)
Akkor mi a baj? Futtassuk csak meg!
A végeredmény persze nem determinisztikus, de ahogy a kép is mutatja, lehet baj. Ennél a futtatásnál éppen a 2, 8, 12… számok fordultak elő több alkalommal, pedig ugyanúgy 25 szálat indítunk el. Tehát vannak számok, amelyek nem jelennek meg egyik szálon sem, vannak, amelyek többször is. WTF??? (Why the face. :) )
Ismerősöm ekkor átállt közvetlen ThreadPool használatra – ebben az esetben jó eséllyel már minden szál 25-öt ír ki. :D
A megoldás egyszerűen bonyolult. Mielőtt megmutatnám, még nézzünk bele a generált IL-kódba! (Az egyszerűség kedvéért a release fordítás utánit mutatom.)
Na, ez mi? Ott a Program osztály, eddig sima ügy. De ott van egy beágyazott osztály egy elég fura névvel… Ráadásul van benne egy i nevű mező. Nagyszerű, a compiler szabadidejében random osztályokat generál… :) Természetesen nem ez a helyzet. Gondoljuk csak meg, mi is a névtelen metódus! Ez is csak egy normál függvény lesz, tehát meg kell jelennie egy osztályban. (Tehát igen, a névtelen metódusnak is van neve, csak PR szempontból szebb névtelennek nevezni, mint azt mondani, hogy method with a name that is ugly beyond expression.) Mit gondoltok, mi lenne a logikus, hol tároljuk ezt a metódust?
Szerintem kivétel nélkül arra gondoltatok, hogy rakjuk bele abba az osztályba, mely a névtelen metódust tartalmazó “igazi” metódust tartalmazza. És alapesetben pontosan ezt teszi a fordító!
Próbáljátok ki: írjátok át a lambdában lévő WriteLine-t, hogy csak írja ki: alma. Nézzétek meg a generált IL-kódot, nem lesz plusz osztály.
A mi esetünkben azonban van egy fontos dolog a lambda/névtelen metódus belsejében. Felhasználjuk az i-t. Az i a tartalmazó metódus saját változója, vagyis a névtelen metódusunkon kívülről jön! Azt tudjuk, hogy a névtelen metódus is valódi metódusként jelenik meg a háttérben, vagyis nincs ténylegesen beágyazva a tartalmazó metódusba. Ebből következik, hogy az i változót sem láthatja. Tehát ha használni akarjuk, valahogy át kell adni ennek a metódusnak!
Na, erre való ez a beágyazott, ronda nevű osztály, és ezért tartalmaz egy i nevű publikus mezőt!
Nézzünk bele a beágyazott osztályba! Van benne egy konstruktor, sima ügy, viszont van egy furcsa nevű metódusa is. Lessük meg az ő kódját neki jól, egyre csak!
.method public hidebysig instance void '<Main>b__0'() cil managed
{
// Code size 37 (0x25)
.maxstack 8
IL_0000: ldstr "Something: {0} on Thread no {1}"
IL_0005: ldarg.0
IL_0006: ldfld int32 Test.Program/'<>c__DisplayClass2'::i
IL_000b: box [mscorlib]System.Int32
IL_0010: call class [mscorlib]System.Threading.Thread [mscorlib]System.Threading.Thread::get_CurrentThread()
IL_0015: callvirt instance int32 [mscorlib]System.Threading.Thread::get_ManagedThreadId()
IL_001a: box [mscorlib]System.Int32
IL_001f: call void [mscorlib]System.Console::WriteLine(string,
object,
object)
IL_0024: ret
} // end of method '<>c__DisplayClass2'::'<Main>b__0'
Elég átlátható, gyakorlatilag néhány betöltögetés, dobozolás, és a végén a WriteLine meghívása. (Azért kicsit durva, hogy egy ilyen egyszerű kódban is két dobozolást talál az ember, ha a függöny mögé néz.) Most akkor nézzük meg a másik fontos metódust, a Program.Maint!
.method private hidebysig static void Main() cil managed
{
.entrypoint
// Code size 77 (0x4d)
.maxstack 3
.locals init ([0] class [mscorlib]System.Threading.Thread t,
[1] class [mscorlib]System.Threading.ThreadStart 'CS$<>9__CachedAnonymousMethodDelegate1',
[2] class Test.Program/'<>c__DisplayClass2' 'CS$<>8__locals3')
IL_0000: ldnull
IL_0001: stloc.1
IL_0002: newobj instance void Test.Program/'<>c__DisplayClass2'::.ctor()
IL_0007: stloc.2
IL_0008: ldloc.2
IL_0009: ldc.i4.0
IL_000a: stfld int32 Test.Program/'<>c__DisplayClass2'::i
IL_000f: br.s IL_003c
IL_0011: ldloc.1
IL_0012: brtrue.s IL_0021
IL_0014: ldloc.2
IL_0015: ldftn instance void Test.Program/'<>c__DisplayClass2'::'<Main>b__0'()
IL_001b: newobj instance void [mscorlib]System.Threading.ThreadStart::.ctor(object,
native int)
IL_0020: stloc.1
IL_0021: ldloc.1
IL_0022: newobj instance void [mscorlib]System.Threading.Thread::.ctor(class [mscorlib]System.Threading.ThreadStart)
IL_0027: stloc.0
IL_0028: ldloc.0
IL_0029: callvirt instance void [mscorlib]System.Threading.Thread::Start()
IL_002e: ldloc.2
IL_002f: dup
IL_0030: ldfld int32 Test.Program/'<>c__DisplayClass2'::i
IL_0035: ldc.i4.1
IL_0036: add
IL_0037: stfld int32 Test.Program/'<>c__DisplayClass2'::i
IL_003c: ldloc.2
IL_003d: ldfld int32 Test.Program/'<>c__DisplayClass2'::i
IL_0042: ldc.i4.s 25
IL_0044: blt.s IL_0011
IL_0046: call int32 [mscorlib]System.Console::Read()
IL_004b: pop
IL_004c: ret
} // end of method Program::Main
Ahhoz már elég kemény tudás kell, hogy ezt végigelemezzük, de néhány nagyon fontos helyet megjelölnék:
- a 0002-es sorban jön létre a háttérben lévő rejtett objektum (ami eltárolja az i-t, illetve amin hívható lesz a névtelen metódus),
- a 000a sorban kap értéket ennek i tagja, illetve
- a 0011-es sorban kezdődik a ciklus.
Most akkor nézzük, hogyan tudjuk megoldani a problémát! Egy apró változtatást rakjunk bele a ciklusba, illetve a névtelen metódusba…
static void Main()
{
for (int i = 0; i < 25; i++)
{
int n = i;
Thread t = new Thread(() => Console.WriteLine("Something: {0} on Thread no {1}", n, Thread.CurrentThread.ManagedThreadId));
t.Start();
}
Console.Read();
}
Ugye, hogy szinte észrevehetetlen? Futtassuk meg, lássuk, mit csinál!
Ésmegyésmegyésmegy! De miért???
Az egyszerű válasz az, hogy feketemágia!!! :) De ne elégedjünk meg azzal, hogy a programozóból hidat építünk – mert azt kőből is lehet –, hanem nézzük meg, mi is történik itt.
Elég egyértelmű, hogy a választ nem találjuk meg a C# szintjén, mélyebbre kell ásnunk. Ha most vetünk egy pillantást a disassemblyre, láthatjuk, hogy a beágyazott osztály továbbra ott van, viszont nem i, hanem n nevű mezővel. A névtelen (randa nevű) metódus kódja ugyanaz, mint eddig, ott is csak annyi változott, hogy n változót néz i helyett. Viszont ha rányitunk a Mainre, érdekes dolgot láthatunk!
.method private hidebysig static void Main() cil managed
{
.entrypoint
// Code size 57 (0x39)
.maxstack 3
.locals init ([0] int32 i,
[1] class [mscorlib]System.Threading.Thread t,
[2] class Test.Program/'<>c__DisplayClass1' 'CS$<>8__locals2')
IL_0000: ldc.i4.0
IL_0001: stloc.0
IL_0002: br.s IL_002d
IL_0004: newobj instance void Test.Program/'<>c__DisplayClass1'::.ctor()
IL_0009: stloc.2
IL_000a: ldloc.2
IL_000b: ldloc.0
IL_000c: stfld int32 Test.Program/'<>c__DisplayClass1'::n
IL_0011: ldloc.2
IL_0012: ldftn instance void Test.Program/'<>c__DisplayClass1'::'<Main>b__0'()
IL_0018: newobj instance void [mscorlib]System.Threading.ThreadStart::.ctor(object,
native int)
IL_001d: newobj instance void [mscorlib]System.Threading.Thread::.ctor(class [mscorlib]System.Threading.ThreadStart)
IL_0022: stloc.1
IL_0023: ldloc.1
IL_0024: callvirt instance void [mscorlib]System.Threading.Thread::Start()
IL_0029: ldloc.0
IL_002a: ldc.i4.1
IL_002b: add
IL_002c: stloc.0
IL_002d: ldloc.0
IL_002e: ldc.i4.s 25
IL_0030: blt.s IL_0004
IL_0032: call int32 [mscorlib]System.Console::Read()
IL_0037: pop
IL_0038: ret
} // end of method Program::Main
Huhh, elég nagy különbség, már akkor is ha a kódsorok számát nézzük. De a lényeg néhány művelet sorrendjében van. Ugyanis a ciklus első sora a 0004-es, ahol létrehozza a program a rejtett objektumot, aminek majd meghívhatja a “névtelen” metódusát. Mit jelent ez? Hogy itt 25 ilyen objektumunk lesz, míg az előző esetben, az n = i sor nélkül csak egyetlen egy volt! Tehát nem egy integer lesz, amin osztozik az összes metódus, hanem mindegyiknek lesz egy sajátja – és mivel ezen saját integerek mindig az iteráción belül kerülnek beállításra, mindegyik a helyes értéket fogja tartalmazni, szemben a korábbi példával, ahol ha egy szál nem futott le elég gyorsan, az i értékét felülírta a ciklus következő iterációja.
Mi a jobb megoldás?
A esetben sok (igazi) metódust pakolunk a osztályba, szanaszét szemetelve azt olyan kóddal, amit lehet, hogy csak egyszer-egyszer használunk fel.
B esetben megoldjuk névtelen metódusokkal: kevesebb kód, szebb kód*, cserébe a háttérben plusz osztályok, objektumok jönnek létre, tehát esetenként erősen ráterhelhetünk a GC-re. Plusz jönnek az olyan finomságok, mint amit fentebb is bemutattam.
Úgyhogy csak óvatosan azokkal a névtelen metódusokkal!
Na, folytassuk a kódolást!
*: ízlés kérdése persze. Mindenesetre gondoljuk meg, hogy nézne ki egy LINQ lekérdezés, ha nem lennének névtelen metódusaink…
Slussz poén: ha beraktok egy nagyon minimális 10-20 msec-os sleepet a Start után, akkor is jól fog működni, mivel a szál előbb lefut, mint ahogy az i értéke megváltozna; nincs szükség a plusz változóra. Tanulság: minél lassabb a program, annál jobban működik! De ezt azért a megrendelőnek ne hangoztassuk… :)))
A teljes bejegyzést itt olvashatod: http://davidfulop.spaces.live.com/Blog/cns!BC8EF43294ECB87!600.entry
Elküldve
2010. 02. 08. 16:54
by
[assembly: AssemblyTitle("Chev")]
Lementve: Microsoft, .NET, C#, .NET Framework, IL, lambda expression, Shard, Threading, anonymous method, Intermediate Language, Dev, Code &
Megtekintve:
316
alkalommal