I thread in C#

Vorreste preparare l’esame 70-483 (Programming in C#) per una certificazione Microsoft? Bene, questo blog fa al caso vostro. Questo che avete davanti è il primo di una serie di articoli dedicati alla preparazione di questo esame. Non scenderò troppo nel dettaglio, ma spero di darvi comunque un buon input. Iniziamo.

Thread: che cos’è?

Un thread non è altro che una sequenza di istruzioni che può essere eseguita concorrentemente ad un’altra. Che significa? Esempio pratico: mentre io sto scrivendo questo articolo (thread) sto ascoltando (altro thread) un po’ di sano blues. Queste due sequenze di operazioni (il picchiettare sulla tastiera e l’ascolto di Steve Ray Vaughan) vengono portate avanti contemporaneamente, e sono indipendenti. Non è sempre così: i thread in qualche modo collaborano ad un compito comune, o competono per l’acquisizione di una risorsa, quindi così indipendenti proprio non sono.

Abbiamo quindi un paradigma di programmazione diverso: da una parte c’è la programmazione sequenziale, dall’altra c’è la programmazione concorrente. La programmazione sequenziale è indubbiamente più semplice, ma allora perché sbattersi? Perché altrimenti i millemila processori che avete sul vostro smartphone o computer sarebbero perfettamente inutili. Senza la concorrenza il vostro fichissimo sistema operativo (qualunque esso sia) sarebbe inutilizzabile farebbe una sola cosa per volta).

Tutto ciò chiaramente non è gratis: il sistema operativo deve gestire tutto ciò, e questo ha un costo in termini di risorse (CPU e memoria). Questo significa anche che non basta mettere su qualche thread per risolvere tutti i nostri probemi, anzi: se i thread non sono necessari (e questo dipende dal contesto della nostra applicazione) facciamo solo danni.

Per una trattazione esaustiva dell’argomento vi rimando alla tonnellata di letteratura in materia, o a qualunque corso universitario di programmazione concorrente.

La classe Thread

La classe Thread la troviamo sotto il namespace System.Threading, e modella un Thread del CLR. Vediamo un piccolo esempio.


using System;
using System.Threading;

namespace EsempiThread
{
    public class Introduzione
    {
        public static void Main(string[] args)
        {
            Thread t = new Thread(new ThreadStart(Worker));
            t.Start();
            for (int i = 0; i < 4; i++)
            {
                Console.WriteLine("Thread principale. Faccio cose, vedo gente...");
                Thread.Sleep(0);
            }
            t.Join();
            Console.WriteLine("Tutti i thread hanno finito il loro lavoro. Premere un tasto per terminare");
            Console.ReadKey();
        }

        private static void Worker()
        {
            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine("Thread worker numero {0}\n",i);
                Thread.Sleep(0);
            }
        }
    }
}

In questo semplice esempio troviamo già alcuni concetti importanti.

  • Thread.Sleep(0): L’overload con un intero come parametro significa “questo thread aspetta per X millisecondi”. Fin qui tutto bene, ma a cosa serve dire “aspetta 0 millisecondi”? Serve a dire “io ho finito”. Quando si ha a che fare con più thread ad ognuno di questi viene associato un certo tempo della CPU. Quando un thread dice Thread.Sleep(0) sta dicendo “io ho finito, la CPU non mi serve più, avanti il prossimo thread”.
  • t.Join(): questo metodo non a caso si chiama come una delle primitive del modello fork-join. La chiamata a Join è un punto di sincronizzazione tra i vari Thred. Da questo punto in poi il thread principale prosegue soltanto quando gli altri thread hanno terminato il loro compito.

E’ bene spendere qualche altra parola sul metodo Join(). Quando va richiamata? Poco sopra ho scritto che i thread non sono indipendenti. Anzi, alcuni thread devono attendere il risultato di altri prima di proseguire. Bene, c’è un punto del codice in cui devo avere il risultato di tutti i thread, ed è lì che va richiamata la Join().
Un possibile output di questo codice è questo:


Attenzione: come avrete notato ho scritto “un possibile output”. Se questo fosse stato un codice sequenziale avrei scritto “L’output di questo codice è il seguente”. Il motivo è presto detto: non abbiamo nessuna garanzia sull’ordine di esecuzione dei thread. Se ne abbiamo 10 (ad esempio), ma anche solo due, non possiamo (anzi, non dovremmo) fare alcuna assunzione sull’ordine di esecuzione dei thread. Poi possiamo anche farlo, siamo tutti adulti consenzienti, ma poi dobbiamo essere pronti a pagarne le spese. Non la sto facendo tragica: imprevedibile l’ordine di esecuzione, imprevedibile il risultato.

Per il codice degli esempi potete fare riferimento al mio repository GitHub.

Se l’articolo vi è piaciuto non esitate a contattarmi sui miei profili social (trovate i link qui accanto) oppure, se preferite, offritemi un caffè (chi mi conosce sa che sono un caffeinomane).

Pierpaolo Paris

Sono uno dei tanti che sono entrati nel girone infernale di Ingegneria Informatica, e che poi ne è anche uscito. Attualmente lavoro come sviluppatore presso una società di consulenza. Ogni tanto mi diletto con la fotografia.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *