Egy szál indítása nem pusztán új végrehajtási út létrehozása: kölcsönadunk neki adatokat, felelőst rendelünk a befejezésének megfigyeléséhez, és meghatározzuk, mi történjen leállításkor. A legfontosabb kérdés ezért nem az, hogy „melyik függvény indít szálat?”, hanem az, hogy ki garantálja a szál által használt objektumok élettartamát, és ki várja meg az utolsó hozzáférést.
A Tulajdonlási szerződés és cleanup ágak elvei itt is érvényesek. A Függvénypointerek, callbackek és context pointer mintája most párhuzamos végrehajtással egészül ki. A fork, exec, pipe és gyermekfolyamat kapcsán tárgyalt folyamatoktól eltérően a szálak ugyanannak a folyamatnak az objektumait érhetik el; külön végrehajtási állapotuk nem jelent külön tulajdonlást.
POSIX: a pthreads API deklarációit a
<pthread.h> biztosítja. A szál belépési függvényének
típusa:
/* Deklarációs részlet; C17 + POSIX. */
void *worker(void *context);ISO C17: a C11-ben bevezetett, opcionálisan
támogatott szálkönyvtár a <threads.h> fejlécben
található. Ennek belépési függvénye eltérő típusú:
/* Deklarációs részlet; C17, támogatott <threads.h> mellett. */
int worker(void *context);A két függvénytípus nem felcserélhető. A típuseltérést függvénypointer-casttal elfedni nem javítás.
| Feladat | POSIX pthreads | ISO C17 szálkönyvtár |
|---|---|---|
| Szálazonosító típusa | pthread_t |
thrd_t |
| Indítás | pthread_create |
thrd_create |
| Befejezés megvárása | pthread_join |
thrd_join |
| Leválasztás | pthread_detach |
thrd_detach |
| Belépési függvény eredménye | void * |
int |
| API sikerjelzése | Az itt tárgyalt függvényeknél 0 |
thrd_success |
| Szálmegszakítási API | Van cancellation mechanizmus | Nincs megfelelő standard cancellation API |
A továbbiakban csatlakoztatható szálnak (joinable thread) nevezzük azt, amelynek befejezését egy másik szál join művelettel megvárhatja. A leválasztott szál (detached thread) befejezésekor a szálkezelő rendszer a szálhoz tartozó erőforrásokat külön join nélkül visszanyerheti.
A belépési függvény visszatérése és a szál erőforrásainak visszanyerése tehát külön esemény. Egy már befejeződött joinable szálhoz is tartozhatnak visszanyerésre váró erőforrások.
A következő pthread-hívás új szálat kér, alapértelmezett attribútumokkal:
/* Hívási részlet; C17 + POSIX, a változók deklarációját feltételezi. */
int rc = pthread_create(&thread, NULL, worker, &job);Siker esetén a szál joinable. Az új szál már a
pthread_create visszatérése előtt is elkezdhet
futni. Emiatt a job szükséges inicializálását az
indítás előtt be kell fejezni.
A void * paraméter csak pointerérték. Nem másolja le a
megcímzett objektumot, és nem hosszabbítja meg annak élettartamát.
Egy stacken létrehozott context teljesen megfelelő, ha az azt tartalmazó blokk nem ér véget a worker utolsó hozzáférése előtt. Ennek egyszerű biztosítéka a blokk elhagyását megelőző sikeres join.
| Entitás | Tulajdonos vagy felelős | Élettartami szerződés | Érvénytelenítés |
|---|---|---|---|
job objektum és tárolója |
Az indító függvény | A worker befejezéséig él | A létrehozó blokk vége |
Worker context pointere |
Kölcsönzött hozzáférés | Csak élő job objektumot érhet el |
A job élettartamának vége |
| Joinable szál életciklusa | Kijelölt join-felelős | Sikeres joinnal vagy tudatos detach-csel rendezendő | Az életciklus lezárása |
| Worker által írt eredmény | A job része |
Az indító csak sikeres join után olvassa | A job élettartamának vége |
A pthread_create hibája esetén nem jött létre kezelendő
szál; a kimeneti szálazonosítót nem szabad joinhoz használni.
A példa minimumkövetelménye C17 és POSIX pthreads. Konkrét reprodukciós célprofilként Linux x86-64 System V ABI, glibc 2.40, GCC 14.2.0 vagy Clang 19.1.7 választható. Ez megadott, nem fordítással ellenőrzött profil; a saját laborban a tényleges verziókat rögzíteni kell.
A job.count előfeltétele ebben a példában legfeljebb
1000. Nincs külső cancellation, és csak a main kezeli a
szálazonosítót.
/* thread_job.c — C17 + POSIX pthreads */
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
struct job {
unsigned long count;
unsigned long result;
};
static void *worker(void *context)
{
struct job *job = context;
unsigned long sum = 0;
for (unsigned long i = 0; i < job->count; ++i) {
sum += i;
}
job->result = sum;
return context;
}
static void fatal_pthread(const char *operation, int code)
{
fprintf(stderr, "%s: hibakod=%d\n", operation, code);
exit(EXIT_FAILURE);
}
int main(void)
{
struct job job = {
.count = 1000,
.result = 0
};
pthread_t thread;
int rc = pthread_create(&thread, NULL, worker, &job);
if (rc != 0) {
fatal_pthread("pthread_create", rc);
}
void *returned = NULL;
rc = pthread_join(thread, &returned);
if (rc != 0) {
fatal_pthread("pthread_join", rc);
}
if (returned != &job) {
fputs("Ervenytelen worker-eredmeny\n", stderr);
return EXIT_FAILURE;
}
printf("%lu\n", job.result);
return EXIT_SUCCESS;
}Fordítás a külön laborban:
gcc -std=c17 -Wall -Wextra -Wpedantic -O2 -pthread thread_job.c -o thread_job
clang -std=c17 -Wall -Wextra -Wpedantic -O2 -pthread thread_job.c -o thread_job-clangA -pthread a GCC/Clang platformfüggő driveropciója, nem
ISO C nyelvi elem. A fordítási és linkelési lépésben is használni kell;
a fenti parancsok mindkettőt elvégzik.
A számítás szerint várt, nem mért eredmény:
Helyességi bizonyításvázlat.
main inicializálja a contextet a szál indítása
előtt.main nem módosítja a bemenetet,
és nem olvassa az eredményt.pthread_join megvárja a worker befejezését,
és biztosítja a szükséges szinkronizációt az eredmény ezt követő
olvasásához.job blokkja csak ezután ér véget.A pontos memóriamodellbeli kapcsolatot a Data race, happens-before és publikalás tárgyalja.
A join hibája különösen fontos: ilyenkor a program nem állíthatja, hogy a worker már nem használja a contextet. A példa ezért folyamatmegszakító hibapolitikát alkalmaz. Egy könyvtár nem dönthet önkényesen így; annak az unresolved életciklusért viselt felelősséget is meg kell őriznie vagy át kell adnia.
errnoPOSIX: a pthread_create,
pthread_join és pthread_detach hibakódját a
függvény visszatérési értéke adja. Nem a szokásos „-1, majd
errno” mintát követik.
Ezért hibás diagnosztikai minta:
/* HIBÁS részlet: a perror nem a visszaadott pthread-hibakódot írja ki. */
if (pthread_create(&thread, NULL, worker, &job) != 0) {
perror("pthread_create");
}A perror az aktuális errno alapján
dolgozik, amely nem feltétlenül kapcsolódik ehhez a hibához. A helyes
minta eltárolja a visszatérési értéket; a teljes példában ezért
közvetlenül a numerikus hibakód szerepel.
A pthread_join esetében két külön eredménycsatorna
van:
int visszatérési értéke a join
művelet státusza;void * a
worker eredménye.A worker nem adhat vissza használható pointert a saját megszűnt automatikus objektumára. Heapobjektum pointerét visszaadhatja, ha az átadott tulajdonlást és a felszabadítás felelősét dokumentáljuk. A példában nincs tulajdonátruházás: ugyanaz a kölcsönzött pointer tér vissza.
ISO C17: a szálkönyvtár opcionális. A
__STDC_NO_THREADS__ makró 1 értéke jelzi a támogatás
hiányát. A makró vizsgálata mellett a tényleges compiler–libc–linker
összeállítást is ellenőrizni kell.
A következő önálló program egyszerre mutatja az API eltérő eredményszerződését, és használható fordítási/linkelési próbának:
/* c11_probe.c — C17, opcionális ISO szálkönyvtár */
#if defined(__STDC_NO_THREADS__) && __STDC_NO_THREADS__ == 1
#error "Az implementacio nem tamogatja a C szalkonyvtarat"
#endif
#include <stdlib.h>
#include <threads.h>
static int worker(void *context)
{
(void)context;
return 7;
}
int main(void)
{
thrd_t thread;
int status = thrd_create(&thread, worker, NULL);
if (status != thrd_success) {
return EXIT_FAILURE;
}
int worker_result = 0;
status = thrd_join(thread, &worker_result);
if (status != thrd_success) {
exit(EXIT_FAILURE);
}
return worker_result == 7 ? EXIT_SUCCESS : EXIT_FAILURE;
}A Linuxos GCC/Clang célprofilhoz:
gcc -std=c17 -Wall -Wextra -Wpedantic -pthread c11_probe.c -o c11_probeItt nincs kölcsönzött contextobjektum: a callback nem dereferálja a
null pointert. A szál életciklusáért a main felel. A
sikertelen indítás hibakimenet, a join hibája folyamatleállítást
eredményez.
A thrd_create státusza például
thrd_success, thrd_nomem vagy
thrd_error lehet. Ezek nem POSIX hibakódok, és nem
errno-értékek. A thrd_join által átvett
7 pedig nem könyvtári státusz, hanem a worker
alkalmazásszintű eredménye.
A sikeres fordítás és linkelés csak a szükséges felület elérhetőségét igazolja. A működés vizsgálata külön futtatási lépés; itt ilyen ellenőrzést nem feltételezünk.
ISO C17: a _Thread_local
tárolásiosztály-specifikátor szálhoz kötött tárolási idejű objektumot
jelöl. Minden szálnak saját példánya van, amelynek élettartama az adott
szál végrehajtásának idejéhez kötődik.
/* Részlet; C17, TLS-t támogató compiler/target.
A számláló túlcsordulása unsigned moduloaritmetika. */
static _Thread_local unsigned long completed_jobs;
static void record_completion(void)
{
++completed_jobs;
}A static itt fájlszinten belső nyelvi kapcsolást ad; a
_Thread_local ettől függetlenül a tárolási időt határozza
meg.
A szálankénti számláló nem közös összesítő. Attól, hogy minden worker növeli a saját példányát, a főszál példányában nem jelenik meg az összeg.
A TLS-cím továbbadása sem általános megosztási technika:
_Thread_local önmagában nem biztosít alkalmazási
erőforrásokat felszabadító destruktort.Például egy TLS-pointerben tárolt heapobjektum felszabadításáról
továbbra is gondoskodni kell. Az explicit szálkilépési destruktorokhoz a
pthread kulcsalapú TLS vagy a C szálkönyvtár tss_* felülete
külön mechanizmust nyújt.
A detach nem leállítási kérés, és nem várakozás. Csak azt változtatja meg, hogyan rendeződik a szálkezelési erőforrás életciklusa.
| Művelet | Megvárja a befejezést? | Felszabadítja az alkalmazás contextjét? |
|---|---|---|
Sikeres pthread_join |
Igen | Nem |
Sikeres pthread_detach |
Nem | Nem |
| Worker visszatérése | A worker befejeződik | Csak saját explicit cleanup esetén |
| Folyamat megszűnése | Nem szabályos join-sorozat | Az alkalmazási lezárási protokoll nem garantált |
Egy szálhoz egyértelmű join-felelőst rendeljünk. Azonos szál
párhuzamos joinolása, saját magunk joinolása, illetve már leválasztott
vagy lezárt életciklusú szálazonosító használata nem elfogadható
protokoll. A pthread_t nem hordozható egész szám, és nem
tartós, korlátlanul újrahasználható azonosító.
A rendezett leállítás tipikus sorrendje:
A stopjelző nem lehet szinkronizálatlan közös bool, és a
volatile sem teszi megfelelővé. A megoldást a Mutex, condition variable és producer–consumer rész
építi fel.
POSIX folyamatéletciklus: a main
visszatérése vagy az exit a folyamatot állítja le, nem csak
a hívó szálat. A detached workerek sem maradnak életben. A
pthread_exit ezzel szemben csak a hívó szálat fejezi be, de
nem helyettesíti a join-felelősséget, és nem hosszabbítja meg a megszűnő
automatikus contextobjektumok élettartamát.
POSIX: a pthread_cancel megszakítási
kérelmet küld. A sikeres visszatérés nem bizonyítja, hogy a célpont már
befejeződött.
Az alapértelmezett, halasztott (deferred) cancellation engedélyezett állapotban meghatározott cancellation pointokon léphet érvénybe. Egy ilyen pont nélküli számítás nem feltétlenül reagál rá. Az aszinkron cancellation tetszőleges ponton szakíthatja félbe az invariánsok helyreállítását; általános alkalmazáskódban kerülendő.
A pthread_cleanup_push és
pthread_cleanup_pop cleanupkezelőket rendelhet az aktuális
szálhoz. Ezek:
pthread_exit hatására;pop paraméterével explicit módon is
végrehajthatók;A belépési függvény közönséges visszatérése nem futtatja
automatikusan a még regisztrált cleanupkezelőket. A cleanup-régiót
átugró return vagy goto sem megfelelő
használat.
Sikeres join esetén a megszakított pthread eredménye
PTHREAD_CANCELED lehet; ez nem join-hibakód. Ráadásul maga
a pthread_join is cancellation point, ezért a join-felelős
megszakíthatóságát is kezelni kell.
Szolgáltatásokban általában könnyebben indokolható a kooperatív shutdown: a worker ismert, helyes állapotban észleli a leállítási kérést, majd maga végez cleanupot.
Egy függvény lokális struct job címével indít workert,
sikeresen detach-eli a szálat, majd visszatér. Miért nem elegendő a
detach? Adj két javítást.
Bővítsd a pthread-példát két, külön contextet használó workerre. Ha a második indítás sikertelen, az első szál életciklusát akkor is szabályosan rendezd.
Egy alkalmazás a pthread_create hibáját
perror segítségével írja ki, sikeres indítás után pedig
join nélkül kiolvassa a worker eredményét. A worker végtelen számítási
ciklusát pthread_cancel hívással próbálja leállítani.
Azonosíts három külön hibát vagy hibás feltételezést.
A detach csak a szálkezelési erőforrásokra vonatkozik. A függvény visszatérése megszünteti a lokális context élettartamát, miközben a worker még hozzáférhet.
Két javítás:
A második változatban sikertelen indításkor az indító szabadít fel; siker esetén a worker. Ellenőrzési feltétel: minden hibautat is figyelembe véve pontosan egy felszabadítás legyen, az utolsó hozzáférés után.
A kiinduló programba illeszthető vezérlési részlet, változatlan workerrel és hibakezelővel:
/* Részlet; C17 + POSIX, a thread_job.c definícióival. */
struct job jobs[2] = {
{ .count = 1000, .result = 0 },
{ .count = 1000, .result = 0 }
};
pthread_t threads[2];
size_t started = 0;
int create_error = 0;
for (size_t i = 0; i < 2; ++i) {
int rc = pthread_create(&threads[i], NULL, worker, &jobs[i]);
if (rc != 0) {
create_error = rc;
break;
}
++started;
}
for (size_t i = 0; i < started; ++i) {
int rc = pthread_join(threads[i], NULL);
if (rc != 0) {
fatal_pthread("pthread_join", rc);
}
}
if (create_error != 0) {
fprintf(stderr, "pthread_create: hibakod=%d\n", create_error);
return EXIT_FAILURE;
}A main tulajdonolja a contexttömböt; minden worker csak
a saját elemét kölcsönzi. A tömb a joinokig él. Ellenőrzési feltétel:
pontosan started érvényes szálazonosítót joinolunk, és
indítási hiba miatt nem hagyjuk el idő előtt a context blokkát.
Az első hiba a téves hibacsatorna: a pthread visszaadott hibakódját
kell használni, nem az errno aktuális értékét.
A második a szinkronizálatlan eredményolvasás: megfelelő rendezés nélkül ütközhet a worker írásával. Ebben a mintában a sikeres join utáni olvasás a javítás.
A harmadik a cancellation téves értelmezése: a kérelem nem azonnali befejezés, és cancellation point nélküli ciklus nem feltétlenül reagál. Ellenőrzési feltétel: a leállításnak legyen dokumentált észlelési pontja, majd igazolt befejezése.
A szálkezelés alapja az élettartam és a felelősség: az indító érvényes contextet ad, a worker betartja a hozzáférési szerződést, egy kijelölt szereplő pedig rendezi a befejezést. A join megvárja a szálat; a detach nem. A pthread függvények közvetlen hibakódot, a C szálkönyvtár saját státuszokat használ. A TLS szálanként külön objektumot ad, de nem automatikus erőforrás-cleanupot. A kiszámítható shutdown elsődleges eszköze a kooperatív leállítás és az igazolt utolsó hozzáférés.