Un thread è un flusso di esecuzione indipendente all'interno di un processo. Tutti i thread di un processo condividono lo stesso spazio di indirizzamento (code, data, heap, file descriptor, segnali), ma mantengono stack, registri, errno, signal mask e dati thread-local propri.
POSIX Threads (pthread) è l'API standardizzata da IEEE Std 1003.1c-1995. Su Linux è implementata da NPTL (Native POSIX Threads Library) in cui ogni pthread è un task del kernel (1:1) creato tramite la syscall clone().
| Modello | Descrizione | Esempi |
|---|---|---|
1:1 | Ogni thread user ↔ thread kernel. Vera parallelizzazione multi-core, syscall bloccanti isolate. | Linux NPTL, Solaris, Windows |
N:1 | Più thread user su un solo thread kernel. Scheduling in user-space, syscall bloccante blocca tutti. | GNU Portable Threads |
M:N | M thread user mappati su N thread kernel. Complesso, poco usato oggi. | Solaris pre-9, NetBSD SA |
Vantaggi:
• Creazione/distruzione veloce (no COW, no fork)
• Comunicazione via memoria condivisa
• Switch di contesto leggero
• Parallelismo reale su multi-core
• Condivisione automatica risorse
Svantaggi:
• Race condition, deadlock
• Nessun isolamento: un crash uccide tutto il processo
• Debug complesso (non determinismo)
• Tutte le API libc devono essere MT-safe
• Overhead di sincronizzazione
| Risorsa | Processi (dopo fork) | Thread |
|---|---|---|
| Spazio di indirizzamento | Copia (COW) | Condiviso |
| Code, data, heap | Copia privata | Condivisi |
| Stack | Copia privata | Privato per thread |
| File descriptor | Copia (stesso file offset) | Condivisi |
| PID | Diverso | Unico PID (TID diverso) |
| Signal handler | Ereditati (copia) | Condivisi |
| Signal mask | Ereditata | Privata per thread |
| Signal pendenti | Separati | Per thread + per processo |
errno | Separato | Privato (TLS) |
| cwd, umask, rlimit | Ereditati | Condivisi |
| Timer / alarm | Azzerati | Condivisi |
| ID thread | — | pthread_t / TID |
Usa i processi quando:
• Serve isolamento (sandbox, sicurezza)
• Esegui programmi esterni (exec)
• Il task può crashare senza ucciderti
• I task non devono condividere stato
• Server: pre-fork (nginx, apache)
Usa i thread quando:
• Servono comunicazioni frequenti
• Parallelismo CPU-intensivo
• Serve bassa latenza creazione
• Condivisione di grandi strutture dati
• Worker pool, I/O asincrono
| Header | Contenuto |
|---|---|
<pthread.h> | Tutte le API pthread_* (thread, mutex, cond, rwlock, barrier, spinlock, once, key) |
<semaphore.h> | Semafori POSIX (sem_t, sem_init, sem_wait, sem_post) |
<sched.h> | Politiche di scheduling, sched_yield(), affinity CPU |
<stdatomic.h> | Operazioni atomiche C11 (atomic_int, atomic_flag, memory order) |
<threads.h> | C11 threads (thrd_create, mtx_t, cnd_t) — opzionale |
<time.h> | struct timespec, clock_gettime (per timed wait) |
<errno.h> | errno thread-local (macro), codici EAGAIN, EBUSY, ETIMEDOUT, EDEADLK |
# GCC / Clang — il flag -pthread abilita macro e linka libpthread gcc -Wall -Wextra -pthread -o prog main.c # Solo linking (sconsigliato, non abilita _REENTRANT): gcc -Wall -Wextra -o prog main.c -lpthread # Con sanitizer per race-condition (ThreadSanitizer): gcc -fsanitize=thread -g -O1 -pthread -o prog main.c # Richiedere feature POSIX specifiche: gcc -D_POSIX_C_SOURCE=200809L -pthread -o prog main.c # C11 threads (richiede implementazione libc, es. glibc ≥ 2.28): gcc -std=c11 -pthread -o prog main.c
-pthread (senza la "-l") definisce la macro _REENTRANT (o _POSIX_THREADS) e attiva versioni thread-safe delle funzioni libc. Preferiscilo sempre a -lpthread./* Le funzioni pthread_* NON usano errno: ritornano 0 o codice di errore */ int err = pthread_create(&tid, NULL, worker, arg); if (err != 0) { fprintf(stderr, "pthread_create: %s\n", strerror(err)); exit(EXIT_FAILURE); } /* Eccezioni (usano errno): - pthread_getcpuclockid (POSIX-XSI) - sem_* (semafori POSIX, usano errno) */
perror() dopo una pthread_* fallita: errno non è settato. Usa sempre strerror(err) sul valore di ritorno.Ogni thread ha il proprio stack (default ~8 MiB su Linux), mentre code, heap, data e bss sono condivisi.
static e globali sono condivise tra tutti i thread: ogni accesso in scrittura richiede sincronizzazione o dichiarazione _Thread_local / __thread.#include <pthread.h> int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg); // thread : output — identificativo del thread creato // attr : attributi (NULL = default: joinable, stack 8 MiB) // start : funzione d'ingresso del thread // arg : argomento passato a start // Ritorna : 0 se OK, codice errore (EAGAIN, EINVAL, EPERM) altrimenti
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <pthread.h> void *worker(void *arg) { int id = *(int *)arg; printf("Thread #%d attivo (tid=%lu)\n", id, (unsigned long)pthread_self()); return (void *)(intptr_t)(id * id); } int main(void) { pthread_t t[4]; int ids[4]; for (int i = 0; i < 4; i++) { ids[i] = i; int err = pthread_create(&t[i], NULL, worker, &ids[i]); if (err) { fprintf(stderr, "create: %s\n", strerror(err)); exit(1); } } for (int i = 0; i < 4; i++) { void *ret; pthread_join(t[i], &ret); printf("Thread #%d → %ld\n", i, (intptr_t)ret); } return 0; }
&i: tutti i thread vedrebbero lo stesso indirizzo in evoluzione. Usa un array di indici o una struct allocata separatamente.typedef struct { int id; double *data; size_t n; } task_t; void *worker(void *arg) { task_t *t = (task_t *)arg; /* usa t->id, t->data, t->n */ free(t); // cleanup della task allocata return NULL; } /* lato chiamante */ task_t *t = malloc(sizeof(*t)); t->id = i; t->data = buf; t->n = len; pthread_create(&tid, NULL, worker, t);
malloc per ogni task rispetto allo stack del chiamante: elimina il rischio che la variabile scada prima che il thread la legga.| errno | Significato |
|---|---|
EAGAIN | Risorse insufficienti (ulimit -u, memoria per stack) |
EINVAL | Attributi non validi |
EPERM | Permessi insufficienti per la policy di scheduling richiesta |
void pthread_exit(void *retval); // Termina il thread corrente, rendendo retval disponibile // al chiamante di pthread_join. Esegue i cleanup handler e // i distruttori delle chiavi TLS.
Modalità equivalenti di terminazione:
• return val; dalla funzione start_routine → equivalente a pthread_exit(val)
• pthread_exit(val) da una funzione chiamata dal thread
• pthread_cancel(tid) da un altro thread (se cancellable)
• Il processo termina (exit, _exit, kill) → tutti i thread terminano
int main(void) { pthread_t t; pthread_create(&t, NULL, worker, NULL); /* Se main chiama return o exit() → TUTTO il processo termina, uccidendo anche i thread detached ancora in esecuzione. Per lasciarli continuare, usare pthread_exit: il processo vive finché resta almeno un thread. */ pthread_exit(NULL); }
exit() termina l'intero processo. Usa pthread_exit() o return per uscire dal singolo thread.void *worker(void *arg) { int *ret = malloc(sizeof(int)); *ret = 42; pthread_exit(ret); // lato join: liberare la memoria } void *res; pthread_join(tid, &res); printf("%d\n", *(int *)res); free(res);
int pthread_join(pthread_t thread, void **retval); // Blocca finché il thread termina, recupera il valore di ritorno, // libera le risorse del TCB. DEVE essere chiamata su ogni thread // joinable altrimenti le risorse del thread restano occupate. // Ritorna 0 o codice errore.
Stati di ritorno:
• ESRCH — nessun thread con quel TID
• EINVAL — il thread è detached o un altro thread sta già joinando
• EDEADLK — tentativo di join su se stessi
#define _GNU_SOURCE #include <pthread.h> /* pthread_tryjoin_np — ritorna EBUSY se il thread è ancora vivo */ int err = pthread_tryjoin_np(tid, &ret); /* pthread_timedjoin_np — join con timeout assoluto */ struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); ts.tv_sec += 5; err = pthread_timedjoin_np(tid, &ret, &ts);
Un thread joinable terminato ma mai joinato rimane in stato "zombie": il suo TCB, lo stack e il valore di ritorno non vengono liberati. Soluzioni:
• Chiamare sempre pthread_join sui thread joinable
• Creare il thread detached se non interessa il risultato
• Chiamare pthread_detach(pthread_self()) dal thread stesso
int pthread_detach(pthread_t thread); // Dichiara il thread come "detached": alla terminazione le risorse // vengono liberate automaticamente. NON può più essere joinato.
| Stato | pthread_join | Pulizia risorse |
|---|---|---|
PTHREAD_CREATE_JOINABLE (default) | Obbligatorio | Al join |
PTHREAD_CREATE_DETACHED | Non permesso | Automatica |
pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); pthread_t tid; pthread_create(&tid, &attr, worker, arg); pthread_attr_destroy(&attr); // Non serve pthread_join: il TCB viene rilasciato alla fine del worker
void *fire_and_forget(void *arg) { pthread_detach(pthread_self()); // il thread si stacca da solo /* ... lavoro ... */ return NULL; }
pthread_t pthread_self(void); // Ritorna l'ID del thread chiamante. int pthread_equal(pthread_t t1, pthread_t t2); // Ritorna 0 se diversi, != 0 se uguali. // USARE SEMPRE questa funzione: pthread_t è un tipo opaco, // non è garantito che sia un intero confrontabile con ==.
#include <sys/syscall.h> #include <unistd.h> pid_t tid = syscall(SYS_gettid); // Il "TID kernel" è visibile in /proc/PID/task/TID, in // top -H, in gdb. È diverso da pthread_t (opaco user-space). /* Da glibc 2.30: wrapper gettid() */ pid_t tid = gettid();
pthread_attr_t attr; pthread_attr_init(&attr); // inizializza /* ... setter ... */ pthread_create(&tid, &attr, worker, arg); pthread_attr_destroy(&attr); // rilascia
create: puoi distruggerli subito dopo, anche se il thread non è ancora terminato.| Attributo | Funzione setter | Valori |
|---|---|---|
| Stato detach | pthread_attr_setdetachstate | PTHREAD_CREATE_JOINABLE / DETACHED |
| Dimensione stack | pthread_attr_setstacksize | size_t (min PTHREAD_STACK_MIN) |
| Indirizzo stack | pthread_attr_setstack | stack_addr + stack_size |
| Guard size | pthread_attr_setguardsize | byte di guardia oltre lo stack |
| Scope | pthread_attr_setscope | PTHREAD_SCOPE_SYSTEM / PROCESS |
| Inherit scheduling | pthread_attr_setinheritsched | PTHREAD_INHERIT_SCHED / EXPLICIT |
| Politica | pthread_attr_setschedpolicy | SCHED_OTHER / FIFO / RR |
| Parametri | pthread_attr_setschedparam | struct sched_param |
pthread_attr_t attr; pthread_attr_init(&attr); /* Stack minimo supportato dal sistema */ size_t size = PTHREAD_STACK_MIN; if (size < 64 * 1024) size = 64 * 1024; pthread_attr_setstacksize(&attr, size); /* Guard zone (default: 1 page) */ pthread_attr_setguardsize(&attr, 4096); pthread_create(&tid, &attr, worker, NULL); pthread_attr_destroy(&attr);
guardsize o compila con -fstack-protector.struct sched_param sp = {.sched_priority = 50}; pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED); pthread_attr_setschedpolicy(&attr, SCHED_FIFO); pthread_attr_setschedparam(&attr, &sp); /* Policy disponibili: SCHED_OTHER - default time-sharing (priority 0) SCHED_BATCH - meno preemption (CPU-bound) SCHED_IDLE - priorità molto bassa SCHED_FIFO - realtime FIFO (1..99) SCHED_RR - realtime round-robin con quanto */
#define _GNU_SOURCE #include <pthread.h> #include <sched.h> cpu_set_t set; CPU_ZERO(&set); CPU_SET(0, &set); CPU_SET(2, &set); // thread può girare solo su core 0 o 2 pthread_setaffinity_np(tid, sizeof(set), &set); pthread_getaffinity_np(tid, sizeof(set), &set);
#include <sched.h> int sched_yield(void); // cede la CPU allo scheduler #include <time.h> struct timespec ts = {0, 10 * 1000000L}; // 10 ms nanosleep(&ts, NULL);
Un mutex (MUTual EXclusion) garantisce che al più un thread alla volta sia all'interno di una sezione critica. Ha due stati: unlocked e locked. Un mutex locked ha un owner: solo lui può rilasciarlo.
#include <pthread.h> int pthread_mutex_init(pthread_mutex_t *m, const pthread_mutexattr_t *attr); int pthread_mutex_destroy(pthread_mutex_t *m); int pthread_mutex_lock(pthread_mutex_t *m); // blocca se occupato int pthread_mutex_trylock(pthread_mutex_t *m); // EBUSY se occupato int pthread_mutex_timedlock(pthread_mutex_t *m, const struct timespec *abs_timeout); int pthread_mutex_unlock(pthread_mutex_t *m);
/* Statica — mutex globale con attributi di default */ pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER; /* Dinamica — obbligatoria se servono attributi non-default */ pthread_mutex_t m; pthread_mutex_init(&m, NULL); // ... uso ... pthread_mutex_destroy(&m);
memcpy) un pthread_mutex_t.pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; long counter = 0; void *worker(void *arg) { for (int i = 0; i < 1000000; i++) { pthread_mutex_lock(&lock); counter++; // sezione critica pthread_mutex_unlock(&lock); } return NULL; }
/* In C non esiste RAII; si usano macro o la funzione cleanup (GCC) */ /* 1) Macro helper */ #define MUTEX_LOCKED(m) \ for(int _done = (pthread_mutex_lock(m), 0); \ !_done; _done = (pthread_mutex_unlock(m), 1)) MUTEX_LOCKED(&lock) { /* sezione critica — unlock automatico al termine */ counter++; } /* 2) __attribute__((cleanup)) — GCC/Clang */ static inline void unlock_helper(pthread_mutex_t **m) { pthread_mutex_unlock(*m); } #define SCOPED_LOCK(mtx) \ pthread_mutex_t *_l __attribute__((cleanup(unlock_helper))) \ = ((pthread_mutex_lock(mtx)), mtx) void fn(void) { SCOPED_LOCK(&lock); counter++; /* unlock automatico all'uscita dallo scope */ }
pthread_mutexattr_t ma; pthread_mutexattr_init(&ma); pthread_mutexattr_settype(&ma, PTHREAD_MUTEX_RECURSIVE); pthread_mutexattr_setpshared(&ma, PTHREAD_PROCESS_SHARED); pthread_mutexattr_setrobust(&ma, PTHREAD_MUTEX_ROBUST); pthread_mutexattr_setprotocol(&ma, PTHREAD_PRIO_INHERIT); pthread_mutex_init(&m, &ma); pthread_mutexattr_destroy(&ma);
| Attributo | Valori | Note |
|---|---|---|
type | NORMAL, ERRORCHECK, RECURSIVE, DEFAULT | Vedi tabella sotto |
pshared | PTHREAD_PROCESS_PRIVATE / SHARED | SHARED → può vivere in shared memory tra processi |
robust | STALLED / ROBUST | ROBUST: se l'owner muore, il successivo lock ritorna EOWNERDEAD |
protocol | NONE / INHERIT / PROTECT | Priority Inheritance / Priority Ceiling |
prioceiling | priorità | Solo con PRIO_PROTECT |
| Tipo | Relock dell'owner | Unlock non-owner | Unlock non-locked |
|---|---|---|---|
NORMAL | Deadlock | Comportamento indefinito | Comportamento indefinito |
ERRORCHECK | EDEADLK | EPERM | EPERM |
RECURSIVE | Contatore++ (ok) | EPERM | EPERM |
DEFAULT | Impl-defined (tipicamente = NORMAL) | Impl-defined | Impl-defined |
/* Utile per API che richiamano se stesse senza dover rilasciare */ pthread_mutexattr_t ma; pthread_mutexattr_init(&ma); pthread_mutexattr_settype(&ma, PTHREAD_MUTEX_RECURSIVE); pthread_mutex_t m; pthread_mutex_init(&m, &ma); pthread_mutex_lock(&m); // count = 1 pthread_mutex_lock(&m); // count = 2 (stesso thread, ok) pthread_mutex_unlock(&m); // count = 1 pthread_mutex_unlock(&m); // count = 0, rilasciato
pthread_mutexattr_setrobust(&ma, PTHREAD_MUTEX_ROBUST); pthread_mutex_init(&m, &ma); /* Nel consumatore: */ int r = pthread_mutex_lock(&m); if (r == EOWNERDEAD) { /* L'owner precedente è morto mentre deteneva il lock. Devo ripristinare gli invarianti e dichiarare consistente: */ /* ... ripara lo stato ... */ pthread_mutex_consistent(&m); }
/* Evita di bloccarsi: utile in event loop */ if (pthread_mutex_trylock(&m) == 0) { /* acquisito */ pthread_mutex_unlock(&m); } else { /* EBUSY — non disponibile ora, riprova più tardi */ } /* Blocca al massimo N secondi */ struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); ts.tv_sec += 2; if (pthread_mutex_timedlock(&m, &ts) == ETIMEDOUT) { /* non è riuscito entro il timeout */ }
Una condition variable permette a un thread di attendere che una condizione (predicato sullo stato) diventi vera, senza busy-wait. Una cond è sempre usata insieme a un mutex che protegge il predicato.
Passi tipici:
• Il waiter blocca il mutex, controlla il predicato, e se falso chiama pthread_cond_wait, che rilascia il mutex e si sospende atomicamente.
• Il signaler blocca il mutex, modifica lo stato, chiama pthread_cond_signal (o broadcast), e rilascia il mutex.
• Al risveglio, pthread_cond_wait riprende il mutex e il waiter ricontrolla il predicato in un while.
int pthread_cond_init(pthread_cond_t *c, const pthread_condattr_t *a); int pthread_cond_destroy(pthread_cond_t *c); int pthread_cond_wait(pthread_cond_t *c, pthread_mutex_t *m); int pthread_cond_timedwait(pthread_cond_t *c, pthread_mutex_t *m, const struct timespec *abstime); int pthread_cond_signal(pthread_cond_t *c); // risveglia almeno 1 waiter int pthread_cond_broadcast(pthread_cond_t *c); // risveglia tutti /* Statica */ pthread_cond_t c = PTHREAD_COND_INITIALIZER;
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t c = PTHREAD_COND_INITIALIZER; int ready = 0; /* WAITER */ pthread_mutex_lock(&m); while (!ready) // SEMPRE while, mai if (spurious wakeup) pthread_cond_wait(&c, &m); /* ... usa lo stato ... */ pthread_mutex_unlock(&m); /* SIGNALER */ pthread_mutex_lock(&m); ready = 1; pthread_cond_signal(&c); pthread_mutex_unlock(&m);
pthread_cond_wait può ritornare anche senza un signal esplicito. Il test del predicato deve quindi essere in un ciclo while, non in un if.| Funzione | Quando usarla |
|---|---|
pthread_cond_signal | Il cambiamento di stato permette a un solo waiter di procedere (tipico: coda "uno sloto libero") |
pthread_cond_broadcast | Lo stato permette a molti waiter (es. "ho appena finito la fase X", rwlock, shutdown) |
signal è solo un'ottimizzazione.typedef struct { pthread_mutex_t m; pthread_cond_t c; int fired; } event_t; void event_init(event_t *e) { pthread_mutex_init(&e->m, NULL); pthread_cond_init(&e->c, NULL); e->fired = 0; } void event_wait(event_t *e) { pthread_mutex_lock(&e->m); while (!e->fired) pthread_cond_wait(&e->c, &e->m); pthread_mutex_unlock(&e->m); } void event_fire(event_t *e) { pthread_mutex_lock(&e->m); e->fired = 1; pthread_cond_broadcast(&e->c); pthread_mutex_unlock(&e->m); }
typedef struct { pthread_mutex_t m; pthread_cond_t c; int count; } latch_t; void latch_countdown(latch_t *l) { pthread_mutex_lock(&l->m); if (--l->count == 0) pthread_cond_broadcast(&l->c); pthread_mutex_unlock(&l->m); } void latch_wait(latch_t *l) { pthread_mutex_lock(&l->m); while (l->count > 0) pthread_cond_wait(&l->c, &l->m); pthread_mutex_unlock(&l->m); }
struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); // orario attuale ts.tv_sec += 5; // timeout assoluto fra 5 s pthread_mutex_lock(&m); while (!ready) { int r = pthread_cond_timedwait(&c, &m, &ts); if (r == ETIMEDOUT) { /* timeout */ break; } } pthread_mutex_unlock(&m);
CLOCK_MONOTONIC (preferibile) bisogna settarlo negli attributi della cond con pthread_condattr_setclock.pthread_condattr_t ca; pthread_condattr_init(&ca); pthread_condattr_setclock(&ca, CLOCK_MONOTONIC); pthread_cond_t c; pthread_cond_init(&c, &ca); pthread_condattr_destroy(&ca); struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); ts.tv_sec += 2; pthread_cond_timedwait(&c, &m, &ts); // Non soggetto a salti all'indietro dell'ora di sistema
Permette a N lettori di accedere concorrentemente al dato, ma solo a 1 scrittore alla volta, in mutua esclusione con tutti i lettori. Conviene quando le letture dominano nettamente le scritture.
pthread_rwlock_t rw = PTHREAD_RWLOCK_INITIALIZER; int pthread_rwlock_init(pthread_rwlock_t *rw, const pthread_rwlockattr_t *a); int pthread_rwlock_destroy(pthread_rwlock_t *rw); int pthread_rwlock_rdlock(pthread_rwlock_t *rw); // read, bloccante int pthread_rwlock_tryrdlock(pthread_rwlock_t *rw); // EBUSY se impossibile int pthread_rwlock_timedrdlock(pthread_rwlock_t *rw, const struct timespec *abs); int pthread_rwlock_wrlock(pthread_rwlock_t *rw); // write, bloccante int pthread_rwlock_trywrlock(pthread_rwlock_t *rw); int pthread_rwlock_timedwrlock(pthread_rwlock_t *rw, const struct timespec *abs); int pthread_rwlock_unlock(pthread_rwlock_t *rw); // unica unlock per read/write
pthread_rwlock_t db_lock = PTHREAD_RWLOCK_INITIALIZER; int lookup(int key) { pthread_rwlock_rdlock(&db_lock); int v = db_get(key); pthread_rwlock_unlock(&db_lock); return v; } void update(int key, int val) { pthread_rwlock_wrlock(&db_lock); db_set(key, val); pthread_rwlock_unlock(&db_lock); }
setkind_np (GNU) permette di preferire gli scrittori: PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP.• Un thread non deve tenere contemporaneamente un rdlock e un wrlock sullo stesso rwlock (UB)
• rdlock ricorsivo per lo stesso thread è permesso su molte impl., ma non portabile
• L'unlock funziona sia su rdlock sia su wrlock: libera il tipo corrente
Una barriera sincronizza un gruppo di N thread: nessuno procede finché tutti N non l'hanno raggiunta. Dopo lo sblocco la barriera si "ricarica" e può essere usata di nuovo per la fase successiva. Utile in algoritmi a fasi (simulazioni, calcolo matriciale).
pthread_barrier_t b; int pthread_barrier_init(pthread_barrier_t *b, const pthread_barrierattr_t *a, unsigned count); int pthread_barrier_wait(pthread_barrier_t *b); int pthread_barrier_destroy(pthread_barrier_t *b);
pthread_barrier_wait ritorna:
• 0 per tutti tranne uno
• PTHREAD_BARRIER_SERIAL_THREAD (positivo) per uno dei thread (utile per eseguire codice di "fine fase" una sola volta)
• codice errore altrimenti
#define N 4 pthread_barrier_t bar; void *phase(void *arg) { int id = (intptr_t)arg; for (int step = 0; step < 3; step++) { compute(id, step); int r = pthread_barrier_wait(&bar); if (r == PTHREAD_BARRIER_SERIAL_THREAD) printf("--- fase %d completata ---\n", step); } return NULL; } int main(void) { pthread_barrier_init(&bar, NULL, N); pthread_t t[N]; for (int i = 0; i < N; i++) pthread_create(&t[i], NULL, phase, (void*)(intptr_t)i); for (int i = 0; i < N; i++) pthread_join(t[i], NULL); pthread_barrier_destroy(&bar); }
Uno spinlock è un lock che, se occupato, fa busy-wait anziché mettere il thread a dormire. Utile solo quando la sezione critica è brevissima (decine di ns) e si è sicuri che il thread owner sia su un'altra CPU. Con una sola CPU logica o sezioni lunghe → solo spreco di cicli.
pthread_spinlock_t sl; int pthread_spin_init(pthread_spinlock_t *l, int pshared); int pthread_spin_destroy(pthread_spinlock_t *l); int pthread_spin_lock(pthread_spinlock_t *l); // busy-wait int pthread_spin_trylock(pthread_spinlock_t *l); int pthread_spin_unlock(pthread_spinlock_t *l); // pshared: PTHREAD_PROCESS_PRIVATE / PTHREAD_PROCESS_SHARED
pthread_mutex_t è più veloce di pthread_spinlock_t in user-space.pthread_once_t once = PTHREAD_ONCE_INIT; int pthread_once(pthread_once_t *once, void (*init)(void)); // Esegue init() esattamente una volta, anche se chiamata da N thread. // I thread successivi attendono la fine di init() e poi ritornano subito.
static pthread_once_t g_once = PTHREAD_ONCE_INIT; static Config *g_cfg = NULL; static void init_cfg(void) { g_cfg = config_load("/etc/app.conf"); } Config *get_cfg(void) { pthread_once(&g_once, init_cfg); return g_cfg; }
Un semaforo contatore mantiene un intero ≥ 0. sem_wait decrementa (bloccando se già a 0); sem_post incrementa e risveglia un waiter. A differenza dei mutex, un semaforo non ha un owner: può essere rilasciato da un thread diverso da quello che l'ha acquisito. Perfetto come contatore di risorse o per segnalazione.
<semaphore.h>)sem_t s; int sem_init(sem_t *s, int pshared, unsigned value); // pshared=0 : condiviso solo tra thread // pshared!=0: in shared memory tra processi (s deve stare nel segmento condiviso) int sem_wait(sem_t *s); // blocca se count == 0 int sem_trywait(sem_t *s); // EAGAIN se impossibile int sem_timedwait(sem_t *s, const struct timespec *abs); int sem_post(sem_t *s); // incrementa int sem_getvalue(sem_t *s, int *v); int sem_destroy(sem_t *s);
sem_* seguono la convenzione POSIX "storica": ritornano -1 in caso di errore settando errno (al contrario delle pthread_*).static sem_t slots; void *handle(void *arg) { sem_wait(&slots); // prendi uno slot do_work(arg); sem_post(&slots); // rilascia lo slot return NULL; } int main(void) { sem_init(&slots, 0, 10); // 10 risorse disponibili /* ... crea tanti thread che chiamano handle ... */ sem_destroy(&slots); }
sem_wait può ritornare con EINTR a causa di un segnale, quindi va messo in un ciclo di retry./* C11 standard */ _Thread_local int errors; /* Macro equivalente (C11) */ #include <threads.h> thread_local int errors; /* Estensione GCC/Clang (pre-C11, molto portabile) */ __thread int errors;
Ogni thread ha la propria copia della variabile, allocata automaticamente alla creazione del thread e distrutta alla sua uscita. Molto più veloce di pthread_key_t.
• Solo per variabili a durata statica (globali, static)
• Non può essere inizializzata con espressioni non costanti
• Non invoca distruttori: se tiene un puntatore, devi gestirlo a mano
• Alcune vecchie piattaforme non la supportano → fallback con pthread_key_t
int pthread_key_create(pthread_key_t *k, void (*dtor)(void*)); int pthread_key_delete(pthread_key_t k); void *pthread_getspecific(pthread_key_t k); int pthread_setspecific(pthread_key_t k, const void *val);
Crea una "chiave" globale; ogni thread può associarle un proprio valore void *. Alla terminazione del thread il distruttore associato viene chiamato su ogni valore non-NULL, permettendo free() automatiche.
static pthread_key_t buf_key; static pthread_once_t once = PTHREAD_ONCE_INIT; static void buf_dtor(void *p) { free(p); } static void init_key(void) { pthread_key_create(&buf_key, buf_dtor); } char *thread_buf(void) { pthread_once(&once, init_key); char *p = pthread_getspecific(buf_key); if (!p) { p = malloc(4096); pthread_setspecific(buf_key, p); } return p; }
int pthread_cancel(pthread_t tid); int pthread_setcancelstate(int state, int *old); // state: PTHREAD_CANCEL_ENABLE (default) / DISABLE int pthread_setcanceltype(int type, int *old); // type: PTHREAD_CANCEL_DEFERRED (default) / ASYNCHRONOUS void pthread_testcancel(void); // punto di cancellazione esplicito
La cancellazione non è un "kill" immediato: il thread termina solo quando raggiunge un cancellation point (se DEFERRED) oppure subito (ASYNCHRONOUS, rischioso — stato inconsistente).
Molte syscall bloccanti lo sono: read, write, open, close, wait, sleep, nanosleep, pthread_cond_wait, pthread_join, select, poll, sem_wait. Lista completa: man 7 pthreads.
void *worker(void *arg) { pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL); while (1) { compute_chunk(); pthread_testcancel(); // controllo esplicito } return NULL; } /* Da un altro thread: */ pthread_cancel(tid); pthread_join(tid, &ret); if (ret == PTHREAD_CANCELED) { /* cancellato */ }
void pthread_cleanup_push(void (*routine)(void*), void *arg); void pthread_cleanup_pop(int execute); // push/pop sono MACRO che aprono/chiudono un blocco: devono // apparire nello STESSO scope sintattico. // L'handler viene eseguito se: // - il thread viene cancellato // - il thread chiama pthread_exit // - pop(1) viene eseguito esplicitamente
static void unlock_m(void *arg) { pthread_mutex_unlock((pthread_mutex_t *)arg); } void *worker(void *arg) { pthread_mutex_lock(&m); pthread_cleanup_push(unlock_m, &m); while (!ready) pthread_cond_wait(&c, &m); // cancel point! pthread_cleanup_pop(1); // esegue unlock normale return NULL; }
pthread_cond_wait, il mutex viene ri-acquisito prima della terminazione: l'handler lo rilascia correttamente.• I sigaction handler sono condivisi tra tutti i thread del processo
• La signal mask è invece privata a ogni thread
• Un segnale diretto al processo viene consegnato a uno qualsiasi dei thread che non lo ha mascherato
• pthread_kill invia un segnale a uno specifico thread
int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset); // how: SIG_BLOCK / SIG_UNBLOCK / SIG_SETMASK int pthread_kill(pthread_t tid, int sig); int sigwait(const sigset_t *set, int *sig); int sigtimedwait(const sigset_t *set, siginfo_t *info, const struct timespec *t);
sigset_t set; sigemptyset(&set); sigaddset(&set, SIGINT); sigaddset(&set, SIGTERM); pthread_sigmask(SIG_BLOCK, &set, NULL); // BLOCCA nel main PRIMA di creare i thread void *sig_thread(void *arg) { sigset_t *s = (sigset_t *)arg; int sig; while (1) { sigwait(s, &sig); // attesa sincrona if (sig == SIGTERM) shutdown_gracefully(); } } pthread_t tid; pthread_create(&tid, NULL, sig_thread, &set);
Dentro un signal handler si possono chiamare solo funzioni async-signal-safe (lista in man 7 signal-safety): write, _exit, read, sem_post, pthread_sigmask, etc. Non sono sicuri: printf, malloc, pthread_mutex_lock.
Leggere/scrivere una variabile condivisa senza lock è una race condition, anche per un singolo int: il compilatore può tenerla in registro, riordinare, e una scrittura non allineata può non essere atomica. Le operazioni in <stdatomic.h> garantiscono atomicità e ordine di memoria.
#include <stdatomic.h> atomic_int counter = 0; atomic_bool flag = false; _Atomic(MyStruct *) ptr; /* Funzioni principali */ atomic_store(&counter, 0); int v = atomic_load(&counter); atomic_fetch_add(&counter, 1); atomic_fetch_sub(&counter, 1); atomic_exchange(&counter, 42); /* Compare-And-Swap */ int expected = 0; if (atomic_compare_exchange_strong(&counter, &expected, 1)) { /* ha scambiato 0 → 1 */ } else { /* expected ora contiene il valore corrente */ }
static atomic_flag spin = ATOMIC_FLAG_INIT; void lock(void) { while(atomic_flag_test_and_set(&spin)); } void unlock(void) { atomic_flag_clear(&spin); }
/* Pre-C11, ancora diffusi nel kernel e in molte librerie */ int old = __sync_fetch_and_add(&counter, 1); if (__sync_bool_compare_and_swap(&ptr, old, new)) { /* ok */ } /* Più moderni: __atomic_* con memory order esplicito */ __atomic_load_n(&counter, __ATOMIC_ACQUIRE); __atomic_store_n(&counter, 1, __ATOMIC_RELEASE);
| Ordering | Garanzie | Costo |
|---|---|---|
relaxed | Solo atomicità. Nessun constraint di ordine. | Minimo |
consume | Dipendenze sui dati. (Praticamente trattato come acquire) | Basso |
acquire | Sulle load: nessuna lettura/scrittura successiva può migrare prima. | Medio |
release | Sulle store: nessuna lettura/scrittura precedente può migrare dopo. | Medio |
acq_rel | Su RMW: combina acquire (sul load) e release (sulla store). | Medio |
seq_cst | Ordine totale globale su tutte le operazioni seq_cst. Default. | Massimo |
_Atomic(Item *) shared = NULL; /* Publisher */ Item *it = make_item(); atomic_store_explicit(&shared, it, memory_order_release); /* Subscriber */ Item *it = atomic_load_explicit(&shared, memory_order_acquire); if (it) use(it); // release → acquire stabilisce "happens-before": tutte le scritture // fatte dal publisher PRIMA dello store sono visibili al subscriber // DOPO il load.
seq_cst è quasi sempre abbastanza veloce e drasticamente più facile da ragionare.| Categoria | Significato |
|---|---|
| MT-Safe | Può essere chiamata da più thread simultaneamente senza problemi |
| MT-Unsafe | Comportamento indefinito se usata da più thread senza sincronizzazione esterna |
| AS-Safe | Sicura in un signal handler async |
| AC-Safe | Sicura rispetto alla cancellazione del thread |
man 3 strtok riporta "MT-Unsafe race:strtok".| Non-Safe | Alternativa reentrant |
|---|---|
strtok | strtok_r |
asctime, ctime | asctime_r, ctime_r |
gmtime, localtime | gmtime_r, localtime_r |
rand | rand_r, random_r, o PRNG thread-local |
getpwuid, getpwnam | getpwuid_r, getpwnam_r |
readdir | In POSIX.1-2008 è MT-Safe con DIR* diversi; altrimenti readdir_r (deprecato) |
gethostbyname | getaddrinfo |
/* -pthread definisce _REENTRANT: alcune funzioni libc
(es. errno, stdio) usano allora versioni MT-safe. */
gcc -pthread -Wall -Wextra -c main.c
• Reentrant: può essere interrotta a metà e rientrata (es. da un signal handler) senza corruzione.
• Thread-safe: può essere chiamata da thread diversi in concorrenza.
Sono concetti diversi: una funzione protetta da un mutex è thread-safe ma non reentrant (il mutex non è ricorsivo e il signal handler si auto-deadlock-erebbe).
• Nessuna variabile globale/static di stato modificabile
• Nessun buffer statico restituito (ctime style) → usare buffer del chiamante
• Non chiamare funzioni non-reentrant
• Non modificare il proprio codice, né lo stato della libc (signal mask, locale)
In una libc moderna con _REENTRANT/-pthread, errno è una macro che si espande in una lvalue thread-local (tipicamente (*__errno_location())). Quindi ogni thread vede il proprio errno, e non c'è bisogno di sincronizzarlo.
/* Tipica definizione in <errno.h> con _REENTRANT */ #define errno (*__errno_location()) /* Quindi questo è safe in un programma multithread: */ if (read(fd, buf, n) < 0 && errno == EINTR) /* ... */;
Ricorda: le funzioni pthread_* ritornano direttamente il codice errore. Non ispezionare errno dopo. Eccezione: sem_* (semafori POSIX) usano errno.
#define CAP 64 typedef struct { void *buf[CAP]; size_t head, tail, count; pthread_mutex_t m; pthread_cond_t not_full; pthread_cond_t not_empty; int closed; } queue_t; void q_init(queue_t *q) { q->head = q->tail = q->count = 0; q->closed = 0; pthread_mutex_init(&q->m, NULL); pthread_cond_init(&q->not_full, NULL); pthread_cond_init(&q->not_empty, NULL); } int q_push(queue_t *q, void *v) { pthread_mutex_lock(&q->m); while (q->count == CAP && !q->closed) pthread_cond_wait(&q->not_full, &q->m); if (q->closed) { pthread_mutex_unlock(&q->m); return -1; } q->buf[q->tail] = v; q->tail = (q->tail + 1) % CAP; q->count++; pthread_cond_signal(&q->not_empty); pthread_mutex_unlock(&q->m); return 0; } int q_pop(queue_t *q, void **out) { pthread_mutex_lock(&q->m); while (q->count == 0 && !q->closed) pthread_cond_wait(&q->not_empty, &q->m); if (q->count == 0) { /* closed e vuota */ pthread_mutex_unlock(&q->m); return -1; } *out = q->buf[q->head]; q->head = (q->head + 1) % CAP; q->count--; pthread_cond_signal(&q->not_full); pthread_mutex_unlock(&q->m); return 0; } void q_close(queue_t *q) { pthread_mutex_lock(&q->m); q->closed = 1; pthread_cond_broadcast(&q->not_full); pthread_cond_broadcast(&q->not_empty); pthread_mutex_unlock(&q->m); }
closed permette lo shutdown pulito.• Coda unbounded: basta not_empty, niente not_full (il produttore non blocca mai).
• Coda lock-free: uso di CAS e atomic_*; complesso, solo per latenze estreme.
• Single-Producer Single-Consumer (SPSC): ring buffer con due indici atomici, senza mutex.
pthread_rwlock_t cache_lock = PTHREAD_RWLOCK_INITIALIZER; void *reader(void *arg) { pthread_rwlock_rdlock(&cache_lock); read_cache(); pthread_rwlock_unlock(&cache_lock); return NULL; } void *writer(void *arg) { pthread_rwlock_wrlock(&cache_lock); update_cache(); pthread_rwlock_unlock(&cache_lock); return NULL; }
typedef struct { pthread_mutex_t m; pthread_cond_t no_writers; int readers; int writer; } rw_t; void rd_lock(rw_t *rw) { pthread_mutex_lock(&rw->m); while (rw->writer) pthread_cond_wait(&rw->no_writers, &rw->m); rw->readers++; pthread_mutex_unlock(&rw->m); } void rd_unlock(rw_t *rw) { pthread_mutex_lock(&rw->m); if (--rw->readers == 0) pthread_cond_signal(&rw->no_writers); pthread_mutex_unlock(&rw->m); } void wr_lock(rw_t *rw) { pthread_mutex_lock(&rw->m); while (rw->writer || rw->readers > 0) pthread_cond_wait(&rw->no_writers, &rw->m); rw->writer = 1; pthread_mutex_unlock(&rw->m); } void wr_unlock(rw_t *rw) { pthread_mutex_lock(&rw->m); rw->writer = 0; pthread_cond_broadcast(&rw->no_writers); pthread_mutex_unlock(&rw->m); }
typedef void (*task_fn)(void *); typedef struct { task_fn fn; void *arg; } task_t; typedef struct { pthread_t *threads; size_t nthreads; queue_t q; // coda produttore/consumatore int stop; } pool_t; static void *pool_worker(void *arg) { pool_t *p = arg; while (1) { void *v; if (q_pop(&p->q, &v) < 0) break; // coda chiusa task_t *t = v; t->fn(t->arg); free(t); } return NULL; } void pool_init(pool_t *p, size_t n) { p->nthreads = n; p->threads = calloc(n, sizeof(*p->threads)); q_init(&p->q); for (size_t i = 0; i < n; i++) pthread_create(&p->threads[i], NULL, pool_worker, p); } void pool_submit(pool_t *p, task_fn fn, void *arg) { task_t *t = malloc(sizeof(*t)); t->fn = fn; t->arg = arg; q_push(&p->q, t); } void pool_shutdown(pool_t *p) { q_close(&p->q); for (size_t i = 0; i < p->nthreads; i++) pthread_join(p->threads[i], NULL); free(p->threads); }
sysconf(_SC_NPROCESSORS_ONLN) worker è una buona baseline per lavori CPU-bound; per I/O-bound spesso conviene alzare a 2×/4× il numero di core.Un monitor incapsula lo stato e la sincronizzazione in un'unica struttura: ogni metodo pubblico acquisisce il mutex, opera, e lo rilascia. Esterno al monitor non esistono accessi diretti allo stato.
typedef struct { pthread_mutex_t m; int value; } counter_t; void counter_inc(counter_t *c) { pthread_mutex_lock(&c->m); c->value++; pthread_mutex_unlock(&c->m); } int counter_get(counter_t *c) { pthread_mutex_lock(&c->m); int v = c->value; pthread_mutex_unlock(&c->m); return v; }
Un deadlock richiede TUTTE queste condizioni:
• Mutua esclusione: la risorsa non è condivisibile
• Hold & wait: un thread tiene una risorsa mentre ne attende un'altra
• No preemption: la risorsa viene rilasciata solo volontariamente
• Circular wait: esiste un ciclo di attesa T1→T2→...→T1
/* Thread A */ /* Thread B */
pthread_mutex_lock(&m1); pthread_mutex_lock(&m2);
pthread_mutex_lock(&m2); // blocca pthread_mutex_lock(&m1); // blocca
... ...
←─────── DEADLOCK ───────→
• Ordine lock totale: assegna un numero a ogni mutex; acquisiscili sempre in ordine crescente. Rompe il circular wait.
• Try-lock + backoff: se un lock è occupato, rilascia quelli già presi e riprova.
• Big lock: un unico mutex globale (semplice, non scala).
• Lock-free: strutture dati che non usano lock (CAS).
• Hierarchical locks: un solo lock per livello di astrazione.
void lock_two(pthread_mutex_t *a, pthread_mutex_t *b) { if ((uintptr_t)a < (uintptr_t)b) { pthread_mutex_lock(a); pthread_mutex_lock(b); } else { pthread_mutex_lock(b); pthread_mutex_lock(a); } }
• ERRORCHECK mutex: pthread_mutex_lock restituisce EDEADLK se il chiamante già lo detiene.
• ThreadSanitizer (-fsanitize=thread): identifica cicli di lock.
• helgrind (valgrind): rileva ordini di lock incoerenti.
• gdb su un processo bloccato: thread apply all bt mostra dove ogni thread è bloccato.
I thread non sono bloccati ma non fanno progresso: entrambi continuano a riprovare "gentilmente" senza mai avanzare. Esempio: due try-lock + backoff sincronizzati.
Rimedio: backoff casuale (esponenziale + jitter).
Un thread non ottiene mai una risorsa perché altri thread la prendono sempre per primi. Tipica in:
• rwlock con molti lettori (gli scrittori muoiono di fame)
• priorità troppo basse (SCHED_OTHER con nice alti)
• lock "unfair" dove il kernel dà sempre preferenza allo stesso thread
Rimedio: lock fair (FIFO), priority inheritance, aging.
Un thread ad alta priorità resta in attesa di un lock tenuto da un thread a bassa priorità, che a sua volta non gira perché soppiantato da un thread a priorità media. Soluzione: attributo mutex PTHREAD_PRIO_INHERIT: il thread che tiene il lock eredita temporaneamente la priorità del più alto waiter.
Due o più thread accedono a una stessa variabile, di cui almeno uno in scrittura, senza una relazione happens-before (mutex, atomic, join, etc.). Comportamento indefinito in C11.
/* 1. Incremento non atomico */ counter++; // load, add, store — non atomica! /* 2. Double-checked locking SENZA atomic (scorretto) */ if (!init) { // read non sincronizzata → data race pthread_mutex_lock(&m); if (!init) { do_init(); init = 1; } pthread_mutex_unlock(&m); } /* Corretto: usare pthread_once o atomic_load(acquire) */ /* 3. Visibilità di un flag di shutdown */ volatile int done = 0; // volatile NON basta tra thread! /* Usa atomic_int o un mutex. */
volatile è per MMIO e setjmp, non per la concorrenza tra thread. In C/C++ moderni usa _Atomic o i mutex.if (access("/tmp/file", R_OK) == 0) { // check int fd = open("/tmp/file", O_RDONLY); // use → un altro processo/thread // può aver cambiato il file! }
Tra il check e lo use può inserirsi un altro thread o processo. Soluzioni: openat + O_NOFOLLOW, fstat sull'fd già aperto, flock, transazioni.
Lo standard C11 introduce un'API threading standard (header <threads.h>). Supportata da glibc ≥ 2.28 e Musl. È un sottoinsieme minimale di pthread ma portabile anche fuori POSIX.
| C11 | Equivalente POSIX |
|---|---|
thrd_t | pthread_t |
thrd_create, thrd_join, thrd_detach, thrd_exit | pthread_create, pthread_join, ... |
mtx_t (plain, recursive, timed) | pthread_mutex_t |
mtx_init, mtx_lock, mtx_trylock, mtx_unlock, mtx_destroy | pthread_mutex_* |
cnd_t, cnd_wait, cnd_signal, cnd_broadcast | pthread_cond_* |
once_flag, call_once | pthread_once_t, pthread_once |
tss_t, tss_create, tss_get, tss_set | pthread_key_t, pthread_getspecific |
#include <threads.h> int worker(void *arg) { // firma diversa: ritorna int, non void* return 0; } int main(void) { thrd_t t; thrd_create(&t, worker, NULL); int res; thrd_join(t, &res); return res; }
thrd_cancel: la cancellazione non è parte di C11. Per codice non-portabile o che usa cancellation, rimani su pthread.
gcc -fsanitize=thread -g -O1 -pthread -o prog main.c
./prog
# Output esempio:
# WARNING: ThreadSanitizer: data race (pid=12345)
# Write of size 4 at 0x7b04... by thread T2:
# #0 worker main.c:42
# Previous read of size 4 at 0x7b04... by thread T1:
# #0 worker main.c:38
Identifica data race, deadlock e uso scorretto di mutex. Leggero overhead (5–15×). Non combinabile con AddressSanitizer nella stessa build.
valgrind --tool=helgrind ./prog
valgrind --tool=drd ./prog
# helgrind: rileva race e ordering violations sui pthread
# drd: più focalizzato su deadlock e API misuse
Più lenti di tsan (20–50×) ma non richiedono ricompilare. Utili se la toolchain non supporta -fsanitize=thread.
info threads # lista tutti i thread thread 3 # passa al thread #3 thread apply all bt # backtrace di tutti i thread thread apply all bt full # con variabili locali set scheduler-locking on # step solo del thread corrente break func thread 2 # breakpoint solo per quel thread break func if pthread_self()==X
# Contare i thread di un processo ls /proc/<PID>/task | wc -l cat /proc/<PID>/status | grep Threads # top con visualizzazione per thread top -H -p <PID> ps -T -p <PID> # strace di tutti i thread strace -f -p <PID> # Profiling dei lock (contesa) perf lock record -- ./prog perf lock report
• Inizializza sempre i mutex e verifica i ritorni delle API
• Preferisci _Thread_local quando basta invece di pthread_key_t
• Tieni la sezione critica piccola (copia fuori, elabora, ri-lock per scrivere)
• Evita printf dentro sezioni critiche (lock implicito su stdout)
• Un'unica convenzione di acquisizione lock nel progetto (ordine totale)
• Testa in debug con -fsanitize=thread in CI
• Niente "volatile" per la concorrenza, solo atomic/mutex
• Documenta le invarianti che ogni mutex protegge
| Codice | Significato |
|---|---|
EAGAIN | Risorse insufficienti (max threads, memoria stack) |
EINVAL | Argomento non valido (mutex non inizializzato, attr errato) |
EBUSY | Tentativo di destroy su mutex locked, o trylock fallita |
EDEADLK | Deadlock rilevato (ERRORCHECK) o join su se stessi |
EPERM | Unlock di un mutex non posseduto / policy scheduling non permessa |
ESRCH | Nessun thread con quel TID (probabilmente già joinato) |
ETIMEDOUT | Timeout scaduto in timed wait/lock |
EOWNERDEAD | Il precedente owner di un robust mutex è morto tenendo il lock |
ENOTRECOVERABLE | Robust mutex non recuperabile (consistent non chiamata) |
#define PT_CHECK(expr) do { \ int _e = (expr); \ if (_e != 0) { \ fprintf(stderr, "%s:%d: %s: %s\n", \ __FILE__, __LINE__, #expr, strerror(_e)); \ abort(); \ } \ } while (0) PT_CHECK(pthread_create(&tid, NULL, worker, arg)); PT_CHECK(pthread_mutex_lock(&m)); PT_CHECK(pthread_mutex_unlock(&m));
pthread_mutex_lock è quasi sempre un bug irrecuperabile: meglio abort() che proseguire con invarianti rotte.