MODELLO DI THREAD POSIX
Cos'è un Thread

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().

Modelli 1:1, N:1, M:N
ModelloDescrizioneEsempi
1:1Ogni thread user ↔ thread kernel. Vera parallelizzazione multi-core, syscall bloccanti isolate.Linux NPTL, Solaris, Windows
N:1Più thread user su un solo thread kernel. Scheduling in user-space, syscall bloccante blocca tutti.GNU Portable Threads
M:NM thread user mappati su N thread kernel. Complesso, poco usato oggi.Solaris pre-9, NetBSD SA
Vantaggi vs Svantaggi

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

THREAD VS PROCESSO
Risorse Condivise / Private
RisorsaProcessi (dopo fork)Thread
Spazio di indirizzamentoCopia (COW)Condiviso
Code, data, heapCopia privataCondivisi
StackCopia privataPrivato per thread
File descriptorCopia (stesso file offset)Condivisi
PIDDiversoUnico PID (TID diverso)
Signal handlerEreditati (copia)Condivisi
Signal maskEreditataPrivata per thread
Signal pendentiSeparatiPer thread + per processo
errnoSeparatoPrivato (TLS)
cwd, umask, rlimitEreditatiCondivisi
Timer / alarmAzzeratiCondivisi
ID threadpthread_t / TID
Quando Usare Cosa

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 & COMPILAZIONE
Header Principali
HeaderContenuto
<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
Compilazione e Linking
# 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
Il flag -pthread (senza la "-l") definisce la macro _REENTRANT (o _POSIX_THREADS) e attiva versioni thread-safe delle funzioni libc. Preferiscilo sempre a -lpthread.
Convenzione Ritorno delle API pthread
/* 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)  */
NON usare perror() dopo una pthread_* fallita: errno non è settato. Usa sempre strerror(err) sul valore di ritorno.
SPAZIO DI MEMORIA DI UN PROCESSO MULTITHREAD
Layout con più Thread

Ogni thread ha il proprio stack (default ~8 MiB su Linux), mentre code, heap, data e bss sono condivisi.

0xFFFF...FFFF
KERNEL SPACEInaccessibile in user mode
0x7FFF...FFFF
STACK THREAD MAINCresce ↓. Default RLIMIT_STACK (~8 MiB)
STACK THREAD #1Allocato da pthread_create (default 8 MiB)
STACK THREAD #2Thread-local (TLS) segue il TCB di ogni thread
... mmap area (stack aggiuntivi, librerie, mmap) ...
HEAP (condiviso)malloc/free da qualunque thread — richiede MT-safe allocator
BSS (condiviso)Globali/static non inizializzati
DATA / .rodata (condivisi)Globali inizializzati, costanti
0x0040 0000
TEXT (condiviso)Codice eseguibile, condiviso tra tutti i thread
Le variabili static e globali sono condivise tra tutti i thread: ogni accesso in scrittura richiede sincronizzazione o dichiarazione _Thread_local / __thread.
pthread_create()
Prototipo
#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
Esempio Base
#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;
}
Non passare mai l'indirizzo di una variabile d'iterazione come &i: tutti i thread vedrebbero lo stesso indirizzo in evoluzione. Usa un array di indici o una struct allocata separatamente.
Passare Strutture Dati
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);
Preferisci malloc per ogni task rispetto allo stack del chiamante: elimina il rischio che la variabile scada prima che il thread la legga.
Errori Comuni di pthread_create
errnoSignificato
EAGAINRisorse insufficienti (ulimit -u, memoria per stack)
EINVALAttributi non validi
EPERMPermessi insufficienti per la policy di scheduling richiesta
pthread_exit()
Terminazione del Thread
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

main() e pthread_exit
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);
}
In un thread diverso dal main, exit() termina l'intero processo. Usa pthread_exit() o return per uscire dal singolo thread.
Passare un Valore di Ritorno
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);
Non restituire mai un puntatore a una variabile locale al thread: il suo stack viene deallocato alla terminazione.
pthread_join()
Prototipo
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

Join Non-Bloccante (GNU)
#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);
Queste estensioni sono GNU/Linux-specifiche: per codice portabile, simulale con un flag e una condition variable o con un detached thread.
Zombie Thread

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

pthread_detach()
Modalità Detach
int pthread_detach(pthread_t thread);
// Dichiara il thread come "detached": alla terminazione le risorse
// vengono liberate automaticamente. NON può più essere joinato.
Statopthread_joinPulizia risorse
PTHREAD_CREATE_JOINABLE (default)ObbligatorioAl join
PTHREAD_CREATE_DETACHEDNon permessoAutomatica
Creare un Thread Detached
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
Auto-Detach
void *fire_and_forget(void *arg) {
    pthread_detach(pthread_self());  // il thread si stacca da solo
    /* ... lavoro ... */
    return NULL;
}
pthread_self() & pthread_equal()
Identificazione
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 ==.
TID Kernel (Linux)
#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();
ATTRIBUTI DEL THREAD (pthread_attr_t)
Ciclo di Vita degli Attributi
pthread_attr_t attr;
pthread_attr_init(&attr);                  // inizializza
/* ... setter ... */
pthread_create(&tid, &attr, worker, arg);
pthread_attr_destroy(&attr);               // rilascia
Gli attributi vengono copiati dentro il thread al momento della create: puoi distruggerli subito dopo, anche se il thread non è ancora terminato.
Setter / Getter
AttributoFunzione setterValori
Stato detachpthread_attr_setdetachstatePTHREAD_CREATE_JOINABLE / DETACHED
Dimensione stackpthread_attr_setstacksizesize_t (min PTHREAD_STACK_MIN)
Indirizzo stackpthread_attr_setstackstack_addr + stack_size
Guard sizepthread_attr_setguardsizebyte di guardia oltre lo stack
Scopepthread_attr_setscopePTHREAD_SCOPE_SYSTEM / PROCESS
Inherit schedulingpthread_attr_setinheritschedPTHREAD_INHERIT_SCHED / EXPLICIT
Politicapthread_attr_setschedpolicySCHED_OTHER / FIFO / RR
Parametripthread_attr_setschedparamstruct sched_param
Stack Size Personalizzato
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);
Uno stack troppo piccolo → overflow silenzioso in pagine adiacenti. Usa sempre una guardsize o compila con -fstack-protector.
Scheduling Realtime (root / CAP_SYS_NICE)
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  */
CPU Affinity (GNU/Linux)
#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);
Yield e Sleep
#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);
MUTEX — pthread_mutex_t
Cos'è

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.

API Principali
#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);
Inizializzazione Statica vs Dinamica
/* 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);
Mai distruggere un mutex locked o con thread in attesa: comportamento indefinito. Mai copiare (memcpy) un pthread_mutex_t.
Sezione Critica
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;
}
Tieni la sezione critica piccola: copiare fuori i dati, elaborarli senza lock, poi riacquisire per scrivere il risultato. Lock tenuti a lungo → contesa e perdita di scalabilità.
Pattern RAII-like (scoped lock)
/* 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 */
}
ATTRIBUTI DEL MUTEX
pthread_mutexattr_t
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);
Opzioni
AttributoValoriNote
typeNORMAL, ERRORCHECK, RECURSIVE, DEFAULTVedi tabella sotto
psharedPTHREAD_PROCESS_PRIVATE / SHAREDSHARED → può vivere in shared memory tra processi
robustSTALLED / ROBUSTROBUST: se l'owner muore, il successivo lock ritorna EOWNERDEAD
protocolNONE / INHERIT / PROTECTPriority Inheritance / Priority Ceiling
prioceilingprioritàSolo con PRIO_PROTECT
TIPI DI MUTEX
Comportamento per Tipo
TipoRelock dell'ownerUnlock non-ownerUnlock non-locked
NORMALDeadlockComportamento indefinitoComportamento indefinito
ERRORCHECKEDEADLKEPERMEPERM
RECURSIVEContatore++ (ok)EPERMEPERM
DEFAULTImpl-defined (tipicamente = NORMAL)Impl-definedImpl-defined
Mutex Ricorsivo
/* 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
I mutex ricorsivi costano di più e spesso nascondono un design confuso. Preferisci refactoring (API pubblica che lock→ chiama API privata senza lock).
Robust Mutex
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);
}
trylock e timedlock
/* 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 */
}
CONDITION VARIABLES — pthread_cond_t
Concetto

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.

API
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;
Pattern wait/signal Canonico
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);
Spurious wakeup: 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.
signal vs broadcast
FunzioneQuando usarla
pthread_cond_signalIl cambiamento di stato permette a un solo waiter di procedere (tipico: coda "uno sloto libero")
pthread_cond_broadcastLo stato permette a molti waiter (es. "ho appena finito la fase X", rwlock, shutdown)
Se non sai quanti waiter possono procedere, broadcast è sempre corretto (al costo di più wakeup). signal è solo un'ottimizzazione.
PATTERN CON CONDITION VARIABLES
Una tantum (event)
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);
}
Latch / Countdown
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);
}
TIMED WAIT
Attesa con Timeout
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);
Timestamp assoluto, non durata: se si usa CLOCK_MONOTONIC (preferibile) bisogna settarlo negli attributi della cond con pthread_condattr_setclock.
Clock Monotonic
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
READ-WRITE LOCK — pthread_rwlock_t
Concetto

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.

API
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
Esempio
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);
}
Rischio di starvation degli scrittori: letture continue possono bloccare sempre una scrittura in attesa. L'attributo setkind_np (GNU) permette di preferire gli scrittori: PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP.
Regole

• 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

BARRIERE — pthread_barrier_t
Concetto

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).

API
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

Esempio a Fasi
#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);
}
SPINLOCK — pthread_spinlock_t
Quando Usarlo

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.

API
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
Su glibc+futex, i mutex hanno già un adaptive spin prima di bloccarsi. Nella maggior parte dei casi pthread_mutex_t è più veloce di pthread_spinlock_t in user-space.
pthread_once() — INIT UNA SOLA VOLTA
API
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.
Esempio Singleton
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;
}
Equivalente thread-safe al pattern "if (!initialized) init();" senza il rischio di double-checked locking scorretto.
SEMAFORI POSIX (unnamed, tra thread)
Concetto

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.

API (header <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);
Le sem_* seguono la convenzione POSIX "storica": ritornano -1 in caso di errore settando errno (al contrario delle pthread_*).
Esempio: Limitare le Connessioni
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);
}
I semafori non sono interrotti in modo pulito: sem_wait può ritornare con EINTR a causa di un segnale, quindi va messo in un ciclo di retry.
THREAD-LOCAL STORAGE — _Thread_local / __thread
Storage Class Thread-Local
/* 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.

Limitazioni

• 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

pthread_key_t — TSD CON DISTRUTTORI
API
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.

Esempio: Buffer per Thread
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;
}
CANCELLAZIONE DI THREAD
Concetti
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).

Principali Cancellation Point

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.

Esempio
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 */ }
La cancellazione con stato inconsistente è facile: un thread cancellato mentre tiene un mutex deve rilasciarlo. Soluzione: cleanup handler.
CLEANUP HANDLER
push / pop
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
Esempio Tipico
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;
}
Se un thread viene cancellato dentro pthread_cond_wait, il mutex viene ri-acquisito prima della terminazione: l'handler lo rilascia correttamente.
SEGNALI IN PROGRAMMI MULTITHREAD
Regole Principali

• 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

API
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);
Pattern: Thread Dedicato ai Segnali
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);
Questo pattern evita i problemi di async-signal-safety: il lavoro è fatto da un thread normale, non dentro un handler.
Async-Signal-Safety

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.

ATOMIC C11 — <stdatomic.h>
Perché

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.

Tipi e Funzioni
#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 */
}
atomic_flag (lock-free garantito)
static atomic_flag spin = ATOMIC_FLAG_INIT;

void lock(void)   { while(atomic_flag_test_and_set(&spin)); }
void unlock(void) { atomic_flag_clear(&spin); }
Built-in GCC/Clang
/* 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);
MEMORY ORDER
memory_order_*
OrderingGaranzieCosto
relaxedSolo atomicità. Nessun constraint di ordine.Minimo
consumeDipendenze sui dati. (Praticamente trattato come acquire)Basso
acquireSulle load: nessuna lettura/scrittura successiva può migrare prima.Medio
releaseSulle store: nessuna lettura/scrittura precedente può migrare dopo.Medio
acq_relSu RMW: combina acquire (sul load) e release (sulla store).Medio
seq_cstOrdine totale globale su tutte le operazioni seq_cst. Default.Massimo
Esempio: Publish / Subscribe Senza Lock
_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.
Il codice lock-free è difficile: usa memory order rilassati solo se profili e misuri. Il default seq_cst è quasi sempre abbastanza veloce e drasticamente più facile da ragionare.
THREAD-SAFETY
Classificazione POSIX delle Funzioni libc
CategoriaSignificato
MT-SafePuò essere chiamata da più thread simultaneamente senza problemi
MT-UnsafeComportamento indefinito se usata da più thread senza sincronizzazione esterna
AS-SafeSicura in un signal handler async
AC-SafeSicura rispetto alla cancellazione del thread
La classificazione dettagliata per ogni funzione libc è nella pagina di manuale della funzione, sotto la sezione ATTRIBUTES. Es.: man 3 strtok riporta "MT-Unsafe race:strtok".
Funzioni Non Thread-Safe Tipiche
Non-SafeAlternativa reentrant
strtokstrtok_r
asctime, ctimeasctime_r, ctime_r
gmtime, localtimegmtime_r, localtime_r
randrand_r, random_r, o PRNG thread-local
getpwuid, getpwnamgetpwuid_r, getpwnam_r
readdirIn POSIX.1-2008 è MT-Safe con DIR* diversi; altrimenti readdir_r (deprecato)
gethostbynamegetaddrinfo
Compilare in Modalità Thread-Safe
/* -pthread definisce _REENTRANT: alcune funzioni libc
   (es. errno, stdio) usano allora versioni MT-safe. */
gcc -pthread -Wall -Wextra -c main.c
REENTRANT VS THREAD-SAFE
Differenza

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).

Regole per Scrivere Codice Reentrant

• 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)

errno PER THREAD
Thread-Local in libc

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) /* ... */;
pthread_* NON setta errno

Ricorda: le funzioni pthread_* ritornano direttamente il codice errore. Non ispezionare errno dopo. Eccezione: sem_* (semafori POSIX) usano errno.

PATTERN: PRODUTTORE-CONSUMATORE
Coda Bounded con Mutex + 2 Condvar
#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);
}
Due condition variables evitano risvegli spuri inutili: il produttore sveglia solo i consumatori, e viceversa. Il flag closed permette lo shutdown pulito.
Varianti

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.

PATTERN: LETTORI-SCRITTORI
Implementazione con rwlock (Semplice)
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;
}
Implementazione Manuale (Readers-Preference)
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);
}
Questa è una "readers-preference": se arrivano sempre lettori, gli scrittori muoiono di fame. Per writer-preference servono due contatori (waiting readers / waiting writers) e logica di priorità.
PATTERN: THREAD POOL
Struttura di Base
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);
}
Una pool di N = 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.
PATTERN: MONITOR
Idea

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;
}
Rende facile ragionare sull'invariante: ogni volta che il lock è libero, lo stato è consistente.
DEADLOCK
Le 4 Condizioni di Coffman

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

Esempio Classico
/* Thread A */                        /* Thread B */
pthread_mutex_lock(&m1);             pthread_mutex_lock(&m2);
pthread_mutex_lock(&m2);  // blocca  pthread_mutex_lock(&m1);  // blocca
...                                   ...
  ←─────── DEADLOCK ───────→
Strategie di Prevenzione

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.

Ordine Totale
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);
    }
}
Diagnostica

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.

LIVELOCK & STARVATION
Livelock

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).

Starvation

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.

Priority Inversion

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.

RACE CONDITION
Definizione

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.

Esempi di Race Sottili
/* 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.
TOCTOU (Time Of Check / Time Of Use)
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.

C11 <threads.h>
Panoramica

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.

C11Equivalente POSIX
thrd_tpthread_t
thrd_create, thrd_join, thrd_detach, thrd_exitpthread_create, pthread_join, ...
mtx_t (plain, recursive, timed)pthread_mutex_t
mtx_init, mtx_lock, mtx_trylock, mtx_unlock, mtx_destroypthread_mutex_*
cnd_t, cnd_wait, cnd_signal, cnd_broadcastpthread_cond_*
once_flag, call_oncepthread_once_t, pthread_once
tss_t, tss_create, tss_get, tss_setpthread_key_t, pthread_getspecific
Esempio
#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;
}
Manca thrd_cancel: la cancellazione non è parte di C11. Per codice non-portabile o che usa cancellation, rimani su pthread.
DEBUGGING MULTITHREAD
ThreadSanitizer (tsan)
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.

Helgrind / DRD (Valgrind)
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.

GDB — Thread Essentials
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
Introspezione a Runtime
# 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
Best Practice

• 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

CODICI DI ERRORE pthread_*
Errori Comuni
CodiceSignificato
EAGAINRisorse insufficienti (max threads, memoria stack)
EINVALArgomento non valido (mutex non inizializzato, attr errato)
EBUSYTentativo di destroy su mutex locked, o trylock fallita
EDEADLKDeadlock rilevato (ERRORCHECK) o join su se stessi
EPERMUnlock di un mutex non posseduto / policy scheduling non permessa
ESRCHNessun thread con quel TID (probabilmente già joinato)
ETIMEDOUTTimeout scaduto in timed wait/lock
EOWNERDEADIl precedente owner di un robust mutex è morto tenendo il lock
ENOTRECOVERABLERobust mutex non recuperabile (consistent non chiamata)
Template di Gestione
#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));
In produzione, un fallimento di pthread_mutex_lock è quasi sempre un bug irrecuperabile: meglio abort() che proseguire con invarianti rotte.