A számítógépünk processzora sorban1 hajtja végre az utasításokat a program futása közben. Ez alapvetően csak egy program futását teszi lehetővé. Azonban már az 1970-es években felhasználói igény mutatkozott arra, hogy egy számítógép egyszerre több programot tudjon futtatni[^2]. Így születtek meg a többfeladatos (multitasking) operációs rendszerek és velük együtt a többszálú programozás, valamint egy külön problémakör.
Egy processzor által feldolgozható adatmennyiséget két dolog befolyásol alapvetően. A bitek száma és az órajel. A bitek száma az egy lépésben feldolgozott adatmennyiséget határozza meg, az órajel pedig az egy másodperc alatt elvégezhető lépések számára van hatással. A bitek számának növelése a processzor kompexitását befolyásolja és nem lineárisan növeli a sebességet, ha duplázzuk a bitek számát. Például ha 32 bites számokat adunk össze, akkor attól, hogy 64 bites processzoron fut a kód, még mágikusan nem lesz 2x olyan gyors a folyamat. Ehhez a kódot is adaptálni kell, hogy egy lépésben két számot dolgozzon fel. Ezzel szemben az órajel növelése kódmódosítás nélkül tudja növelni a sebességet, de ez sem a végtelenségig.
Sajnos fizikai határai vannak annak, hogy egy processzor mekkora órajelen tud működni és a mostani processzorok igencsak ezen határvonal mentén táncolnak már egy jó ideje. É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 (angolul 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 (angolul 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.
A párhuzamos végrehajtás két vagy több feladat párhuzamos elvégzését jelenti. A szálak a concurrency (párhuzamosság) kialakítására használhatók, míg a párhuzamos végrehajtás általában több processzoron vagy magon történik, és valódi párhuzamosítást eredményez.
Mivel egy modern operációs rendszerben több folyamat és szál fut “egyszerre”, mint amennyi processzor van a rendszerben, az operációs rendszerek időosztásos módszerrel váltogatnak közöttük. 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 egyszerűen 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.
Felhasználói élmény szempontjából elkerülhetetlen, hogy a folyamatok ne tudjanak egymással kommunikálni. Erre az operációs rendszerek számos megoldást kínálnak. Ezt nevezzük interprocess kommunikációnak. Ezen lehetőségek elérhetősége és megvalósítása operációs rendszerenként eltérő. Mivel itt memória izolációs határ átlépésről beszélünk, minden esetben az operációs rendszer közbenjárása kell.
Egy folyamaton belül, ha a szálaknak adatokat kell megosztaniuk, akkor azt szinkronizációval lehet megvalósítani. Szinkronizációra akkor van szükség, amikor mindkét szál a közös heap-en elhelyezett adatot kívánja írni. Értelemszerűen két valami egyszerre nem írhatja ugyanazon memória területet. Éppen ezért a programozó felelőssége, hogy megoldja, hogy egyszerre csak egy szál írjon osztott erőforrást.
Ennek módszere a zárolás. A zárolás többféleképpen is történhet: Az operációs rendszer közbenjárásával vagy programon belüli megoldásokkal is. Mindkettőnek megvan az előnye és a hátránya, de alapvetően elmondható, hogy a zárolás egy költséges művelet, mivel egy vagy több szálat várakozásra kényszerít. Éppen ezért általános cél egy többszálú alkalmazás esetén, hogy ezek számát csökkentsük vagy elkerüljük. Utóbbi nem minden esetben kivitelezhető.
Problémák
Többszálú alkalmazásoknál két komoly problémába ütküzhetünk. Az egyik ilyen a holtpont (deadlock), a másik pedig a versenyhelyzet (race condition).
A versenyhelyzet, vagy race condition egy olyan jelenség, amikor A és B kódrészek azonos időben futva befolyásolják egymás eredményét. Ennek hatására a program végrehajtása determinisztikus állapotból nemdeterminisztikusba kerül, vagyis a kódot nézve nem fogjuk tudni megmondani, hogy miért nem az történik, amit írtunk.
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. A probléma jellegét az alábbi kis szösszenet jól szemlélteti:
Interjún:
– Mesélje el, mi az a deadlock és felvesszük!
– Vegyen fel, és elmondom, mi az a deadlock!
Bár két komoly problémáról van szó, a jó hír az, hogy ezek a legtöbb esetben programozói hibára vezethetőek vissza. Ez azt jelenti, hogy több módon is védekezhetünk ellenük. Ezekről a fejezet későbbi részeiben lesz szó.
-
A modern processzorok esetén nem garantálható a sorrendi végrehajtás. Egy modern processzor több végrehajtó egységgel rendelkezik, még akkor is, ha csak egy magja van. Két végrehajtó egység esetén, ha a programunk xxy utasításokból áll é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. A mi szemszögünkből nézve sorban következett a végrehajtás, de valójában nem. (Ezt nevezzük szuperskalár működésnek.)↩