Introduzione ai filetti POSIX

La multithreading è una tecnica di programmazione che permette ad un singolo processo di eseguire più compiti contemporaneamente, facendo un migliore uso delle risorse della CPU e migliorando la reattività dell'applicazione. Nel linguaggio di programmazione C, lo standard più ampiamente adottato per la multithreading su sistemi Unix-like è la libreria POSIX Threads, comunemente chiamata ]pthreads.

Utilizzando i pthread, gli sviluppatori possono progettare programmi che eseguono calcoli di sfondo, gestire più connessioni client contemporaneamente, o parallelizzare le attività di elaborazione dei dati. La libreria astratti i dettagli del sistema operativo a basso livello, mentre dà il controllo fine-grained sul comportamento del thread. Una solida comprensione dei pthreads è essenziale per qualsiasi programmatore C che lavora su applicazioni sensibili alle prestazioni o concorrenti.

Concetti di base di pthreads

Prima di scrivere codice multithreaded, è importante conoscere i tipi e le funzioni di base dei dati che i pthreads forniscono:

  • Thread: Un'unità di esecuzione leggera che si svolge all'interno dello spazio di un processo. I thread multipli condividono la stessa memoria, i descrittori di file e altre risorse, rendendo la comunicazione tra i thread efficienti ma anche introducendo la necessità di una sincronizzazione accurata.
  • pthread t[]: Un tipo di dati opaco usato per rappresentare un identificatore di filetto. Non è un intero; si dovrebbe trattarlo come una maniglia che viene restituita da e utilizzata nelle chiamate come .
  • pthread create[[]]: La funzione utilizzata per generare un nuovo thread. Ci vogliono quattro argomenti: un puntatore a una variabile , un puntatore a un oggetto attributo filettato (solitamente per i difetti), la funzione che il thread eseguirà (un puntatore a una funzione che ritorna [[FLT:
  • pthread join[[]: Una chiamata bloccante che aspetta un thread specifico da terminare.
  • pthread mutex t[[]: Il tipo di dati per una serratura mutex (esclusione reciproca) che è la primitiva di sincronizzazione fondamentale utilizzata per proteggere i dati condivisi dall'accesso concomitante.
  • pthread cond t[[]: Una variabile di condizione utilizzata in combinazione con i mutexe per consentire ai thread di attendere che le condizioni specifiche diventino vere.

I filetti sono creati in uno stato detachable o ]joinable]. Per impostazione predefinita, i filetti sono unibili, il che significa che c'è una corrispondente chiamata che pulisce le loro risorse. Se non si prevede di unire un thread, è possibile staccarlo con [FLT7

Realizzare un semplice programma multithreaded

La classica dimostrazione di pthreads comporta la creazione di due filetti che eseguono contemporaneamente e stampano messaggi. L'esempio seguente illustra i passaggi principali: dichiarando [ variabili, chiamando con una routine di inizio, e poi aspettando che entrambi i filetti finiscano con .

#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 questo programma, crea due filetti, ognuno dei quali chiama [. Le chiamate assicurano che il filo principale aspetta che entrambi i fili del bambino finisca prima dell'uscita.

È buona pratica controllare sempre il valore di ritorno di e altre funzioni di pthread. Essi ritornano zero sul successo, o un codice di errore positivo sul fallimento (simile a ). Il programma sopra usa e ] per gestire i guasti con grazia.

Filo di vita e attributi

Ogni thread in pthreads ha un ciclo di vita: creazione, esecuzione, terminazione e pulizia. Quando un thread viene creato, eredita molti attributi dal processo di chiamata, ma è possibile perfezionare il suo comportamento utilizzando un oggetto .

  • Stato di distacco[[]: Se il thread è creato come unificabile (default) o staccato.
  • Dimensioni di filetto[]: consente di assegnare una dimensione specifica di stack per il thread.
  • Politica e priorità di pianificazione[[]: Per il controllo filettato in tempo reale (richiede privilegi appropriati).

Per impostare gli attributi, inizializzare un con , modificarlo con funzioni come [] o [], quindi passare il puntatore attributo come secondo argomento a []. Quando fatto, chiamare ]].

Sincronizzazione con Mutexes

Quando più fili si accede contemporaneamente ai dati condivisi, c'è il rischio di corse di dati — situazioni in cui il risultato dipende dall'ordine imprevedibile dell'esecuzione del thread. Per evitare questo, i pthreads fornisce mutexe. Un mutex assicura che solo un thread alla volta possa eseguire una sezione critica del codice.

  1. Inizializzare un mutex con (o staticamente con ).
  2. Bloccare il mutex prima di accedere ai dati condivisi con .
  3. Sblocca il mutex dopo la sezione critica con .
  4. Distruggere il mutex quando non è più necessario con .

L'esempio seguente dimostra un contatore condiviso incrementato da due fili con una corretta protezione mutex:

#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;
}

Senza il mutex, il controvalore finale sarebbe probabilmente inferiore a 2.000.000 a causa delle condizioni di gara. Il mutex serializza le operazioni di incremento, garantendo la sicurezza del thread. Tuttavia, l'eccessiva chiusura può declassare le prestazioni; l'arte della programmazione multithread è di ridurre al minimo le dimensioni e la durata delle sezioni critiche.

Tipi di Mutex e gestione degli errori

POSIX definisce diversi tipi di mutex controllati dall'attributo type:

  • Normal[] ([[]]]]): Un semplice mutex che non rileva il deadlock.
  • Controllo errore[[]] ([[]]]): Fornisce il rilevamento degli errori; se lo stesso thread cerca di sbloccare un mutex già di proprietà, ritorna .
  • Ricorrente[] ([[]]): consente al filetto di possedere di bloccare più volte il mutex senza lo sgancio. Ogni serratura deve essere accoppiata con uno sblocco.
  • Default[]] ([[]]]]): L'implementazione può mappare a qualsiasi di quanto sopra. Su Linux (glibc) è equivalente a .

Nel codice di produzione, è necessario gestire eventuali errori (ad esempio per un mutex non valido, se il filetto di chiamata non possiede il mutex).

Variabili di condizione

Le variabili di condizione permettono ai filetti di attendere una condizione specifica per diventare veri. Sono sempre utilizzati con un mutex. Il modello tipico è: un thread blocca il mutex, controlla un predicato (una variabile condivisa), e se il predicato è falso, chiama che rilascia atomicamente il mutex e mette il thread a dormire.

Ecco un classico esempio di produttore-consumer utilizzando una variabile a singola condizione e un 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;
}

Notare il loop: una variabile di condizione può soffrire di [] svegliamenti dispuri[] (un thread può tornare da senza essere effettivamente segnalato).

Trasmissione vs. Segnale

Usa quando un solo thread di attesa deve svegliarsi (ad esempio, una singola risorsa diventa disponibile); usare quando tutti i thread di attesa dovrebbero svegliarsi (ad esempio, una bandiera di chiusura).

Serrature di scrittura

Per le strutture di dati che sono letti frequentemente ma scritte raramente, una serratura a lettura ([]) può migliorare la concorrenzialità. I lettori multipli possono tenere la serratura simultaneamente, ma uno scrittore richiede un accesso esclusivo. L'API è simile ai mutexe: , , , e

Esempio:

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;
}

Essere consapevoli che le serrature di lettura-scrittura possono essere meno efficienti dei mutexe quando la sezione critica è molto breve, e possono causare la fame di scrittore se i lettori stanno arrivando continuamente.

Pitfalls e migliori pratiche comuni

La programmazione multithreaded in C è potente ma non è corretta. Di seguito sono frequenti problemi e come evitarli.

Gara dei dati e Deadlocks

Una dati gara[]] si verifica quando due filetti si trovano nella stessa posizione di memoria senza sincronizzazione e almeno una scrittura. C11 ha aggiunto il qualificante, ma i pthread non forniscono direttamente operazioni atomiche; è necessario utilizzare le serrature.

  • Acquisire sempre serrature in un ordine globale coerente.
  • Usa o ] se non puoi garantire l'ordine.
  • Tenere le sezioni critiche il più breve possibile.

Sicurezza del filo delle funzioni della biblioteca

Molte funzioni della libreria standard C non sono sicure da filettature. Ad esempio, [] utilizza lo stato statico interno. Utilizzare le versioni reentrant (ad esempio [) o proteggere le chiamate con un mutex. Le funzioni come non sono anche sicuro filettato; preferi con un seme per-thread.

Dimensione dello stack e limiti delle risorse

Ogni filetto ha un proprio stack. La dimensione dello stack predefinito può essere grande (ad esempio, 8 MB su Linux). La creazione di migliaia di filetti con stack di default può esaurire la memoria. Usa [ per sintonizzare le dimensioni delle pila quando si conoscono i requisiti di un thread.

Disdetta e pulizia del filo

È possibile annullare un thread con . La cancellazione può essere asincrono] (immediato) o differito (fino a quando il thread raggiunge un punto di cancellazione periodicamente o ]]).

Argomenti avanzati: Piscine di filo e queue di lavoro

Creare e distruggere filetti per ogni piccolo compito è costoso. Un pool di filetti pre-crea un numero fisso di filetti di lavoratori che siedono in uno stato di attesa fino a quando non vengono presentati i compiti. Le attività sono tipicamente memorizzate in una coda di lavoro (una struttura di dati sincronizzata). Il pool di filetti è una pietra angolare di applicazioni server ad alte prestazioni. L'implementazione di uno da zero è un grande esercizio: utilizzare un mutex, una variabile di condizione e un elenco di librerieredi funzioni.

Debug e Performance Tuning

I bug multithreaded sono notoriamente difficili da riprodurre e correggere. Utilizzare strumenti come Valgrind Helgrind] o ThreadSanitizer] (attivato compilando )] per rilevare le razze di dati e i comandi di deadlock.

Conclusioni

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.