A szálkezelés legrégebb óta létező alapeleme a Thread osztály. Ez még a .NET 1.0-ban mutatkozott be és alapvetően a működését Windows-ra tervezték, mivel a .NET ebben az időben még csak Windows specifikus volt. A .NET Core megjelenésével a Thread osztály is át lett emelve a többi operációs rendszerre, de az operációs rendszerek közötti eltérések miatt sok működésbeli változáson ment keresztül.
Éppen ezért, ha van egy régi .NET alkalmazásunk, ami még a Thread osztályt alkalmazza, akkor annak a modernizálása több időt fog igénybevenni.
Meglepó módon a Thread osztály nem implementálja az IDisposable interfészt, helyette Å‘ a CriticalFinalizerObject Å‘sosztályból származik, ami extra garanciákat biztosÃt arra, hogy az objektum finalizer metódusa biztosan lefusson, még akkor is, ha a futtatókörnyezet mondjuk egy OutOfMemoryException vagy StackOverflowException kivétellel találkozik. Ennek az az oka, hogy maga a futtatókörnyezet is erÅ‘sen épÃt a szálakra és extrém körülmények között is biztosÃtania kell azt, hogy az a szálakhoz használt natÃv erÅ‘források gond nélkül felszabaduljanak.
Szál létrehozása
Nézzünk egy példát, ami bemutatja a szálakkal kapcsolatos alapokat:
using System;
using System.Threading;
namespace ThreadPelda
{
internal class Program
{
private static void KulonszalonFuttatott()
{
Console.WriteLine("Külön szálon számolunk");
for (int i = 0; i < 5; i++)
{
Thread.Sleep(1000);
Console.Write("{0} ", i);
}
Console.WriteLine();
Console.WriteLine("Külön szál befejeződött");
}
private static void Main(string[] args)
{
Thread thread = new Thread(KulonszalonFuttatott);
thread.Start();
thread.Join(); //Várakozás a szál befejezésére
Console.WriteLine("Fő szál befejeződött");
}
}
}
A kódban a KulonszalonFuttatott() metódusunk fog egy külön szálon futni. Ebben a Thread.Sleep() metódussal várakozunk 1000 milliszekundumot, mielÅ‘tt kiÃrnánk a konzolra a számláló állapotát, majd 5 ismétlés és nagyjából 5 másodperc után a szál végez.
A Main metódusban a Thread osztály példányosÃtásakor átadjuk a külön szálon futtatni kÃvánt metódusunkat. A példányosÃtás azonnal nem indÃtja el a kód futtatását, de értesÃti az operációs rendszer feladatütemezÅ‘jét, hogy majd egy új szál végrehajtásával foglalkoznia kell. A Start() metódus meghÃvása indÃtja el a folyamatot.
A Join() metódus meghÃvása blokkolja a fÅ‘ szál további futását, egészen addig, amÃg a futtatott szálunk nem végzett. Enélkül az utasÃtás nélkül a fÅ‘ szálunk feladat hiányában azonnal kilépne, ami az alkalmazás bezáródását eredményezné. Itt megjegyzem, hogy ha a szálban egy végtelen ciklust futtatnánk, akkor Join() hÃvása egy deadlock szituációt eredményezne.
Fontos, hogy soha ne hÃvjuk meg annak a Thread objektumnak a Join() metódusát, ami az aktuális szálat képviseli (A Thread.CurrentThread statikus tulajdonsága mutatja az aktuális szálat) az aktuális szálból, mert ez deadlock-ot okoz. Mégpedig azért, mert Ãgy lényegében az aktuális szál a végtelenségig várna magára.
A program kimenete:
Külön szálon számolunk
0 1 2 3 4
Külön szál befejezÅ‘dött
FÅ‘ szál befejezÅ‘dött
A Thread tulajdonságai, metódusai:
A Thread osztály számos tulajdonsággal és metódussal rendelkezik. Ezek közül itt csak az általam legfontosabbnak Ãtéltek kerültek ismertetésre. A Thread osztály teljes dokumentációja a következÅ‘ cÃmen lelhetÅ‘ fel: https://docs.microsoft.com/en-us/dotnet/api/system.threading.thread
public bool IsAlive { get; }
A nevéből adódóan megmondja, hogy a szál él-e.
bool IsBackground { get; set; }
True-val tér vissza, ha háttérszálról beszélünk. Dióhéjban a különbség az elÅ‘térben, illetve a háttérben futó szálak között az az, hogy elÅ‘bbire várni fog az alkalmazásunk, mielÅ‘tt leállna, utóbbira viszont nem. A CLR meghÃvja az Abort függvényt a háttérszálakra, amint az elÅ‘térszálak befejezték a munkát.
public string Name { get; set; }
A szál neve. Ha nem állÃtottunk be nevet, akkor null értéket ad vissza. Fejlesztés és hibakeresés közben jön jól, hogy el tudjuk nevezni.
public ThreadPriority Priority { get; set; }
A szál prioritását tudjuk vele beállÃtani vagy lekérni. A prioritás az operációs rendszer feladatütemezÅ‘jére lehet hatással. Fontos, hogy csak lehet. A .NET multiplatform voltából nem garantálja, hogy az alatta elhelyezkedÅ‘ futtató operációs rendszer ütemezÅ‘je ezt figyelembe vegye.
Egy ThreadState enum-ban megkapjuk a szál jelenlegi állapotát. A ThreadState a következő értékeket veheti fel:
| Érték | LeÃrás |
|---|---|
Aborted |
MegszakÃtásra került, de még nem váltott az állapot Stopped-ra |
AbortRequested |
A megszakÃtása folyamatban van |
Background |
Egy háttér szálon fut |
Running |
Futó állapotban van |
Stopped |
MegállÃtásra került, végállapot |
StopRequested |
MegállÃtása folyamatban van |
Suspended |
Felfüggesztett állapotban van |
SuspendRequested |
Felfüggesztése elindult |
Unstarted |
Futásra kész, de még a Start() metódus nem lett meghÃvva |
WaitSleepJoin |
Blokkolt állapotban van. A Sleep() és a különböző zárolások teszik ilyen állapotba |
A szálak életciklusát az alábbi ábra szemlélteti:
Figyelem: A ThreadState elsÅ‘sorban hibakeresési célokat szolgál, hogy bármikor rá tudjunk nézni egy adott szál állapotára. Logikát erre NE épÃtsünk.
Adatok átadása a szálaknak
A külön szálon futtatott metódusunknak a ThreadStart vagy ParameterizedThreadStart delegate tÃpusnak kell megfelelnie. A ThreadStart egy paraméter nélküli, void visszatérésű metódus, mÃg a ParameterizedThreadStart szintén void visszatérésű, de egy object paraméter fogadására képes.
public delegate void ThreadStart();
public delegate void ParameterizedThreadStart(object obj);
Felmerülhet a kérdés, hogy miért kaptak ezek külön tÃpust, hiszen pont ezen metódusokat Ãrják le az Action és Action<object> delegate tÃpusok. A mögöttes ok az, hogy a Thread objektum a .NET 1.0 óta létezik, ami nem rendelkezett generikusok koncepciójával és a korábbi keretrendszerrel való kód kompatibilitás miatt megtartották ezen tÃpusokat.
A Thread objektumunk Start metódusa paraméter fogadására is képes. Ha ezt paraméterrel hÃvjuk meg, akkor az átadásra kerül a szálon futtatott metódusnak:
using System;
using System.Threading;
namespace ThreadPelda2
{
internal class Program
{
private static void KulonszalonFuttatott(object? obj)
{
int meddig = (int)obj;
Console.WriteLine("Külön szálon számolunk");
for (int i = 0; i < meddig; i++)
{
Thread.Sleep(1000);
Console.Write("{0} ", i);
}
Console.WriteLine();
Console.WriteLine("Külön szál befejeződött");
}
private static void Main(string[] args)
{
Thread thread = new Thread(KulonszalonFuttatott);
thread.Start(15); //Új szál indÃtása és paraméter átadása
thread.Join(); //Várakozás a szál befejezésére
Console.WriteLine("Fő szál befejeződött");
}
}
}
Kivételkezelés
Nézzünk egy példát, hogy mi történik, ha kivétel keletkezik egy szálon:
using System;
using System.Threading;
namespace ThreadPelda3
{
internal class Program
{
private static void Kivetel()
{
throw new InvalidOperationException("Valami hiba történt a szálban");
}
private static void Main(string[] args)
{
Thread thread = new Thread(Kivetel);
try
{
thread.Start();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
finally
{
thread.Join(); //Várakozás a szál befejezésére
}
}
}
}
A fenti példaprogram elsÅ‘ ránézésre jónak tűnhet, mert van egy kivételkezelÅ‘nk, ami a nem várt helyzeteket hivatott lekezelni, azonban a fenti kód nem úgy fog működni, mint amire számÃtunk. Ennek az oka az, hogy a throw utasÃtás egy nagyon szofisztikált goto, ami a hÃvási stack-ben a legközelebbi olyan catch blokkra ugrik, ami el tudja kapni az adott kivétel tÃpust. Ez pedig azért probléma nekünk többszálú környezetben, mert minden egyes szál, amit indÃtunk külön stack-el rendelkezik, ebbÅ‘l adódóan ha egy szálon keletkezik egy kivétel, akkor azt a fÅ‘ szálon elhelyezett kivételkezelÅ‘ sosem fogja tudni elkapni.
Éppen ezért, ha a külön szálon végzett művelet esetén fennáll a kockázata annak, hogy kivétel keletkezzen, akkor azt a szálon futtatott kódban kell elkapnunk és megfelelően lekezelnünk, mivel a szálon keletkező nem kezelt kivétel a program összeomlását okozza, mint ahogy azt a program kimenetében láthatjuk:
Unhandled exception. System.InvalidOperationException: Valami hiba történt a szálban
at ThreadPelda3.Program.Kivetel() in K:\szalkezeles\Peldakod\Program.cs:line 10