Thread

Riassunto IA

Il thread è l'unità di esecuzione minima che condivide lo spazio di indirizzamento del processo, ottimizzando le prestazioni grazie a un overhead di gestione ridotto rispetto ai processi tradizionali. La distinzione fondamentale tra concorrenza logica e parallelismo fisico è oggi supportata da architetture multicore e sistemi eterogenei GPU per l'esecuzione di calcoli massivamente paralleli.

Immagine generata con IA
Immagine generata con IA

Introduzione

In questo articolo esploriamo l'architettura dei thread e la programmazione concorrente all'interno dei sistemi operativi moderni.

Thread

Il thread rappresenta l’unità minima di esecuzione schedulabile dalla CPU all’interno di un processo. Dal punto di vista architetturale, ciascun thread è caratterizzato da:

  • un identificatore univoco;
  • un Program Counter (PC), che individua l’istruzione corrente;
  • un insieme di registri di stato;
  • uno stack privato per la gestione delle chiamate di funzione e delle variabili locali.

All’interno dello stesso processo, i thread condividono lo spazio di indirizzamento e le risorse globali, tra cui il segmento di codice, il segmento dati (heap e variabili globali) e le risorse di sistema allocate (file descriptor, segnali, socket). Tale condivisione costituisce l’elemento distintivo rispetto al modello multiprocesso tradizionale, nel quale ogni processo possiede uno spazio di memoria isolato.

L’introduzione del multithreading risponde a esigenze di efficienza computazionale e di progettazione del software concorrente. I principali benefici possono essere formalizzati come segue:

  • Riduzione della latenza percepita: la suddivisione del lavoro in più thread consente la sovrapposizione tra operazioni di I/O e computazione.
  • Efficienza nell’uso delle risorse: il costo di creazione e distruzione di un thread è inferiore rispetto a quello di un processo, così come l’overhead del context switch.
  • Modularità progettuale: la decomposizione di un’applicazione in unità concorrenti favorisce una strutturazione più chiara delle responsabilità.
  • Scalabilità su architetture multicore: la presenza di più unità di esecuzione fisiche permette l’esecuzione realmente parallela dei thread.

Concorrenza, Parallelismo e Architetture Eterogenee

Nel contesto dei sistemi moderni è necessario distinguere rigorosamente tra concorrenza e parallelismo, abbiamo già affrontato l'argomento quando parlavamo dei processi ma è bene ripetere i concetti al fine di fissare bene le idee.

La concorrenza è una proprietà logica del sistema: più task progrediscono nel tempo, anche se non necessariamente in modo simultaneo. In un sistema single-core, ciò avviene tramite time slicing e meccanismi di scheduling preemptive.

Il parallelismo, invece, è una proprietà fisica dell’hardware: più task sono eseguiti simultaneamente su core distinti. Il parallelismo rappresenta quindi un caso particolare di concorrenza in presenza di risorse computazionali multiple.

L’evoluzione delle architetture ha introdotto sistemi eterogenei CPU–GPU. Le GPU (Graphics Processing Unit), inizialmente progettate per il rendering grafico, sono oggi impiegate nel paradigma GPGPU (General-Purpose computing on GPU). Tali dispositivi integrano un numero elevato di ALU (Arithmetic Logic Units), ottimizzate per il calcolo massivamente parallelo su grandi insiemi di dati omogenei.

Applicazioni tipiche includono:

  • calcolo scientifico e bioinformatica;
  • crittografia e analisi numerica;
  • addestramento di modelli di deep learning;
  • simulazioni fisiche su larga scala.

Modelli di Implementazione del Multithreading

Dal punto di vista sistemistico, si distinguono thread a livello utente e thread a livello kernel.

I thread a livello utente sono gestiti da librerie in spazio utente, senza intervento diretto del kernel nelle operazioni di scheduling interne al processo.

I thread a livello kernel sono invece entità direttamente note al sistema operativo, il quale ne gestisce pianificazione, sospensione e sincronizzazione.

La relazione tra thread utente e thread kernel è formalizzata attraverso modelli di mapping:

  • Many-to-One: più thread utente sono associati a un singolo thread kernel. Il modello minimizza l’overhead (indica il costo aggiuntivo necessario per gestire un’operazione, costo che non contribuisce direttamente al risultato utile, ma è indispensabile per renderla possibile), ma una chiamata bloccante compromette l’intero processo.
  • One-to-One: ogni thread utente corrisponde a un thread kernel. Garantisce parallelismo reale su sistemi multicore, al prezzo di un maggiore consumo di risorse.
  • Many-to-Many: più thread utente sono mappati su un numero limitato di thread kernel, consentendo un compromesso tra flessibilità e scalabilità.
  • Two-Level Model: estensione del many-to-many che consente l’associazione vincolata di specifici thread utente a determinati thread kernel.

La scelta del modello influenza direttamente prestazioni, prevedibilità temporale e complessità di gestione.

Astrazioni di Programmazione e Threading Implicito

Le principali interfacce di programmazione concorrente includono:

  • POSIX Threads (Pthreads): standard che definisce primitive per creazione, sincronizzazione e gestione dei thread in ambienti Unix-like.
  • API Windows: insieme di primitive integrate nel kernel NT per la gestione nativa dei thread.
  • Java Concurrency Framework: insieme di astrazioni ad alto livello (Executor, ExecutorService, Callable, Future) che separano la definizione del task dalla gestione dei thread sottostanti.

Il paradigma del threading implicito trasferisce la responsabilità della gestione concorrente a runtime e librerie specializzate. Tra i principali strumenti:

  • Thread Pool: insieme di thread riutilizzabili per l’esecuzione di task asincroni, con controllo del grado di parallelismo.
  • Fork/Join Framework: modello ricorsivo divide-et-impera basato su work stealing per l’ottimizzazione del bilanciamento del carico.
  • OpenMP, Grand Central Dispatch, Intel TBB: tecnologie che forniscono direttive e costrutti di alto livello per la parallelizzazione controllata.

Tali astrazioni riducono la probabilità di errori sistemici e favoriscono una gestione più robusta delle risorse concorrenti.

Criticità e Differenze tra Sistemi Operativi

La programmazione multithread introduce problematiche complesse, tra cui:

  • condizioni di race e necessità di meccanismi di sincronizzazione (mutex, semafori, monitor);
  • gestione coerente dei segnali in presenza di più flussi di esecuzione;
  • utilizzo del Thread-Local Storage (TLS) per mantenere dati isolati per thread;
  • politiche di cancellazione e terminazione.

La cancellazione può essere asincrona, con interruzione immediata del thread, oppure differita, mediante punti di cancellazione espliciti. L’approccio differito è generalmente preferito per preservare la consistenza dello stato condiviso.

Dal punto di vista implementativo emergono differenze significative:

  • Windows utilizza strutture interne quali ETHREAD, KTHREAD e TEB (Thread Environment Block) per rappresentare e gestire i thread.
  • Linux adotta un modello unificato in cui processi e thread sono rappresentati come task. La system call clone() consente di specificare, tramite flag, il livello di condivisione delle risorse tra entità padre e figlio, realizzando un modello flessibile di creazione concorrente.

L’analisi comparativa evidenzia come le scelte progettuali a livello di kernel influenzino direttamente il modello di programmazione esposto agli sviluppatori e le garanzie offerte in termini di isolamento, prestazioni e controllo della concorrenza.


Commenti