Amikor a C nyelvet megalkották, nem létezett több magos számítógép. Éppen ezért a C beépítetten nem tartalmaz nyelvi elemeket több szálon futó alkalmazáskészítéshez. Ez azonban nem jelenti azt, hogy nem is lehetséges. Azonban a szálak kezelése eléggé operációsrendszer függő. Eltérő szál modellt alkalmaznak a Windows és a Unix-szerű rendszerek.
A Linux a POSIX Thread (pthread) modellt alkalmazza. A POSIX a Portable Operating System Interface for uniX szavak rövidítése. Ez egy szabvány, amit anno azért alkottak, hogy biztosítsa a kompatibilitást és hordozhatóságot a Unix-szerű rendszerek között.
A Windows megalkotásakor a POSIX kompatibilitás a korábbi Windows és DOS alkalmazásokkal való kompatibilitás megtartása miatt nem volt fő cél, de a Windows NT kernele már úgy lett tervezve, hogy végtelen számú alrendszerrel kompatibilis legyen. Ez a Windows 10 esetén a WSL (Windows Subsystem for Linux) segítségével teljesedett ki, ami POSIX kompatibilis.
WSL nélkül is tudtunk pthread szálakat alkalmazó programot írni az eddig használt MINGW segítségével, illetve ha Visual Studio-t alkalmazunk, akkor a pthread-win könyvtárral (https://github.com/erikbasargin/pthread-win) tudjuk ezt elérni.
A C11 egyik újdonsága, hogy beépítetten tartalmaz támogatást szálak kezeléséhez. Ez azonban továbbra sem nyelvi elem, hanem egy szabványosított header fájl a <threads.h>, függvényekkel, amely operációsrendszer függetlenné teszi a szálak használatát, de ennek is megvannak a limitációi. Például az, hogy nem kötelező a szabvány részeként implementálni, hiszen a C kód operációsrendszer nélkül közvetlenül a vason is futhat, ahol nincs ütemező a szálak támogatásához. További érdekesség, hogy a <threads.h> API-ja a POSIX Thread kezelést vette alapul, így a könyv ezen részében erről lesz részletesebben szó.
A szálak használatbavételéhez kezdésképpen két fejléc állományt kell importálnunk: a unistd.h és pthread.h állományokat. A pthread.h állomány tartalmazza a szálak kezeléséhez szükséges eljárásokat, míg az unistd.h állomány vegyes Unix funkciókat biztosít.
Az első legfontosabb függvény a pthread_create, ami létrehoz egy új szálat. Ennek a szignatúrája a következő:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
Első paramétere egy pthread_t típusú változó, ami a szál azonosítására szolgál, második paramétere a szál viselkedésének meghatározására szolgál, általában NULL értékű, ami azt jelenti, hogy az operációs rendszer az alap beállításokat használja.
A harmadik paramétere azon függvény neve, amely a szálon futtatandó kódot tartalmazza. A függvénynek void * típusú paramétert kell átvennie és void * visszatérési értékkel kell rendelkeznie.
A függvény utolsó paramétere pedig a szálnak átadandó argumentumok void * típusban. A szál sikeres létrehozása esetén a függvény 0 értéket ad vissza.
Szálat megszüntetni két módon tudunk: a pthread_exit függvénnyel, aminek a paramétere egy void * a szál visszatérési értékeként, illetve a pthread_cancel függvénnyel. A kettő között az a különbség, hogy a pthread_exit segítségével a futó szál tud kilépni, míg a pthread_cancel segítségével egy másik szálról tudunk megszakítani egy futó szálat.
Ebből adódódóan a pthread_cancel függvény paramétere egy pthread_t típusú szál leíró.
void pthread_exit(void *retval);
int pthread_cancel(pthread_t thread);
Ha kevésbé drasztikus módon szeretnénk egy szálat megszüntetni, akkor érdemes megvárni, hogy az befejezze a feladatát. Erre a pthread_join függvényt alkalmazhatjuk.
int pthread_join(pthread_t thread, void **retval);
Az első paramétere által meghatározott szál kilépését, végzését várja meg. A fő szálban szokás bejegyezni, lehetővé teszi azt, hogy a létrehozott szálak végezzenek, mielőtt a főprogram kilépne.
Az első paraméterével adhatjuk meg, hogy melyik szál végzését szeretnénk bevárni. A második paramétere egy void * pointer, ami a szál által visszaadott érték lekérdezésére szolgál. A függvény visszatérési értéke 0, ha a művelet közben nem történt hiba.
Szálak kezelésénél még egy hasznos függvény a sleep, amivel a paraméterben megadott másodpercig tudunk várakozni.
void sleep(int seconds);
Nézzünk mindezek használatára egy példaprogramot:
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
static void * szal1(void * param)
{
for (int i=0; i<5; i++)
{
printf("Elso szal\r\n");
sleep(1);
}
return (void*)"szal1 vegzett";
}
static void * szal2(void * param)
{
for (int i=0; i<5; i++)
{
printf("Masodik szal\r\n");
sleep(2);
}
return (void*)"szal2 vegzett";
}
int main()
{
void *uzenet1, *uzenet2;
pthread_t sz1, sz2;
/*szál létrehozása*/
pthread_create(&sz1, NULL, szal1, (void*)NULL);
pthread_create(&sz2, NULL, szal2, (void*)NULL);
/*várunk első szál befejezésére*/
pthread_join(sz1, &uzenet1);
printf("%s\r\n", (char*)uzenet1);
/*várunk a második szál végére*/
pthread_join(sz2, &uzenet2);
printf("%s\r\n", (char*)uzenet2);
return 0;
}
A program lefordításához a pthreads könyvtárat hozzá kell linkelnünk a programunkhoz. Ezt fordításkor a -lpthread kapcsoló megadásával tudjuk elérni. A program kimenete:
Elso szal
Masodik szal
Elso szal
Masodik szal
Elso szal
Elso szal
Masodik szal
Elso szal
szal1 vegzett
Masodik szal
Masodik szal
szal2 vegzett
A fenti példában a szal1 és szal2 esetén is kapunk fordítási figyelmeztetést, ha a -Wall opcióval fordítunk. Ez a figyelmeztetés pedig az lesz, hogy a param definiált paramétert nem használtuk fel. Ha olyan szálat szeretnénk indítani, ami nem vár paramétereket, akkor a void * szal() szignatúra is elfogadott.