Get Even More Visitors To Your Blog, Upgrade To A Business Listing >>

Multithreading


      A thread (folyamatszál, eseményszál), a folyamat részét képezi. A
folyamat az alkalmazás futó példánya, mely több szálra szakadhat. A folyamat
indulásakor először mindig a fő eseményszál indul el, majd abból kisebb
mellékszálak ágazhatnak ki és haladhatnak a főszállal párhuzamosan. Eleinte
csak folyamat létezett, azaz egyszerre csak egy eseménnyel tudott a processzor
foglalkozni. Minden feladat (task), amit a processzor el kellett végezzen külön
folyamatot jelentett. Hogy a feladatokat ne szerre, hanem egyszerre végezze a
processzor, bevezették a multitasking módszert, amely abból állt, hogy a futó
folyamatok ideiglenesen megállnak felszabadítván az erőforrásokat más
folyamatok számára, majd idővel visszatérnek és befejezik a feladatot. Mindez
nagyon gyorsran és sűrűn van ütemezve és úgy tűnik, hogy párhuzamosan dolgoznak
a folyamatok, pedig váltogatják egymást. Gyakorlatilag csak akkor futhat
egyszerre több folyamat, ha a számítógép több processzort tartalmaz. Hogy egy
folyamat mennyi ideig használhatja a processzort és a memóriát, az az ütemező algoritmustól
függ, amit többnyire a platform alapján választanak ki. A multitasking
jelentősen javította a számítógépek hatékonyságát, azonban a bonyolult
programoknál szükségessé vált elválasztani az adatgyűjtő, adatfeldolgozó és
eredményszolgáltató folyamatokat. Ettől fogva hatékonyabb egytüttműködésre volt
szükség a folyamatok között az adatok kommunikálása szempontjából, amit csak
úgy lehetett megvalósítani ha a teljes memóriaterületen osztozhattak. Az ilyen
típusú folyamatokat nevezik szálaknak vagy thread-eknek. A szálak tehát olyan
folyamatok melyek egyazon memória kontextusban dolgoznak megosztván
erőforrásaikat a fő folyamattal. A folyamatok nem osztanak meg semmit, csupán
felszabadítanak és újból lefoglalnak. A szálakat is ütemezők kapcsolgatják,
viszont az átkapcsoláshoz nem tartozik hozzá a memória kontextus átkapcsolása
is. Ez azt jelenti, hogy a szálaknak közös változóik, adatstruktúráik lehetnek,
melyekkel kommunikálhatnak egymással. Az ütemezés preemptív, azaz a szál vagy
folyamat futási jogát az ütemező (operációs rendszer) bármikor felülbírálhatja.
Ha egy felhasználói szinten futó szál váratlanul blokkolódik, akkor az ütemező
az egész folyamatot blokkolja. Kivételes eset, amikor az adott szálnak van
belső ütemezője (akár egy kernel szintű szálnak), ilyenkor átadja a vezérlést a
folyamaton belül egy másik szálnak. A preemptív ellentéte a kooperatív
ütemezés, amelyben a folyamatok futása nem áll meg, míg véget nem ér vagy
önként le nem mond. Ennek hátránya, hogy egy folyamat teljesen lefoglalhatja a
processzort ami bezavarhat az operációs rendszer működésébe is. Az ilyen
folyamatokat fiber-nek nevezik és a szálaktól csupán az ütemezés típusában
különböznek.





     
A szálak standardként a C++11-ben jelentek meg először, előtte a Boost, Poco
vagy egyéb könyvtárat illetve a WinAPI vagy a POSIX függvényeket lehetett
használni szálak létrehozására. Éppen ezért a korábbi C++ verziókkal fordított
forráskódok (amelyeket pl. a Visual Studio 6, 2005 vagy 2008-ban írtak)
többnyire platform függőek. A C++11 programozási nyelv olyan platformfüggetlen interfészt
nyújt, amely garantálja a RAII elvet (például megállítja az ott felejtett szálakat)
és a függvényobjektumokat használ függvénymutatók helyett.





     
Minden olyan alkalmazás amely egynél több szálat használ, többszálas
(Multithread) alkalmazásnak tekinthető. Például amikor egy szervernek több
kapcsolatot kell fenntartani, akkor egyszerűbb ha minden kapcsolathoz külön
szálat rendelünk így mindenik szál új socket-et hoz létre a hozzá tartozó
kapcsolatnak. Egy másik lehetséges felasználás a GUI alkalmazásokban lehet. Sok
esetben egy folyamat feladata, hogy várja a felhasználótól az adatbevitelt.
Ilyenkor nem szabad a többi folyamatnak megállnia és várnia a felhasználóra. Ha
nem túl bonyolultak a folyamatok, akkor érdemes egy folyamattá süríteni és azt
szálakra bontani. Ezek a szálak kommunikálnak egymással és folyamatosan firssen
tartják a felhasználói felület státuszát. Mivel a kommunikációt a közös
memóriaterület elérése révén végzik szükség van némi erőforrás-védő
mechanizmusra (mutexek, szemaforok, várakozási feltételek stb.), hogy
véletlenül se próbálja meg két szál ugyanazt a területet egyszerre használni. A
folyamatok (melyek nem osztoznak semmiben) is tudnak egymás között
kommunikálni, ám az sokkal bonyolultabb (pipe-ok, fájlok, socket-ek használatát
igénylik). Ennek is megvan a maga előnye a szálakhoz képest, mert működhetnek
fizikailag különálló gépeken. Ilyen osztott program az NFS szerver, az FTP
kliens-szerver, a Telnet, a chat-programok és a böngészők.





     
A folyamatok nem tudnak más folyamatokat létrehozni, mégcsak meg sem
tudják szakítani saját magukat. Az a folyamat, amely meghívja a fork()
függvényt csupán egy másolatot készít önmagáról, melynek saját változói és PID
azonosítója (Process ID) lesz. Ezt az ütemező is külön kezeli és szinte
függetlenül kezeli a szülő folyamattól. Ezzel szemben, amikor a folyamat egy
thread-et készít, akkor bár annak is meglesz a saját stack-je (helyi változói),
de a globális változók, a fájlleírók, a jelkezelők és a könyvtárállapot közös
marad a szülőfolyamattal.





WinAPI szálak (windows.h)





     
Minden folyamat rendelkezik egy fő szállal (main thread). A következő
programban a főszál feladata lesz, hogy várja a billentyűzetről érkező adatot
és írja ki a
counter változót, ha a bemenet különbözik a „q”
karaktertől. Ebből leágazik egy másik szál, amely folyamatosan növeli a
counter változót a beolvasástól függetlenül. A szál
létrehozásáért a CreateThread
függvény felelős, melynek a következő paraméterei vannak:




  1. lpThreadAttributes
    (típusa LPSECURITY_ATTRIBUTES): opcionális paraméter, amely egy mutató amivel
    megadható, hogy a szál által visszatérített handle örökölhető legyen-e a
    gyermekfolyamatok számára (amiket a CreateProcess függvénnyel hozhatunk létre).
    A NULL mutató nem örökölhető handle-t jelent.

  2. dwStackSize
    (típusa SIZE_T): a szál stack-jének kezdeti méretét lehet itt megadni. Ha nem
    pontos, akkor a rendszer kerekíti, de ha zéró, akkor a szál a folyamat méretét
    kapja.

  3. lpStartAddress
    (típusa LPTHREAD_START_ROUTINE): itt lehet megadni azt a mutatót, amely a szál
    által futtatni kívánt függény címére mutat (függvénymutató). Ez a szál kezdő
    címe, ahol egy speciális felépítésű függvény található:
    DWORD WINAPI
    myThread(LPVOID lpParameter);

  4. lpParameter
    (típusa LPVOID): ez egy olyan opcionális bemenő paraméter, amit a
    myThread függvény módosíthat.

  5. dwCreationFlags
    (típusa DWORD): egy flag, ami jelzi, hogy a szál elkészült.

  6. lpThreadId
    (típusa LPDWORD): kimenő paraméter, amelybe a szál azonosítója kerül.





Ezek az adattípusok WinAPI típusok és
tulajdonképpen típusdefiníciók vagy álnevek különböző struktúrákra vagy
standard adattípusokra. Például az LPVOID a WinDef.h-ban van definiálva mint
typedef void *LPVOID.





#include


#include


using
std::cout;


using
std::endl;





DWORD WINAPI myThread(LPVOID lpParameter)


{


     int& counter = *((int*)lpParameter);    // cast az eredeti int típusra


     while (counter // 2.147.483.647-ig számol


           ++counter;


     return 0;


}





int main()


{


     int myCounter = 0; // a szál ezt fogja növelni amikor elindul


     char myChar = ' '; // kezdeti karakter


     DWORD ID;          // thread ID


                        //          1     2   
3           4      5   
6


     HANDLE
myHandle = CreateThread(NULL, 0, myThread, &myCounter, 0, &ID);





     while (myChar != 'q') {


           cout


           myChar
= getchar();


     }





     CloseHandle(myHandle);


     return 0;


}





A kimenet:


0





901616379





1409959160





2057832213





2147483647





2147483647


q





A szál csak addig él, míg a myThread függvény vissza nem tér, azaz amíg a counter el nem ér az int maximális értékéig. A program minden billentyűnyomáskor kiírja a myCounter aktuális értékét, és ha a lenyomott billentyű a „q”
karakter, akkor az ismétlő ciklus véget ér és elindul a
CloseHandle függvény. Ennek paramétere a CreateThread handle-je, ami egyedi akár a létrehozott
szál ID-ja, csakhogy ha nem sikerül létrehozni a szálat, akkor a handle az, ami
biztosan nulla lesz.


     
A CRT (C Run-Time) függvények nagy része remekül működik a CreateThread
függvénnyel létrehozott szálakban, azonban vannak olyan függvények, melyek
memóriavesztést produkálnak a szál bezárásakor. Ilyen például sz strlen() vagy
a signal() függvény, ami nem készteti a szálat a CRT inicializálására, így a
CRT függvény által használt memória nem szabadul majd fel amikor a szál bezár (70-80
Byte memóriáról van szó minden záráskor). Hogy ez ne történjen meg, érdemes a _beginthread vagy a _beginthreadex függvényt használni.
Ezek első sorban a pramétereik számában különböznek:


- _beginthread(start_address, stack_size,
arglist)


- _beginthreadex(security, stack_size,
start_address, arglist, initflag, thrdaddr)


A paraméterek a következők:




  • start_address
    (típusa void*): annak a függvénynek
    a címe, ahonnan a szál indul.

  • stack_size
    (típusa unsigned): a szál stack-jének kezdeti mérete.

  • arglist
    (típusa void
    *): a szálnak
    átadott argumentumok listája.

  • security
    (típusa void*): a szál által
    visszatérített handle örökölhető legyen-e a gyermekfolyamatok számára. (NULL =
    nem örökölhető)

  • initflag
    (típusa unsigned): a szál kezdeti státusza (azonnal induljon, legyen
    felfüggesztve stb.). Felfüggesztett státuszban létrehozott szál a ResumeThread
    függvénnyel indítható be.

  • thraddr
    (típusa unsigned
    *): a szál azonosítója
    kerül ebbe a változóba.





A _beginthread
és a _beginthreadex még abban is
különbözik, hogy míg a _beginthread() bezárja a handle-jét miután véget ér a
szál, addig a _beginthreadex()-nek szüksége van a CloseHandle()-re a handle
bezárásához, akár a CreateThread esetén.





A két függvény a szálat képviselő
függvények hívásában is különbözik: a  _beginthread()
a _cdecl (natív, operációs
rendszertől függő, alapértelmezett) vagy a _clrcall
(menedzselt, operációs rendszertől független, virtuális függvény) híváskonvencióval
hívja meg a start_address címen lévő függvényt, a _beginthreadex() pedig az _stdcall (natív, operációs rendszertől
függő, Win32 API függvény) vagy a _clrcall
konvenciókat használja.





A következő program háromféleképp alkot
szálakat: CreateThread, _beginthreadex és _beginthread.





#include


#include


#include


using std::cout;


using std::endl;





DWORD WINAPI
mythreadA(LPVOID lpParameter)


{


      cout "CreateThread
ID: "
GetCurrentThreadId()


      return 0;


}





unsigned int __stdcall
mythreadB(void* data)


{


      cout "_beginthreadex
ID: "


      return 0;


}





void mythreadC(void* data) //alapértelmezetten
_cdecl híváskonvenció


{


      cout "_beginthread
ID: "


}





int main()


{


      HANDLE myhandleA, myhandleB, myhandleC;





      myhandleA = CreateThread(0, 0, mythreadA, 0, 0, 0);


      WaitForSingleObject(myhandleA, INFINITE);


      CloseHandle(myhandleA);





      myhandleB = (HANDLE)_beginthreadex(0, 0, &mythreadB, 0, 0,
0);


      WaitForSingleObject(myhandleB, INFINITE);


      CloseHandle(myhandleB);





      myhandleC = (HANDLE)_beginthread(&mythreadC, 0, 0);


      WaitForSingleObject(myhandleC, INFINITE);





      return 0;


}





A CreateTthread() által meghívott függvény
a standarad
DWORD WINAPI típust kell, hogy használja, a _beginthreadex()
függvényének típusa
unsigned int __stdcall és a _beginthread() függvénye void
típusú. Mindhárom függvény kiírja a saját azonosítóját. A
WaitForSingleObject
függvény egy objektumra vár, hogy az jelezze készenlétét. Ebben az esetben egy
HANDLE típusú objektumra
vár, ami akkor lesz kész, amikor az általa által képviselt szál a végéhez ér
(ezt jelzi az INFINITE paraméter). Ez egyfajta szünetet jelent a processzornak,
hogy ne lépjen tovább a következő utasításra, amíg a szál be nem fejeződik.
Mindhárom szál létrehozása után szerepel, így azok egymást megvárva sorban
fognak elindulni, mint mikor nincs szükség párhuzamos futásra.





A kimenet:


CreateThread ID:
8100


_beginthreadex
ID: 6176


_beginthread ID:
7204





A _beginthread() egyszerűbb, mert nincs
szüksége annyi paraméterre és nem kell a handle-jétt a CloseHandle()
függvénnyel bezárni, ellenben mégis a _beginthreadex() függvényt előnyösebb
használni. Amikor a _beginthread() szál véget ér, a visszatérített handle
érvénytelen lehet vagy pedig újra felhasználható. Ez akkor jelent problémát,
mikor egy _beginthread() szál véget ér, és egy másik indul el, amely ugyanazt a
handle értéket kapja mint a véget ér szál. Ha ezután ellenőrizni szeretnénk az
első szál handle-jét, akkor valójában a másik szál handle-jét ellenőrizzük.
Ilyenkor a WaitForSingleObject() sem biztos, hogy a megfelelő szál befejezésére
vár. Mivel a _beginthreadex() esetében a handle-t csakis kézzel lehet lezárni,
ilyen hiba nem fordulhat elő.





A WaitForSingleObject() társa a
WaitForMultipleObject(), mely egyszerre több szálra várakozik.





A szálakat többféleképp be lehet zárni, de
a legjobb, ha abban a függvényben ér véget, amit kezdetben elindított. A fenti
példákban az ID kiírása után rögtön véget is értek a szálak. Létezik az ExitThread() vagy a TerminateThread() függvény is, ám ezek
használata félbeszakíthat egy olyan műveletet, amely a programot
meghatározatlan állapotba hozza, például nem szabadulnak fel memóriaterületek
vagy a közös változókba érvénytelen értékek kerülnek. A _beginthread() és
_beingthreadex() társai az _endthread()
és az _endthreadex() függvények.
Ezek úgy vannak megírva, hogy felszabadítják a lefoglalt memóriát. Az
_endthreadex() után a handle-t továbbra is kézzel kell bezárni.





#include


#include


#include





unsigned int __stdcall
mythread(void* data)


{


      printf("Thread %d\n",GetCurrentThreadId());


      return 0;


}





int main()


{


      HANDLE myhandle[2];





      myhandle[0] = (HANDLE)_beginthreadex(0, 0, &mythread, 0, 0,
0);


      myhandle[1] = (HANDLE)_beginthreadex(0, 0, &mythread, 0, 0,
0);





      WaitForMultipleObjects(2, myhandle, true,
INFINITE);





      CloseHandle(myhandle[0]);


      CloseHandle(myhandle[1]);





      return 0;


}





A kimenet:





Thread 8044


Thread 6276





A cout() helyett a printf() kiíró függvény volt használva, hogy egyetlen
utasításként legyen a „Thread” szöveg és az ID kiírása értelmezve. A
WaitForMultipleObjects() első paramétere a szálak számát határozza meg, a második egy mutató a
handle objektumokat tartalmazó tömbre. A harmadik paramétert ha igazra
állítjuk, akkor addig vár, amíg minden szál be nem fejeződik, viszont a hamis
azt jelentené, hogy elég az egyik szál befejezését megvárni. Az utolsó
paraméter a várakozási idő a szál(ak) befejezéséig.





Ha a szálat felfüggesztett állapotúra
inicializáljuk a létrehozásánál, akkor nem fog rögtön elindulni. A már futó
szálat is fel lehet függeszteni a SuspendThread() függvénnyel, ám ez veszélyesebb
művelet, mert az általa használt erőforrások (pl. mutexek) is felfüggesztődnek.
A felfüggesztett állapotból a ResumeThread() hozza vissza a szálat futó
állapotba. A következő program egy billentyűnyomásra vár, hogy elindíthassa a
szálat.





#include


#include


#include


using std::cout;


using std::endl;





unsigned int __stdcall
mythread(void* data)


{


      cout "Thread ID:
"
GetCurrentThreadId()


      return 0;


}





int main()


{


      HANDLE myhandle;





      myhandle = (HANDLE)_beginthreadex(0, 0, &mythread, 0,
CREATE_SUSPENDED, 0);


     




This post first appeared on Altair Gate - News, please read the originial post: here

Share the post

Multithreading

×

Subscribe to Altair Gate - News

Get updates delivered right to your inbox!

Thank you for your subscription

×