C#, un thread e i suoi dati

Ogni programma, e parte di esso, ha i suoi dati. I thread non fanno eccezione. Le variabili vengono allocate, inizializzate, lette e così via. In questo articolo vedremo gli attributi ThreadStaticThreadLocal (con, come al solito, dei piccoli esempi pratici che cercheranno di dipanare i vostri dubbi in merito): due strumenti messi in campo da C# per avere una sorta di stato di un thread.

ThreadStatic: un campo per tutti i thread

Ogni thread ha il suo stack, in cui alloca le chiamate ai metodi e le variabili. Questo significa anche che ogni thread ha la sua copia delle variabili:

Se facciamo girare questo esempio possiamo vedere che il valore massimo del campo contatore, a fine esecuzione, sarà 20:

using System;
using System.Threading;
namespace EsempiThread
{
    public class ThreadStatic : IEsempio
    {
        public static int contatore;
        public void Run()
        {
            new Thread(() =>
            {
                for (int x = 0; x < 10; x++) { contatore++; Console.WriteLine("Thread A: {0}", contatore); } }).Start(); new Thread(() =>
        {
            for (int x = 0; x < 10; x++)
            {
                contatore++;
                Console.WriteLine("Thread B: {0}", contatore);
            }
        }).Start();
            Console.ReadKey();
        }
    }
}

Quindi tutti e due i thread leggono e scrivono la stessa area di memoria. Ciò può essere male (pensate cosa potrebbe succedere in ambito bancario…).
Se vogliamo che ogni thread legga e scriva la sua copia della variabile sarà sufficiente etichettare il campo contatore con l’attributo [ThreadStatic]. Il risultato sarà che alla fine dell’esecuzione il campo contatore varrà 10, per tutti e due i thread. Facciamo attenzione a quanti campi etichettiamo con ThreadStatic: se da una parte è vero che .NET non pone un limite al numero di campi etichettabili in questo modo, è altresì vero che la dimensione dello stack un limite ce lo pone.

ThreadStatic ha due limitazioni importanti: per prima cosa può essere usato solo con campi statici, e se vogliamo dei dati specifici di un certo thead e di un certo oggetto dobbiamo usare qualche altro strumento. Inoltre, proprio per la natura di ThreadStatic, dobbiamo essere molto, molto attenti quando lo inizializziamo (il valore di partenza sarà uguale per tutti).

ThreadLocal: un’alternativa a ThreadStatic

ThreadLocal si prende carico della località dei dati indipendentemente dalla variabile a cui è applicato. Adesso possiamo memorizzare un riferimento ad un oggetto locale, e non importa se in una variabile statica o d’istanza. Come diretta conseguenza ogni thread ha la sua copia della variabile, e si occupa lui stesso di inizializzarla.


using System;
using System.Threading;

namespace EsempiThread
{
    public class ThreadLocal : IEsempio
    {
        private ThreadLocal<int> interoRandom;
        public ThreadLocal()
        {
            Random randomGenerator = new Random(DateTime.Now.Second);
            interoRandom = new ThreadLocal<int>(() => randomGenerator.Next(15));
        }
        public void Run()
        {
            Thread t1 = new Thread(() =>
            {
                for (int x = 0; x < interoRandom.Value; x++) { Console.WriteLine("Thread A: {0}", x); } }); Thread t2 = new Thread(() =>
            {
                for (int x = 0; x < interoRandom.Value; x++)
                {
                    Console.WriteLine("Thread B {0}", x);
                }
            });
            t1.Start();
            t2.Start();
            t1.Join();
            t2.Join();
            Console.WriteLine("Esecuzione terminata, premere un tasto per terminare.");
            Console.ReadKey();
        }
    }
}

In questo codice ogni thread ha inizializzato il campo interoRandom con un valore casuale da 0 a 15 e ha fatto quello che doveva fare. Un possibile output di questo codice è quello che trovate nell’immagine seguente.

Un po’ di attenzione

Quando un thread viene inizializzato il framework si occupa di copiare il contesto del thread principale (accessibile con Thread.CurrentThread) in quello nuovo. Questa copia ha ovviamente un costo, ed è uno dei motivi alla base di ThreadPool, che vedremo in un articolo successivo. Se il contesto del thread non mi serve posso evitare questa copia tramite una chiamata al metodo ExecutionContext.SuppressFlow().

In conclusione

Abbiamo visto due meccanismi che permettono ad un thread di avere i suoi dati. Come ogni cosa in questo mondo (soprattutto informatico) ognuno dei due meccanismi ha i suoi punti di forza e i suo svantaggi, e la scelta su quale dei due usare dovrebbe (anzi, DEVE) essere guidata dal contesto del particolare problema, e non da cose del tipo “Questo è più succinto” o “Questo mi piace di più”.

Trovate il codice dell’esempio nel 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 *