Úgy általában

Az eddigi programjaink mind egy szálon futottak, egy úgynevezett elsődleges szálon. Azonban a Windows 95/98, és Windows NT alá írt programok már elindíthatnak egy vagy több másodlagos szálat is, melyek egymástól függetlenül párhuzamosan futhatnak. A párhuzamosság gyakorlatilag persze azt jelenti, hogy az operációs rendszer hol az egyik szálat, hol másikat futtatja gyors egymásutánban. Ezt a Windows NT 3.5 óta bevezetett preemptív multitasking teszi lehetővé. Tehát a többszálúság API szinten támogatott. Ennek a lehetőségnek olyan programoknál vehetjük hasznát, ahol egyszerre több feladatot kell végrehajtanunk. Szálak (Thread) alkalmazásával a program leegyszerűsödik és hatékonyabb lesz. Szálakat alkalmazhatunk az üzenetkezelés gyorsítására is. Ha egy lassú, vagy nagy számításigényű folyamatot az elsődleges szálban végzünk, az üzenetkezelés lelassul, vagy esetleg egészen addig nem működik, amíg a folyamat le nem zárul. A lassú folyamat egy másodlagos szálba helyezése megoldja a problémát. Ilyen külön szálba rendezett feladatok lehetnek hálózatra való connect-elés, vagy mondjuk a Word automata helyesírás-ellenőrzője, ami minden második szavamat piros hullámos vonallal aláhúzza, minden ötödiket pedig zölddel, én meg nem tudom, hogy hol kell lekapcsolni!

Egy szál elindítása viszonylag gyors folyamat, és kevés memóriát igényel. A Program minden szála ugyanabban a memóriatartományban fut, így ugyanazokat az erőforrásokat és globális változókat használják. Az erőforrások és globális változók ilyen megosztása problémákat is okozhat, ezért van szükség a szálak szinkronizálására.

A dolog MFC-ben

Mivel a dolog már API szinten támogatott, az MFC-nek nincs nehéz dolga a szálakkal, és sok a többszálú programozást elősegítő osztálya, eljárása van. De még az elején említsük meg, hogy bizonyos MFC korlátozások is vannak. Ezek kellemetlenek tudnak lenni, de annak érdekében születtek, hogy ne szálljon el mindig a program.

Project beállítások:

A Project/ Settings… dialógusablakban a C/C++ fület választva a Category listán válasszuk a Code Generation-t. A Use RunTime Library beállítást válasszuk MultiThread-re, vagy Multithread DLL-re. Debug konfigurációnál a Debug MultiThread-et, vagy a Debug MultiThread Dll-t válasszuk.

Ez egyébként a ClassWizard-al létrehozott programok esetében mindig helyesen van beállítva.

Új szál létrehozása

Egy új szál elindításához egy CWinThread osztályra van szükségünk, és az AfxBeginThread függvényt kell meghívni.

CWinThread* AfxBeginThread

( AFX_THREADPROC pfnThreadProc,
LPVOID pParam,
int nPriority = THREAD_PRIORITY_NORMAL,
UINT nStackSize = 0,
DWORD dwCreateFlags = 0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL );
Vagy:

CWinThread* AfxBeginThread

( CRuntimeClass* pThreadClass,
int nPriority = THREAD_PRIORITY_NORMAL,
UINT nStackSize = 0, DWORD dwCreateFlags = 0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL );
Az első függvény egy úgynevezett dolgozó (working) szálat hoz létre. Ezt használhatjuk folyamatok levezérlésére. A második egy Felhasználói Interface (User-Interface) szálat hoz létre. Egy ilyen szál a felhasználói merényletek, és események kezelésére használatos, maga a program elsődleges szála is egy ilyen szál, ritkán van szükség még egy létrehozására. Sokkal gyakrabban van szükségünk dolgozó szál (working Thread) létrehozására, ezért a továbbiakban ezzel foglalkozunk. Tehát tekintsük a függvény első megadását.

Az AfxBeginThread függvény elindítja az új szálat, és visszatérése után a két szál párhuzamosan fut. Az első paraméter (pfnThreadProc) a szál-függvényt (Thread Function) határozza meg. Ezt a függvényt kezdi az új szál végrehajtani. A függvényből természetesen hívhatunk újabb függvényeket, de ha a szál-függvény visszatér, a szál befejeződik. A pParam egy paraméter, amit a szál-függvény fog megkapni mikor meghívódik. A szál-függvényt a következőképpen kell megadni:

UINT MyThreadFunction (LPVOID pParam)

A függvény által visszaadott UINT a kilépési kód. Normális esetben a szál függvény nullát ad vissza, ami a rendeltetés szerinti kilépést jelenti, de használhatunk bármi más értéket is egyet kivéve, a STILL_ACTIVE -ot (0X00000103L), ami azt jelzi, hogy a szál még mindig fut. Az átadott pParam lehet egy egyszerű számra, vagy egy struktúrára mutató pointer, ami információkat tartalmaz a szál számára.

Az AfxBeginThread utolsó négy paramétere előre definiált értékekkel is rendelkezik, amik általában meg is felelnek, de azért nézzük.

Az nPriority a szál prioritását jelenti. Minél magasabb, annál több processzoridőt fog kapni az operációs rendszertől. Alapértelmezett értéke az átlagos prioritás THREAD_PRIORITY_NORMAL.

A prioritások növekvő sorrendben:
 

THREAD_PRIORITY_IDLE Csak akkor kap processzoridőt, ha többi szál nem fut.
THREAD_PRIORITY_LOWEST  Átlag alatt 2 ponttal
THREAD_PRIORITY_BELOW_NORMAL Átlag alatt 1 ponttal
THREAD_PRIORITY_NORMAL Átlag
THREAD_PRIORITY_ABOVE_NORMAL Átlag felett 1 ponttal
THREAD_PRIORITY_HIGHEST Átlag felett 2 ponttal
THREAD_PRIORITY_TIME_CRITICAL Időkritikus szál. A többi szál nem nagyon kap időt.

A dwCreateFlags paraméter alapértelmezett értéke nulla. Ez azt jelenti, hogy létrehozás után a szál azonnal elindul. Ha CREATE_SUSPENDED értékkel hozzuk létre, akkor magunknak kell majd elindítani (CWinThread::ResumeThread). A többi paramétert nem kell piszkálni, a szálhoz tartozó verem méretét, és a biztonsági jellemzőket adhatnánk meg, ha nem lennének jók az alapértelmezett értékek.

Egy szál elindítása egyszerűen:

UINT Function(LPVOID pParam){     return(0);}Void proc(){     Int Attr=13;     CWinThread *PwinThread=AfxBeginThread (Function, &Attr)     .     .}
Szálak megállítása:

MFC-ben egy szálat, egy másik szálból nem tudunk megállítani. Egy szál csak akkor fog leállni, ha saját maga is, meg a párt is úgy akarja. Ezért, ha egy másik szálban derül ki, hogy neki meg kell állni, annyit tehetünk, hogy jelezzünk ezt a kívánságot számára (a később tárgyalásra kerülő megoldások bármelyikével), majd reménykedünk a jóindulatában.

Egy szál kétféleképpen állíthatja meg magát. Az egyszerűbb módja, ha egyszerűen a szál-függvényből visszatér. Ez egy szál befejezésének rendeltetésszerű módja. Ekkor felszabadul a szálnak lefoglalt verem, meghívódik az általa készített minden objektumra a destruktor. A másik út, hogy erőszakosan megállunk. Ezt az AfxEndThread függvénnyel tehetjük meg.

Void AfxEndThreat(UINT ExitCode);

Ezt a függvényt a szálon belülről bárhonnan meghívva azonnali leállást tudunk produkálni. Paraméterként a szál kilépési kódját kell csak megadni. Annyi szépséghibája van a dolognak, hogy a szálhoz tartozó verem felszabadítódik, de minden más lefoglalt memória, vagy létrehozott objektum a helyén marad. Ezért mielőtt behúznánk a kéziféket, tegyük meg a szükséges destruktorhívásokat, meg minden egyebet.

Szálak kezelése

A szál létrehozásakor egy CWinThread objektumra mutató poinert kaptunk vissza (a példában PWinThread). Ennek a pointernek a segítségével tudjuk a szálon kívülről is kezelni a szálat.
Egy szál felfüggesztéséhez a SuspendTread metódust hívhatjuk:

PWinThread -> SuspendThread();

Egy felfüggesztett szál újraindításához, illetve CREATE_SUSPENDED paraméterrel létrehozott szál elindításához a ResumeThread függvényt használhatjuk.

PWinThread -> ResumeThread();

Egy szál prioritási szintjét megváltoztathatjuk a SetThreadPriority függvénnyel.

PWinThread -> SetThreadPriority (THREAD_PRIORITY_HIGHEST);

Prioritási szintjét pedig a GetThreadPriority függvénnyel tudjuk lekérdezni.

Ezek mellett az MFC függvények mellett használhatunk a WinAPI-ban már meglévő függvényeket is a szálak kezelésére. Tipikusan ilyen a ::GetExitCodeThread. Le tudjuk vele kérdezni egy szál kilépési értékét, vagy ha még mindig fut, akkor STILL_ACTIVE-ot fogunk kapni.

DWORD ExitCode;
:: GetExitCodeThread(PWinThread->m_hThread, &ExitCode);

A függvénynek a szál Windows handler-jét kell megadni (m_hThread), és egy DWORD címét, ahova meg fogjuk kapni a kilépési értéket. Ha nem STILL_ACTIVE-ot kapunk, azt jelenti, hogy a szál már nem fut.

Azért van egy kis probléma ezzel a dologgal. Alapértelmezésben egy szál mikor befejezi a futását, a hozzá tartozó CWinThread objektum is megsemmisül. Így mikor feltesszük egy már lefutott szálnak a nagy kérdést, hogy futsz-e még (közben a már nem létező objektum m_hThread tagjára hivatkozunk), a Windows egy nem túl informatív, de annál aggasztóbb általános védelmi hiba című üzenetet fog megereszteni.

Úgy tudjuk megakadályozni, hogy a CWinThread objektum automatikusan legyilkolja magát, ha az m_bAutoDelete tagjának FALSE értéket adunk. Előtte a szálat felfüggesztve hozzuk létre, hogy biztos mi legyünk a gyorsabbak. Ugyanis, ha a szál lenne a gyorsabb, és már ki is lépett mire eljutunk az objektumra való hivatkozásig, szintén védelmi hibát generálnánk.

CWinThread *PwinThread=AfxBeginThread (Function,             &Attr,             THREAD_PRIORITY_NORMAL,             0,             CREATE_SUSPEND); PwinThread->m_bAutoDelete=FALSE; PwinThread->ResumeThread();

Ekkor viszont azzal a problémával találjuk szembe magunkat, hogy az objektum örökké élni akar. Miután biztos nincs már rá szükségünk, nekünk kell megszüntetni:

delete PWinThread;

Korlátozások MFC osztályokra.

Kétféle korlátozás van:

  1. Két szál nem próbálhat egyszerre ugyanahhoz az MFC objektumhoz hozzáférni. Természetesen megoszthatnak MFC objektumokat, de csak külön-külön férhetnek hozzá.
  2. Az alábbi osztályú, vagy abból származtatott osztályú objektumokat csak az őt létrehozó szál használhatja:
    -CWnd
    -CDC
    -Cmenu
    -CgdiObject
    Ezeknek az osztályoknak mind van olyan adattagjuk, amely a Windows handle-t tartalmazza. Ha egy másik szálból akarunk egy ilyen objektumot használni, át kell adni a szálnak ezt a handle-t, és a szál készít magának a handle-ből egy objektumot, (FromHandle metódus) amit szabadon csak ő használhat.
Felmerül a kérdés, mi van, ha nem tartjuk be ezeket a korlátozásokat. A fordító nem fog szólni érte, minden nyavalygás nélkül lefordítja, csak a program futása során érhetnek minket kellemetlen meglepetések. Könnyen előfordulhat, hogy egyszerűen nem áll le a szál, így ha a szál leállására várunk egy másik szálból, ott is meg fogunk fagyni. Persze ez a probléma csak akkor fog jelentkezni, ha már jóval előbb nem kaptunk egy védelmi hibát. De azért olyan is elképzelhető, hogy véletlenül tökéletesen működik. Hiába, a Windows útjai kifürkészhetetlenek.

Szálak szinkronizálása

Már az 1. Korlátozásból is adódik, hogy a szálakat valahogy szinkronizálni kell, hiszen tudnunk kell, mikor szabad hozzáférni egy áhított megosztott MFC objektumhoz. Akkor szabad hozzáférni, mikor más nem akar. Az ilyen kérdések eldöntéséhez kell használnunk az MFC nyújtotta, illetve az API által nyújtott szinkronizálási lehetőségeket.

A legegyszerűbb a kölcsönös kizárás (mutual exclusion) elve. Ezt, ahogy az összes többi szinkronizációs megoldást kétféleképpen használhatjuk. Használhatjuk az MFC-s megoldást (CMutex), vagy a Win32 API függvényeit.

Nézzük meg mindkét esetet:

A Win32 API-val egy példa:

Először deklarálunk globálisan egy mutex handle-t, hogy később minden szálból elérjük:

HANDLE Hmutex;

Ezután valahol a programban létrehozzuk a mutex-et:

Hmutex=::CreateMutex(NULL, FALSE, NULL);

Az első paraméter NULL, ez azt jelenti, hogy az alapértelmezett biztonsági jellemzőket elfogadjuk, és a mutex azonosító (handle) nem lesz örökölhető. A második paraméter FALSE, ez azt jelenti, hogy kezdetben a mutex jelezni fog. (Ha TRUE lenne, akkor kezdetben nem jelezne). A harmadik paraméter NULL, ez azt jelenti, hogy nem adunk meg a mutexnek nevet. Ha a mutexet nem fogjuk a process-en kívülről használni, akkor nem is lesz szükségünk névre. A függvény egy azonosítót ad vissza, amire a későbbiekben szükségünk lesz.

Ezek után következhet a tényleges kizárás. Egy ::WaitForSingleObject hívást kell elvégeznünk a programrészlet elején , ahol a "védeni kívánt erőforrást használjuk. A használat után pedig egy ::ReleaseMutex hívással felszabadítjuk a védett erőforrást.

::WaitForSingleObject(HMutex, INFINITE); strBuffer = _T("CSA! "); ::ReleaseMutex(HMutex);

A ::WaitForSingleObject függvénynek az első paramétere a mutex, a második, hogy mennyi időt vagyunk hajlandó várni, amíg a mutex nem jelez. Egy mutex akkor jelez, ha szabad, tehát éppen senki sem használja az áhított erőforrást. Ha nem jelez, akkor a paraméterként megadott időt várunk, hogy szabad legyen. INFINITE paraméter esetén bármeddig. A ReleaseMutex hívásával szabadítjuk fel a használt erőforrást.

Ahányszor bármelyik szálból használni akarjuk ezt az erőforrást, annyiszor meg kell hívnunk a ::WaitForSingleObject függvényt paraméterként mindig ugyanazt a mutex azonosítót átadva, majd a ::ReleaseMutex függvényt szintén ezzel az azonosítóval.

Annyi mutex-et kell csinálnunk, ahány erőforrást meg akarunk osztani.

A ::WaitForSingleObject egyébként más várakozásoknál is jó. Segítségével például megvárhatjuk egy szál lefutását:

bRun=FALSE; //megkerjuk allljon le ::WaitForSingleObject(m_PThread->m_hThread,INFINITE);       //megvarjuk mig leall

Ugyanez MFC-vel:

Deklarálunk egy CMutex objektumot valahol, ahol mindegyik szál látja:

CMutex m_mutex;

Majd ezután valahol meghívjuk a mutex konstruktorát:

m_mutex(FALSE,NULL);

Az eső paraméter azt jelzi, abból a szálból, ahol meghívtuk a konstruktort hozzá akarunk-e férni a mutex által vezérelt erőforráshoz. A második paraméter a mutex neve. Ha nem adunk meg, más processzekből nem tudjuk majd elérni.

Ezután következik a mutex vezérlése a szálból:

CSingleLock sLock(&m_mutex); sLock.Lock(); strBuffer = _T("CSA! "); sLock.Unlock();

Egy CSingleLock objektumot kell létrehoznunk, ennek segítségével zárjuk le, illetve szabadítjuk fel az erőforrást.
A további szinkronizációs objektumok részletes leírását mellőzném, inkább egy rövid táblázat:
 

Szinkronizációs lehetőség Célja Win32 API függvények MFC osztály
Mutex Megakadályozza, hogy több szál használjon egy erőforrást ::CreateMutex
::WaitForSingleObject
::WaitForMultipleObject
::ReleaseMutex
::CloseHandle
Cmutex
Kritikus rész Ugyanaz, mint a mutex, csak nem osztható meg processzek között ::InitalizeCriticalSection
::EnterCriticalSection
::LeaveCriticalSection
::DeleteCriticalSection
CcriticalSection
Szemafor Megszabja, hogy maximum hány szál használhat egy erőforrást ::CreateSemafor
::WaitForSingleObject
::WaitForMultipleObjects
::ReleaseSemaphore
::CloseHandle
Csemaphore
Esemény egy szál különféle jelzéseket küldhet más szálaknak ::CreateEvent
::SetEvent
::PulseEvent 
::ResetEvent
::WaitForSingleObject
::WaitForMultipleObjects
::CloseHandle
Cevent

A példaprogram bemutatja egy szál létrehozását, megszüntetését, megállítását, szálból más szál ablakára való rajzolást.
Erről ennyit