Már az 1970-es években, a Unix megjelenésekor igény mutatkozott arra, hogy egy számítógép több programot tudjon futtatni egymás mellett. Ennek következtében születtek meg a többfeladatos (multitasking) operációs rendszerek és velük együtt a többszálú programozás és annak a problémaköre.
A többfeladatos operációs rendszerek egyszerre képesek több feladat végrehajtására. Ehhez több trükköt is alkalmaznak, de alapvetően a működés az, hogy nem egyszerre fut az összes program, hanem időszakosan váltogat közöttük az operációs rendszer.
Ahhoz, hogy ez minél gyorsabban meg tudjon történni és a felhasználó ebből semmit ne vegyen észre, a hardver rengeteget fejlődött. Programozói szempontból nézve a processzorok az utasításokat sorban, egymás után hajtják végre. Azonban hardver szinten ez már nem feltétlen igaz. A modern processzorok úgynevezett szuperskalár működést alkalmaznak, aminek a lényege az, hogy több végrehajtó egység van, ami műveleteket tud elvégezni. Ha két végrehajtó egységünk van és a programunk xxy utasításokat akarja végrehajtani és x utasítás végrehajtása 1 időegységet venne igénybe, míg y végrehajtása 2 időegységet, akkor előfordulhat, hogy az egyik végrehajtón elkezdődik a 2db x utasítás lefuttatása, míg egy másikon az y utasítás. Így lényegében 4 időegység helyett 2 időegység alatt lefuttatható a processzor által a kód. Ezzel programozástechnikai szempontból nincs dolgunk, mivel ez processzor szinten történik.
Sokáig úgy tűnt, hogy ez a működés a végtelenségig skálázható, de kiderült, hogy ennek a megoldásnak is vannak limitációi. Éppen ezért kellett egy más módszert találni, amivel a programok működése gyorsítható. Ez pedig a többszálúsítás lett és ehhez asszisztálva a gyártók elkezdtek többmagos / több szál párhuzamos futtatására alkalmas processzorokat gyártani.
De mi is egy szál? A szál (Thread) egy olyan önálló kód egység, amely egy folyamaton belül tud futni és lehetővé teszi, hogy egyszerre több feladatot végezzen el.
A folyamat (Process) egy önállóan futó program példánya. Minden folyamat saját memóriaterülettel rendelkezik, és elkülönül egymástól. Egy folyamat tartalmazhat több szálat, de minimum egy szállal rendelkezik. A folyamatok életciklusát és a memória izolációt az operációs rendszer menedzseli.
Időosztás tekintetében két fő operációs rendszer család létezik. Vannak a valós idejű és a nem valós idejű operációs rendszerek, vagy másnéven az általános operációs rendszerek. Utóbbi kategóriába tartozik a Windows, Linux és bármelyik napi felhasználásra szánt operációs rendszer. Ezek az operációs rendszerek prioritásként az általános felhasználói kényelmet és a sokoldalúságot kezelik, és nem garantálják a szigorú válaszidőket.
Ezzel szemben a valós idejű operációs rendszer egy olyan speciális rendszer, amely a folyamatokat olyan módon ütemezi, hogy a feladatokat meghatározott időkereten belül (a határidők betartásával) hajtsa végre. Ezek az operációs rendszerek a kritikus időzítést és a pontos válaszidőt prioritásként kezelik, és általában fel vannak készítve arra, hogy speciális feladatokat, például repülésirányítást, autóipari vezérlést vagy orvosi eszközök kezelését szolgálják.
A többszálú programozásnál beszélnünk kell a concurrency és a parallelism fogalmakról. Sajnos mindkét fogalom magyarul párhuzamosságot is jelent, így sokszor tévesen egymás szinonímájaként kezeljük őket, de ez nem igaz.
A concurrency arról szól, hogy egy rendszer hogyan kezel több feladatot, amelyek időben átfedhetik egymást. Ez a feladatok logikai szervezésére és kezelésére vonatkozik, nem feltétlenül azok egyidejű fizikai végrehajtására. Egyetlen processzormagon is megvalósítható. A feladatok nem futnak feltétlenül egyszerre, hanem a rendszer gyorsan váltogat közöttük, így úgy tűnik, mintha egyszerre haladnának. A fő cél itt a rendszer válaszkészségének javítása, erőforrások hatékonyabb kihasználása.
A parallelism arról szól, hogy több feladat valóban egyszerre fut több feldolgozó egységen. Ez a feladatok fizikai, egyidejű végrehajtására vonatkozik. Ez több processzormagot vagy feldolgozó egységet igényel. Minden feladat vagy annak egy része egy különálló magon fut egyidejűleg. A fő cél itt a számítási teljesítmény növelése, a feladatok gyorsabb befejezése.
OS alapok
Az operációs rendszer szempontjából nézve a legkisebb egység a szál. Egy folyamat több szálból is állhat. Ezen szálak ugyanabban a memória térben működnek és ugyanahhoz a heap-hez van hozzáférésük. Minden egyes szál saját stack területtel rendelkezik. Eddig csak olyan programokat írtunk, amelyek egy szálból álltak.
A folyamat egy magasabb szintű izolációs egység. Egy folyamat saját memóriaterülettel rendelkezik és egy másik folyamat memóriaterületéhez és az operációs rendszer saját memóriaterületéhez sem fér hozzá csak úgy. Ezen izoláció biztonsági szempontból elengedhetetlen és ebből adódóan a processzorok külön hardver egységgel rendelkeznek ennek a kikényszerítéséhez. Ez az úgynevezett védett mód. Az operációs rendszer magja, főprogramja ezen védett módban fut és csak neki van közvetlen hozzáférése a fizikai memóriához. Ezt ki tudja osztani a folyamatok között, amik egy virtuális memória címterületet látnak, mintha csak ők futnának a hardveren.
Ezen virtuális címtér és a tényleges fizikai memória címtér között az operációs rendszer konvertálgat és a védett módhoz tartozó hardver megakadályozza, hogy egyik folyamat a másik memóriaterületét tudja módosítani az operációs rendszer engedélye nélkül.
Ha két folyamat között szeretnénk kommunikálni, ahhoz általában egy Inter Process Communication (IPC) megoldást kell alkalmaznunk. Ezekről a későbbiekben lesz szó.
Ha egy folyamaton belül adatot szeretnénk megosztani, akkor viszonylag „egyszerű” dolgunk van, mivel ugyanabban a memória címtérben vagyunk. Az egyszerűség nem véletlenül került idézőjelbe, mert bőven van probléma, amibe ütközhetünk.
Problémák
Alapvetően két komoly problémával találkozhatunk többszálú programok esetén: az egyik a holtpont (deadlock), a másik pedig a versenyhelyzet (race condition) probléma. Mindkettő ahhoz kapcsolódik, hogy mindig lesznek olyan erőforrások, amikből kevesebb van, mint szál.
Ilyen tipikusan például a konzol. Egy folyamat egy konzollal rendelkezik és értelemszerűen két vagy több folyamat egyszerre nem tud a képernyőre írni. Tételezzük fel, hogy van két szálunk, amelyek a képernyőre szeretnének írni egyszerre, mondjuk az A és B szöveget. Ebben az esetben az operációs rendszer ütemezője és a hardver működésének következtében nem garantálható, hogy a szöveg AB sorrendben jelenjen meg a képernyőn. Előfordulhat, hogy BA lesz a sorrend, de az is előfordulhat, hogy csak A jelenik meg és az is előfordulhat, hogy B jelenik meg. Ezt nevezzük versenyhelyzetnek.
Ez a versenyhelyzet zárolással védhető ki. Ennek lényege, hogy a kritikus, nem párhuzamosítható részekhez (pl. fájlba írás) egyszerre csak egy szálnak engedünk hozzáférést. Amíg az erőforrás zárolva van, addig a másik szál nem tud hozzáférni az adott erőforráshoz, így nem tud problémákat okozni.
Ezzel azonban bevezettünk egy másik problémakört, aminek az eredménye deadlock tud lenni. Deadlock-ról akkor beszélünk, amikor két vagy több szál egymást blokkolja, mivel mindegyik vár valamilyen erőforrásra, amit egy másik tart foglalva. Ennek eredményeként egyik folyamat sem tud előrehaladni, és a rendszer megreked.
Ezen problémák megtalálása sok esetben nem triviális, ha az alkalmazásunkban belefutunk, de a jó hír az, hogy ezek programozói hibára visszavezethető módon alakulnak ki, így tudunk ellenük védekezni. Ezekről későbbiekben lesz szó.
.NET esetén a szálak kezeléséhez használt típusok a System.Threading névtérben kaptak helyet.