C# - Szálak menedzselése C#-ban

forráskód letöltése
Alkalmazásainkban nagyon gyakran kell használnunk a többszálas technológiát, vagyis bizonyos időigényes folyamatokat a főszáltól eltérő szál segítségével kell végrehajtanunk, hogy a főszál terhelése csökkenjen. Ennek megoldására a .NET beépített lehetőségeket kínál. Speciális osztályai segítségével egy menedzselt listában helyezhetjük el a folyamatokat végző elindított szálakat, melyekről a továbbiakban a futtatórendszer gondoskodik. Cikkünkben ennek megvalósítására mutatunk egy példát.
Szálmenedzselés (thread pooling)
A .NET Framework segítségével lehetőségünk van hatékonyabb több szálon futó alkalmazást írni. Számtalan alkalmazás használja a többszálas technológiát, azonban a legtöbb esetben egy-egy szál nagyon sok időt tölt várakozással egy adott eseményre.
A .NET rendszerben egy olyan komplex szálmenedzselő algoritmust kapunk készen, mely egy csokorba gyűjti az adott process WORKER szálait úgy, hogy a szálak közt a lehető legoptimálisabban osztja el a processzoridőt. Ennek eredményeképpen a fejlesztő koncentrálhat az alkalmazás logikájára ahelyett, hogy a szálak menedzselésével kellene törődnie.
Olyan esetekben tehát, ahol több rövid, de külön szálat igénylő feladatot kell megoldanunk, használhatjuk a ThreadPool osztály lehetőségeit, így a többszálú programozás minden előnye rendelkezésünkre áll.
A beépített szálmenedzser lehetővé teszi, hogy a rendszer optimalizálja a szálak tevékenységét a lehető legnagyobb eredmény elérése érdekében az adott process esetében, továbbá a számítógép egyéb, a szálakkal kapcsolatban álló egyéb process-ek esetében egyaránt. Mindezekről az alkalmazás természetesen mit sem sejt.
A .NET szálmenedzselő rendszere a következő esetekben használható igen jól:
  • Aszinkron hívások esetében
  • Socket kommunikációt használó kód esetén
  • Aszinkron I/O tevékenység esetén
Menedzselt kód esetén a ThreadPool.QueueUserWorkItem statikus metódus meghívásával adhatunk egy adott szálat a gyűjteménybe, és a WaitOrTimerCallback delegált segítségével adhatjuk át annak a metódusnak a címét, melyet az adott szál végrehajt.
Egy alkalmazás domain egy ThreadPool objektumot tartalmaz, mely akkor jön létre, amikor első alkalommal hívódik meg a ThreadPool.QueueUserWorkItem metódus. Amikor egy task befejeződik, akkor meghívja a megfelelő CALLBACK metódust. A működési elvhez az is hozzátartozik, hogy miután egy szál besorolásra került a menedzselt kollekcióba, azt onnan eltávolítani nem lehet.
A menedzselt kollekcióba felvehető szálak számát csak a PC-ben rendelkezésre álló memória szabja meg, ugyanakkor az algoritmus megszab egy bizonyos maximális szálszámot, melynél még biztonságosan üzemeltethető a szálak szimultán futtatása.
Minden szál az alapértelmezett vermet használja, az alapértelmezett prioritással fut, és a többszálas apartment-ban található (MTA). Abban az esetben, ha a sorban található valamennyi szál foglalt, de van még függőben levő elvégzendő feladat, a szálmenedzselő algoritmus néhány időperiódus után létrehozza a további szükséges munkaszálat.
A szálakkal történő műveletvégzést csak bizonyos speciális esetekben nem ajánlatos a menedzserre bízni. Ezek a következők:
  • A szálakhoz egyedi prioritást szeretnénk beállítani.
  • Van olyan művelet, melynek elvégzése hosszabb időperiódust igényel.
  • Ha a szálat egy single-threaded apartment-ban szeretnénk elhelyezni (STA).
  • Ha stabil azonosítóval kívánjuk ellátni az adott szálat.
Gyakorlati felhasználás
A példánkban a gombra kattintva létrehozhatunk három szálat, melyek bekerülnek a ThreadPool objektum által reprezentált gyűjteménybe, majd minden szálobjektum elkezdi a megadott I/O műveletet úgy, hogy a projekt mappájában megtalálható File.txt állomány 4 sorát kiolvassa egy karakterláncba, majd azt megjelenítse a program ListBox kontroljában.
A gomb lenyomásakor példányosítjuk az AutoResetEvent osztályt, melynek feladata, hogy értesítse az éppen várakozó szálakat arról, hogy egy esemény bekövetkezett. Az objektum addig marad jelző (signaled) állapotban, míg az adott várakozó szál el nem kezdi a műveletet. Ekkor a rendszer automatikusan nem-jelző állapotba állítja az objektumot (nevezhetjük ezt egy szemafornak). Amennyiben nincs várakozó szál, akkor az objektum jelző-állapotban marad.
AutoResetEvent ev = new AutoResetEvent(false);
Ezt a delegáltat a ThreadPool objektum RegisterWaitForSingleObject metódusa segítségével regisztrálhatjuk.
ThreadPool.RegisterWaitForSingleObject(ev, new WaitOrTimerCallback(TimeoutMethod), null, 20000,false);
A metódus második paraméterében kell megadnunk azt a CALLBACK metódust, mely akkor kerül meghívásra, amikor a negyedik paraméterben megadott idő (timeout) lejár, vagy ha a fent deklarált objektum jelzett állapotba kerül.
A példánkban meghíváskor beírunk egy sort a ListBox kontrolba.
if (signaled)
{
  listBox1.Items.Add("TimeoutMethod .....");      
}
A szálak a QueueUserWorkItem metódussal kerülnek a kollekcióba, annak paraméterében megadva a meghívandó metódus nevét.
for(int i=0;i<3;i++)
{
  ThreadPool.QueueUserWorkItem(new WaitCallback(ThreadMethod), i);        
  ev.Set();
  Thread.Sleep(1000);
}
A metódusban az adott hívó szál a menedzser-algoritmus által meghatározott módon kiolvassa az állomány soron következő sorát, majd beírja azt a ListBox kontrolba, feltüntetve az éppen olvasó szálat.
StreamReader reader = new StreamReader("file.txt");      
string line = "";
switch(o.ToString())
{
  case "0":  
    for(int i=0;i<4;i++){
      line = reader.ReadLine();
      listBox1.Items.Add("Első szál -> " + line);
      Thread.Sleep(2000);
    }
    listBox1.Items.Add("Első szál bent a sorban ...");  
    break;
    ...
A program futásakor megfigyelhető, hogy a szálaknak a menedzser-algoritmus osztja a processzoridőt, így azok periodikusan váltakozva olvasnak egy-egy sort az állományból.