Table of Contents
Inleiding tot POSIX-threads
Multithreading is een programmeertechniek die het mogelijk maakt om meerdere taken gelijktijdig uit te voeren, waardoor beter gebruik wordt gemaakt van CPU-bronnen en de responsiviteit van toepassingen verbetert. In de programmeertaal C is de meest gebruikte standaard voor multithreading op Unix-achtige systemen de POSIX Threads-bibliotheek, die algemeen wordt aangeduid als pthreads. De pthreads API biedt een uitgebreide set functies voor draadcreatie, synchronisatie en beheer. Het wordt gedefinieerd door de IEEE POSIX 1003.1c standaard en is beschikbaar op bijna alle moderne Linux, macOS en andere UNIX-gebaseerde besturingssystemen.
Met behulp van pthreads kunnen ontwikkelaars programma's ontwerpen die achtergrondberekeningen uitvoeren, meerdere clientverbindingen tegelijkertijd verwerken of dataverwerkingstaken parallel maken. De bibliotheek abstracteert details van het besturingssysteem op laag niveau terwijl ze fijnkorrelige controle geven over draadgedrag. Een solide begrip van pthreads is essentieel voor elke C programmeur die werkt aan prestatiegevoelige of gelijktijdige toepassingen. Dit artikel behandelt de basisconcepten, biedt praktische codevoorbeelden, en onderzoekt synchronisatie primitieven en beste praktijken.
Basisbegrippen van pthreads
Voordat multithreaded code wordt geschreven, is het belangrijk om vertrouwd te raken met de kerngegevenstypes en functies die pthreads biedt:
- Thread: Een lichtgewicht uitvoeringseenheid die binnen de adresruimte van een proces draait. Meerdere threads delen hetzelfde geheugen, bestandsdescriptoren en andere bronnen, waardoor communicatie tussen threads efficiënt wordt, maar ook de noodzaak van zorgvuldige synchronisatie wordt geïntroduceerd.
- pthread t: Een ondoorzichtig datatype dat wordt gebruikt om een thread identifier te representeren. Het is geen geheel getal; je moet het behandelen als een handvat dat wordt teruggegeven door en wordt gebruikt in oproepen zoals .
- pthread create: De functie die gebruikt wordt om een nieuwe draad te paaien. Er zijn vier argumenten nodig: een verwijzing naar een variabele , een verwijzing naar een thread attribuutobject (meestal ] voor standaardwaarden), de functie die de thread zal uitvoeren (een verwijzing naar een functie die teruggeeft en een enkel argument neemt) en een argument om aan die functie door te geven.
- pthread join: Een blokkerende oproep die wacht tot een specifieke draad wordt beëindigd. Het haalt ook de retourwaarde op uit de startroutine van de thread.
- pthread mutex t: Het datatype voor een mutex (mutual exclusion) slot, dat de fundamentele synchronisatie primitief is die wordt gebruikt om gedeelde gegevens te beschermen tegen gelijktijdige toegang.
- pthread cond t: Een conditievariabele die gebruikt wordt in combinatie met mutexes om threads toe te staan te wachten tot specifieke voorwaarden waar worden.
Threads worden gecreëerd in een afneembaar of joinable toestand. Standaard zijn threads joinable, wat betekent dat er een overeenkomstige oproep is die hun bronnen opruimt. Als je niet van plan bent om een draad te verbinden, kun je het loskoppelen met om automatisch zijn bronnen te laten terughalen bij beëindiging.
Een eenvoudig multithreaded programma uitvoeren
De klassieke demonstratie van pthreads houdt in dat er twee threads worden gemaakt die gelijktijdig worden uitgevoerd en berichten afdrukken. Het voorbeeld hieronder illustreert de kernstappen: declareren variabelen, aanroepen met een startroutine, en dan wachten tot beide threads klaar zijn met .
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
void* print_message(void* message) {
char* msg = (char*) message;
printf("%s\n", msg);
return NULL;
}
int main(void) {
pthread_t thread1, thread2;
char* message1 = "Hello from Thread 1!";
char* message2 = "Hello from Thread 2!";
if (pthread_create(&thread1, NULL, print_message, (void*) message1) != 0) {
perror("pthread_create");
exit(EXIT_FAILURE);
}
if (pthread_create(&thread2, NULL, print_message, (void*) message2) != 0) {
perror("pthread_create");
exit(EXIT_FAILURE);
}
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
return 0;
}
In dit programma, creëert twee draden, elk van die roept . De aanroepen zorgen ervoor dat de hoofddraad wacht tot beide kind threads klaar zijn voor het verlaten. Merk op dat de volgorde van uitvoer niet is gegarandeerd: de besturingssysteem scheduler kan de uitvoering van de twee threads interleave, dus je zou kunnen zien "Hallo van Thread 2!" afgedrukt voor het eerste bericht.
Het is een goede praktijk om altijd de rendementswaarde van en andere pthread functies te controleren. Ze keren nul terug bij succes, of een positieve foutcode bij falen (vergelijkbaar met ). Het programma hierboven gebruikt en om fouten sierlijk te verwerken.
Thread Lifecycle en attributen
Elke draad in pthreads heeft een levenscyclus: creatie, uitvoering, beëindiging en opruiming. Wanneer een draad wordt gemaakt, erft het vele attributen van het aanroepen proces, maar je kunt zijn gedrag fijn afstellen met behulp van een object. Gemeenschappelijke attributen zijn:
- Afzonderingstoestand: Of de draad is aangemaakt als joinable (standaard) of losgekoppeld.
- Stackgrootte: Hiermee kunt u een specifieke stackgrootte toewijzen voor de thread.
- Schedulebeleid en prioriteit : Voor real-time draadcontrole (vereist passende privileges).
Om attributen in te stellen, initialiseer een met , pas het aan met functies als of ], geef dan de attribuut pointer door als het tweede argument aan ]. Wanneer dit gedaan is, roep . Attribuutobjecten worden niet door de thread gewijzigd; ze worden alleen gebruikt tijdens de creatie.
Synchronisatie met Mutexes
Wanneer meerdere threads toegang hebben tot gedeelde gegevens tegelijkertijd, is er een risico van data races . . situaties waar het resultaat afhankelijk is van de onvoorspelbare volgorde van draad uitvoering. Om dit te voorkomen, pthreads biedt mutexes. Een mutex zorgt ervoor dat slechts één thread per keer een kritische sectie van code kan uitvoeren. Het typische patroon is:
- Initialiseer een mutex met (of statisch met ).
- Sluit de mutex af voordat u toegang krijgt tot gedeelde gegevens met .
- Ontgrendel de mutex na de kritische sectie met .
- Vernietig de mutex wanneer het niet langer nodig is met .
Het volgende voorbeeld toont een gedeelde teller die door twee draden met een goede mutex bescherming wordt verhoogd:
#include <pthread.h>
#include <stdio.h>
int counter = 0;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void* increment(void* arg) {
for (int i = 0; i < 1000000; i++) {
pthread_mutex_lock(&lock);
counter++;
pthread_mutex_unlock(&lock);
}
return NULL;
}
int main(void) {
pthread_t t1, t2;
pthread_create(&t1, NULL, increment, NULL);
pthread_create(&t2, NULL, increment, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("Final counter value: %d\n", counter); // Guaranteed to be 2000000
pthread_mutex_destroy(&lock);
return 0;
}
Zonder de mutex, de uiteindelijke teller waarde zou waarschijnlijk minder dan 2.000.000 als gevolg van de race omstandigheden. De mutex serialiseert de toename operaties, zorgen voor draad veiligheid. Echter, overmatige vergrendeling kan de prestaties te degraderen; de kunst van multithreaded programmering is om de grootte en duur van kritieke secties te minimaliseren.
Mutex-typen en -foutafhandeling
POSIX definieert meerdere mutextypes die worden gecontroleerd door het typeattribuut:
- Normaal (): Een eenvoudige mutex die geen impasse opspoort. Poging om het opnieuw te vergrendelen van dezelfde draad resulteert in dodenlot.
- Foutcontrole (): Foutdetectie; als dezelfde draad een reeds in eigendom zijnde mutex probeert te hersluiten, geeft hij terug.
- Recursief (): Hiermee kan de eigenaar de mutex meerdere keren zonder impasse vergrendelen. Elk slot moet worden gekoppeld met een unlock.
- Standaard (): De implementatie kan in kaart brengen naar een van de bovenstaande. Op Linux (glibc) is het equivalent aan .
Controleer altijd de retourwaarde van mutex slot/ontgrendeling functies. In productiecode moet u potentiële fouten (bijvoorbeeld ] behandelen voor een ongeldige mutex, als de aanroepdraad de mutex niet bezit).
Voorwaarde Variabelen
De conditievariabelen laten toe dat draden wachten tot een specifieke voorwaarde waar wordt. Ze worden altijd gebruikt met een mutex. Het typische patroon is: een draad sluit de mutex, controleert een predicaat (een gedeelde variabele), en als het predicaat vals is, roept het ] die atomisch de mutex vrijgeeft en de draad in slaap zet. Wanneer een andere draad de conditie aangeeft (met of ), vereist de wachtdraad de mutex en controleert het predicaat opnieuw.
Hier is een klassiek voorbeeld van producent-consument met een enkele variabele en een mutex:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int data_ready = 0;
void* producer(void* arg) {
sleep(1); // Simulate work
pthread_mutex_lock(&mutex);
data_ready = 1;
printf("Producer: data ready\n");
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
return NULL;
}
void* consumer(void* arg) {
pthread_mutex_lock(&mutex);
while (!data_ready) {
pthread_cond_wait(&cond, &mutex);
}
printf("Consumer: processing data\n");
pthread_mutex_unlock(&mutex);
return NULL;
}
int main(void) {
pthread_t prod, cons;
pthread_create(&cons, NULL, consumer, NULL);
pthread_create(&prod, NULL, producer, NULL);
pthread_join(prod, NULL);
pthread_join(cons, NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
Let op de lus: een conditievariabele kan lijden aan zuivere wake-ups (een draad kan terugkeren van ] zonder daadwerkelijk te worden gesignaleerd). Controleer altijd het predicaat na het wakker worden, niet .
Uitzending vs. Signaal
Gebruik wanneer slechts één wachtdraad wakker hoeft te worden (bijvoorbeeld een enkele hulpbron beschikbaar komt); gebruik wanneer alle wachtende draden wakker moeten worden (bijvoorbeeld een uitschakelingsvlag). Onjuist gebruik van signaal kan leiden tot verloren wek- en draadhonger.
Lees-schrijfsloten
Voor gegevensstructuren die vaak worden gelezen maar zelden worden geschreven, kan een lees-schrijfslot ([) de concurrentie verbeteren. Meerdere lezers kunnen het slot tegelijkertijd vasthouden, maar een schrijver heeft exclusieve toegang nodig. De API is vergelijkbaar met mutexes: , , , en ]. Lees-schrijfsloten zijn bijzonder nuttig in scenario's zoals caching of configuratietabellen die in zeldzame gevallen worden bijgewerkt.
Voorbeeld:
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
void* reader(void* arg) {
pthread_rwlock_rdlock(&rwlock);
// read shared data
pthread_rwlock_unlock(&rwlock);
return NULL;
}
void* writer(void* arg) {
pthread_rwlock_wrlock(&rwlock);
// modify shared data
pthread_rwlock_unlock(&rwlock);
return NULL;
}
Let op: lees-schrijfsloten kunnen minder efficiënt zijn dan mutexes wanneer het kritische gedeelte zeer kort is, en ze kunnen schrijvershonger veroorzaken als de lezers voortdurend aankomen.
Gemeenschappelijke valkuilen en beste praktijken
Multithreaded programmering in C is krachtig maar foutgevoelig. Hieronder staan veel voorkomende problemen en hoe ze te vermijden.
Dataraces en deadlocks
Een datarace treedt op wanneer twee draden dezelfde geheugenlocatie benaderen zonder synchronisatie en ten minste één schrijf. C11 voegde de qualifier toe, maar pthreads biedt geen directe atomaire bewerkingen; je moet sloten gebruiken. A Deadlock] gebeurt wanneer twee of meer draden wachten tot elkaar een slot loslaat. Om impasses te voorkomen:
- Verkrijg altijd sloten in een consistente wereldwijde volgorde.
- Gebruik of als u niet kunt garanderen dat u besteld wordt.
- Houd kritieke secties zo kort mogelijk.
Thread Safety of Library-functies
Veel standaard C-bibliotheekfuncties zijn niet draadveilig. Bijvoorbeeld [ gebruikt interne statische toestand. Gebruik de reentrantversies (bv. ) of bescherm gesprekken met een mutex. Functies zoals zijn ook niet draadveilig; prefereren met een per-thread-zaadje.
Stackgrootte en hulpbronnenlimieten
Elke thread heeft een eigen stack. De standaard stackgrootte kan groot zijn (bijv. 8 MB op Linux). Het creëren van duizenden threads met standaard stacks kan geheugen uitputten. Gebruik ] om stapelgroottes af te stemmen als je een threads-eisen kent. Wees je ook bewust van de per-process limieten: beperkt het aantal threads dat een enkel proces kan creëren.
Thread Annulering en opruiming
U kunt een draad annuleren met . De annulering kan asynchrone (immediate) of afgeleid zijn (totdat de draad een annuleringspunt bereikt zoals of ]). De vastgestelde annulering is veiliger omdat het de draad mogelijk maakt om sloten vrij te geven en bronnen op te ruimen. Gebruik en ] om opruimers te registreren. Vermijd annulering van draden tenzij absoluut noodzakelijk; het is vaak beter om een gedeelde vlag te gebruiken die de draad periodiek controleert.
Geavanceerde onderwerpen: Thread pools en wachtrijen
Het creëren en vernietigen van draden voor elke kleine taak is duur. Een thread pool pre-creëert een vast aantal werkdraden die zitten in een wachttoestand totdat taken worden ingediend. Taken worden meestal opgeslagen in een werkwachtrij (een gesynchroniseerde datastructuur). De thread pool is een hoeksteen van high-performance server toepassingen. Het implementeren van een van nul is een grote oefening: gebruik een mutex, een conditie variabele, en een gekoppelde lijst van functieaanwijzers. Veel productiesystemen gebruiken bibliotheken zoals libpthread (onderdeel van glibc) of externe kaders, maar de principes zijn identiek.
Debuggen en prestatie-tunen
Multithreaded bugs zijn berucht moeilijk te reproduceren en te repareren. Gebruik gereedschappen zoals Valgrinds Helgrind of ThreadSanitizer[] (gecompileerd met ]) om dataraces en patstelling te detecteren. GDB ondersteunt multithreaded debugging met commando's als ] en . Voor prestatieprofilering kan Linuxs of alleen op multicore systemen met zeer korte kritische secties helpen bij het opsporen van bottlenecks. Gebruik alleen op multicore systemen met spinlocks afval CPU als het wachten lang is.
Conclusie
POSIX Threads (pthreads) provide a robust, standard interface for multithreading in C. By mastering thread creation, mutexes, condition variables, and read‑write locks, you can build concurrent applications that are both efficient and correct. Start with simple examples, always handle errors, and gradually incorporate advanced patterns like thread pools. For further reading, consult the IEEE POSIX specification, the Linux pthreads man page, and the book Programming with POSIX Threads by David R. Butenhof. With careful design and testing, pthreads can dramatically improve the performance and responsiveness of your C programs while remaining portable across Unix‑like systems.