Cerca nel sito
Mercoledi, 1 Luglio 2015
Ultimo aggiornamento: Mercoledi, 2 Settembre 2009
Home Articoli Progetti Gallerie
icona freccia navigazioneHomeicona freccia navigazioneArticoliicona freccia navigazioneFramework .NETicona freccia navigazioneThread e sincronizzazione
icona articolo

Thread e sincronizzazione

Spesso ad un programma viene richiesta la possibilità di svolgere più operazioni contemporaneamente: per raggiungere tale finalità si utilizzano i thread che sono dei flussi indipendenti di istruzioni.

Ad esempio l’implementazione di un programma-server potrebbe avvalersi di due thread; uno in attesa di eventuali connessioni in ingresso e l’altro responsabile della comunicazione vera e propria con il client: in tal modo lo svolgimento dell’operazione di attesa non comprometterebbe lo svolgimento dell’operazione di invio e ricezione dei dati e viceversa.


icona articolo

Delegati asincroni



Uno dei modi più semplici per creare un thread consiste nell’utilizzare l’invocazione asincrona di metodi offerta dall’utilizzo dei delegati.

Ricordiamo che un delegato costituisce una sorta di puntatore a funzione in quanto mantiene un riferimento ad un metodo che ha la sua stessa firma: l’invocazione di un delegato comporta l’invocazione del metodo (o dei metodi nel caso di delegati multicast) ad esso associato.

Così se ad esempio si dispone del seguente metodo:

public string Deodifica(string codice)


Una possibile dichiarazione di delegato utilizzabile per il metodo proposto potrebbe essere la seguente:

public delegate string DelegatoDecodifica(string codice)


Una volta definito un delegato esistono differenti modi per richiamarlo in maniera asincrona ed attenderne il completamento delle operazioni.


icona articolo

Polling



Il Polling si avvale dell’utilizzo dei metodi BeginInvoke() e EndInvoke() messi a disposizione dal compilatore all’atto della dichiarazione del delegato: mediante il metodo BeginInvoke() avvia in maniera asincrona il metodo delegato, quindi periodicamente ne controlla lo stato svolgengo nel frattempo altre operazioni ed infine mediante il metodo EndInvoke() recupera il risultato restituito dal metodo.

Il metodo BeginInvoke() presenta come argomenti quelli definiti dal delegato più un tipo AsyncCallback ed un object (che nel caso del polling non utilizzeremo e pertanto saranno posti a null) e restituisce un IAsyncResult mediante il quale è possibile controllare lo stato del nuovo thread dal thread principale.

Nel caso del delegato definito in precedenza il metodo BeginInvoke() risulterebbe così definito:

public IAsyncResult BeginInvoke(string codice, AsyncCallback miometodo, object miooggetto)


Una volta invocato il metodo BeginInvoke() dal thread principale è possibile occuparsi di altre operazioni controllando di tanto in tanto lo stato del secondo thread.

Il metodo EndInvoke() la cui firma nel nostro caso è:

public string EndInvoke(IAsyncResult)


pone invece il thread principale in stato d’attesa finchè il secondo thread non completa il suo lavoro e restituisce come risultato il tipo previsto dalla firma del delegato.

public delegate string DelegatoDecodifica(string codice)

...

DelegatoDecodifica dd1 = Decodifica;

IAsyncResult iar1 = dd1.BeginInvoke("codice", null, null);

while(!iar1.IsCompleted)
{
   ...

   Thread.Sleep(500);
}

string risultato = dd1.EndInvoke(iar1);

...



icona articolo

WaitHandle



Un modo alternativo per attendere il completamente delle operazioni del secondo thread consiste nell’utilizzo del WaitHandle associato all’oggetto IAsyncResult (proprietà AsyncWaitHandle) ottenuto tramite l’invocazione del metodo BeginInvoke().

In particolare, il metodo WaitOne() la cui firma è la seguente:

public virtual bool WaitOne(int millisecondsTimeout, bool exitContext)


blocca il thread corrente finchè la classe WaitHandle non riceve un segnale:
  • il primo argomento permette di indicare il numero di millisecondi di attesa per il metodo (se il WaitHandle non riceve alcun segnale entro il tempo indicato il metodo restituisce false, altrimenti restituisce true)
  • il secondo argomento è un booleano ed indica se è possibile uscire dal dominio di sincronizzazione prima dell’attesa (ovvero se una volta ricevuto un segnale il WaitHandle deve comunque attendere la scadenza del timeout per poter restituire il risultato).

Un possibile utilizzo del WaitHandle è il seguente:

...

DelegatoDecodifica dd1 = Decodifica;

IAsyncResult iar1 = dd1.BeginInvoke("codice", null, null);

while(true)
{
   if (ar1.AsyncWaitHandle.WaitOne(500,false))
   {
      break;
   }

}

string risultato = dd1.EndInvoke(iar1);

...


come vediamo il thread principale esce dal ciclo while (all’interno del quale potrebbe comunque svolgere altre operazioni) quando il metodo WaitOne() restituisce true e quindi il break viene eseguito: ovvero quando l’oggetto WaitHandle associato all’IAsyncResult riceve una notifica di completamento del thread ad esso associato.


icona articolo

Asynchronous Callback



La firma del metodo BeginInvoke() che abbiamo esaminato in precedenza prevede come parametri anche un delegato AsyncCallback ed un object che nei due approcci precedenti (Polling e WaitHandle) abbiamo posto a null.

Mediante l’utilizzo del delegato AsyncCallback è possibile assegnare un metodo che verrà richiamato automaticamente dal sistema nel momento in cui il thread avrà completato il suo lavoro: in tal modo non sarà più necessario controllare lo stato del thread in continuazione.

AsyncCallback restituisce void e prevede un argomento di tipo IAsyncResult che di fatto rappresenta lo stato di un’operazione asincrona:

public delegate void AsyncCallback(IAsyncResult ar)


Il parametro object del metodo BeginInvoke() invece prevede la possibilità di passare anche un oggetto che sarà poi accessibile attraverso la proprietà AsyncState dell’interfaccia IAsyncResult nel momento in cui verrà eseguito il metodo delegato.

L’utilizzo delle asynchronous callback consiste quindi nei seguenti passi:
  • dal thread principale viene invocato il metodo BeginInvoke() del delegato che si vuole eseguire in maniera asincrona specificando il metodo che dovrà essere invocato quando il thread sarà completato (tale metodo dovrà avere la stessa firma del delegato AsyncCallback).

    ...

    public void MioMetodo(IAsyncResult ar)
    {
       ...
    }

    ...

    dd1.BeginInvoke("codice",new AsyncCallback(MioMetodo),dd1);


  • una volta avviato il secondo thread, nel thread principale sarà possibile continuare ad eseguire altre operazioni
  • quando il nuovo thread terminerà il suo lavoro verrà invocato il metodo da noi indicato e questo si dovrà occupare tra le altre cose di invocare il metodo EndInvoke() del delegato asincrono utilizzando come argomento l’oggetto IAsyncResult per recuperare il risultato restituito.

    public void MioMetodo(IAsyncResult ar)
    {
       ...

       DelegatoDecodifica dd1 = (DelegatoDecodifica)ar.AsyncState;
       string risultato = dd1.EndInvoke(ar);
    }


E’ opportuno pertanto utilizzare come quarto parametro del metodo BeginInvoke() un oggetto che consenta di accedere all’istanza del delegato asincrono così da poterne invocare il metodo EndInvoke() o in alternativa definire il delegato asincrono in maniera tale che questo risulti visibile al metodo AsyncCallback definito.


icona articolo

La classe Thread



La classe Thread permette di creare e controllare nuovi thread: il suo costruttore accetta come parametro un delegato di tipo ThreadStart o un delegato di tipo ParameterizedThreadStart.

Il delegato ThreadStart viene utilizzato per la costruzione di thread che non hanno bisogno di parametri per eseguire un determinato lavoro, viceversa il ParameterizedThreadStart viene utilizzato nelle occasioni in cui è necessario passare dei parametri al thread.

La costruzione e l’avvio di un thread attraverso il delegato ThreadStart è molto semplice e consta di due passi:
  • la costruzione della classe Thread utilizzando come argomento un metodo che abbia la stessa firma del delegato ThreadStart() che ricordiamo essere:

    public delegate void ThreadStart ()


  • l’avvio del Thread mediante il metodo Start()

    ...

    public void MioMetodo()
    {
       ...
    }

    ...

    Thread t1 = new Thread(new ThreadStart(MioMetodo));
    t1.Start();

    ...


Queste semplici operazioni comportano la definizione di un nuovo flusso di istruzioni (quelle del metodo indicato) parallelo al flusso principale.

E’ possibile anche definire un Thread utilizzando dei metodi anonimi anche se tale operazioni è sconsigliata per motivi di ordine e pulizia del codice:

...

Thread t1 = new Thread(
   delegate()
   {
      ...
   });

t1.Start();

...


La creazione di thread che necessitano di parametri si avvale, come detto in precedenza, dell’utilizzo del delegato ParameterizedThreadStart la cui firma è :

public delegate void ParameterizedThreadStart(Object obj)


E’ opportuno precisare che il passaggio del parametro object non avviene all’atto della creazione del thread ma come argomento del metodo Start()

...

public void MioMetodoParametrizzato(object obj)
{
   MieiDati mieidati = (MieiDati) obj;
}

...

MieiDati parametro = new MieiDati(...);

Thread t1 = new Thread(MioMetodoParametrizzato);
t1.Start(parametro);

...


L’utilizzo di thread parametrici consiste pertanto nei seguenti passi:
  • definizione di un metodo avente la stessa firma del delegato ParameterizedThreadStart
  • costruzione dell’istanza della classe Thread utilizzando tale metodo come delegato
  • creazione dell’oggetto parametro
  • invocazione del metodo Start() utilizzando come argomento l’oggetto parametro


icona articolo

Background, Foreground e priorità



Quando il thread principale genera un nuovo thread questo di default costituisce un foreground thread: questo significa che se il thread principale termina il suo lavoro l’applicazione non si interromperà finchè tutti i therad foreground non saranno anch’essi completati.

Un background thread invece non da alcuna garanzia in questo senso infatti se il thread principale conclude il suo lavoro l’applicazione viene chiusa e di conseguenza il thread di background non ha modo di essere completato.

E’ possibile definire un background thread o un foreground thread all’atto della creazione attraverso la proprietà IsBackground della classe Thread.

t1.IsBackground = false;


La proprietà Priority invece permette di impostare la priorità di un Thread scegliendola fra i valori dell’enumerazione ThreadPriority (Highest, AboveNormal, Normal, BelowNormal, Lowest).


icona articolo

Stato di un Thread



Nel corso della sua esistenza un thread può trovarsi in uno degli stati definiti dall’enumerazione ThreadState:
  • Aborted: il thread è inattivo
  • Running: il thread è stato avviato e non è stato interrotto
  • Stopped: il thread è stato interrotto
  • Suspended: il thread è stato sospeso
  • Unstarted: il thread non è ancora stato avviato (ovverro non è stato richiamato il metodo Start())
  • WaitSleepJoin: il thread è bloccato (ad esempio a seguito dell’invocazione del metodo Sleep())

Esistono poi gli stati:
  • AbortRequested: indica che il metodo Abort() è stato richiamato ma non è ancora stato ricevuto dal Thread
  • StopRequested: indica che il thread riceverà una richiesta di interruzione
  • SuspendRequested: indica che il thread riceverà una richiesta di sospensione

Il passaggio da uno stato all’altro avviene mediante i metodi Start(), Abort(), Sleep(), Interrupt(), Wait(), Join()... che consentono pertanto di avere pieno controllo sullo stato del thread.


icona articolo

Thread Pools



Dal momento che la creazione di un nuovo thread richiede del tempo, il framework .NET permette di creare anticipatamente una lista di Thread (gestiti dalla classe ThreadPool) ed assegnare un compito a ciascuno di essi in un secondo momento.

I Thread disponibili attraverso la classe ThreadPool sono background thread e non è possibile in alcun modo agire su di essi per modificarne la priorità; è bene precisare inoltre che il loro utilizzo è adatto esclusivamente per i processi di piccole dimensioni e di breve durata (per processi duraturi è bene utilizzare la classe Thread).

I metodi fondamentali della classe ThreadPool sono due:
  • GetMaxThreads(): restituisce il numero massimo di thread di lavoro e di I/O disponibili nel pool

    int maxthreadlavoro;
    int maxthreadio;

    ThreadPool.GetMaxThreads(out maxthreadlavoro, out maxthreadio);


  • QueueUserWorkItem(): associa ad un thread del pool il metodo da eseguire; se non è disponibile alcun thread il metodo viene accodato.

    ThreadPool.QueueUserWorkItem(MetodoDaEseguire);


    Il metodo da eseguire deve rispettare la firma del delegato WaitCallback:

    public delegate void WaitCallback ( Object state )



icona articolo

Sincronizzazione



Può capitare che più thread richiedano l’accesso alla medesima risorsa contemporaneamente: per evitare errori in questi casi si ricorre a meccanismi di sincronizzazione.

Un primo approccio in C# consiste nell’utilizzo della parola chiave lock: che letteralmente blocca l’accesso ad una risorsa o meglio ad un tipo riferimento.

lock (oggetto)
{
   ...
}


In tal modo se due thread accedono quasi contemporaneamente alla risorsa: il primo ne ottiene l’accesso e ne blocca l’utilizzo mentre il secondo è costretto ad attendere che il primo thread completi l’esecuzione del codice contenuto all’interno del costrutto lock.

Il costrutto lock non si presta alla sincronizzazione di operazioni atomiche quale ad esempio potrebbe essere l’incremento di un intero: in queti casi si ricorre all’utilizzo della classe Interlocked e dei metodi:
  • Increment()
  • Decrement()
  • Add()
  • Exchange()

Interlocked.Increment(ref variabile);


Il meccanismo offerto dall’impiego della parola chiave lock può essere altresì implementato mediante la classe Monitor ed i metodi Enter() ed Exit() nel seguente modo:

Monitor.Enter(oggetto);
try
{
   ...
}
finally
{
   Monitor.Exit(oggetto);
}


La classe Monitor offre attraverso il metodo TryEnter() la possibilità di indicare la massima quantità di tempo (in millisecondi) che un thread deve attendere per ottenere accesso alla risorsa bloccata: se al termine del tempo indicato il thread non si ottiene l’accesso alla risorsa il metodo restituisce false ed il thread prosegue l’esecuzione del codice che segue l’if:

Monitor.TryEnter(oggetto, 500);
try
{
   ...
}
finally
{
   Monitor.Exit(oggetto);
}


Per la sincronizzazione fra più processi è possibile utilizzare le classi Mutex e Semaphore.

Il costruttore della classe Mutex:

public Mutex(bool initiallyOwned, string name, out bool createdNew)


prevede:
  • un booleano che permette di indicare se il thread chiamante deve avere la proprietà iniziale della classe Mutex
  • una stringa che indica il nome del mutex
  • un valore booleano che una volta invocato il metodo indica se il mutex era già esistente o meno

bool nuovo;
Mutex mutex = new Mutex(false, "NomeMutex", out createdNew);


Per utilizzare un mutex già esistente è possibile utilizzare il metodo Mutex.OpenExisting().

Dal momento che il Mutex deriva dalla classe WaitHandle, una volta creata l’istanza è possibile utilizzare i metodi:
  • WaitOne(): attende l’occorrenza di un segnale; opzionalmente è possibile indicare il tempo massimo di attesa.
  • WaitAll(): attende finchè tutti gli oggetti WaitHandle dell’array passato come argomento ricevono un segnale
  • WaitAny(): attende finchè almeno uno degli oggetti WaitHandle dell’array passato come argomento riceve un segnale.

Per rilasciare il Mutex si utilizza invece il metodo ReleaseMutex().

if (mutex.WaitOne())
{
   try
   {
      ...
   }
   finally
   {
      mutex.ReleaseMutex();
   }
}
else
{
   ...
}


La classe Semaphore è molto simile alla classe Mutex ma può essere utilizzata da più thread contemporaneamente.

Il costruttore della classe Semaphore:

public Semaphore(int initialCount, int maximumCount)


accetta come argomenti:
  • il numero iniziale delle richieste per il semaforo che possono essere concesse contemporaneamente
  • il numero massimo delle richieste per il semaforo che possono essere concesse contemporaneamente

e viene utilizzato con thread parametrizzati (ParameterizedThreadStart) come argomento del metodo Start() del thread:

...

Semaphore semaforo = new Semaphore(5, 5);
Thread t1 = new Thread(MioMetodo);
t1.Start(semaforo);

...


Ancora una volta è possibile utilizzare i metodi WaitOne(), WaitAll() e WaitAny() ma l’accesso alla risorsa non sarà più limitato ad un solo thread per volta ma ad al numero di thread indicati all’atto della costruzione del semaforo contemporaneamente.

Per rilasciare il semaforo si utilizza il metodo Release().

...

public void MioMetodo(object o)
{
   Semaphore s = (Semaphore) o;
   
   if (o.WaitOne(500,false))
   {
      try
      {
         ...
      }
      finally
      {
         s.Release();
      }
   }
}

...