8. I thread di esecuzione
8.1. Introduzione
Quando si avvia un'applicazione, questa viene eseguita in un flusso di esecuzione denominato thread. La classe .NET che modella un thread è la classe System.Threading.Thread e presenta la seguente definizione:

Utilizzeremo solo alcune delle proprietà e dei metodi di questa classe:
restituisce il thread attualmente in esecuzione | |
nome del thread | |
indica se il thread è attivo (true) o meno (false) | |
avvia l'esecuzione di un thread | |
interrompe definitivamente l'esecuzione di un thread | |
sospende l'esecuzione di un thread per n millisecondi | |
sospende temporaneamente l'esecuzione di un thread | |
riprende l'esecuzione di un thread sospeso | |
operazione bloccante: attende il termine del thread per passare all'istruzione successiva |
Diamo un'occhiata a una prima applicazione che evidenzia l'esistenza di un thread di esecuzione principale, quello in cui viene eseguita la funzione Main di una classe:
' utilizzo dei thread
Imports System
Imports System.Threading
Public Module thread1
Public Sub Main()
' inizializzazione del thread corrente
Dim main As Thread = Thread.CurrentThread
' visualizzazione
Console.Out.WriteLine(("Thread courant : " + main.Name))
' si modifica il nome
main.Name = "main"
' verifica
Console.Out.WriteLine(("Thread courant : " + main.Name))
' ciclo infinito
While True
' visualizzazione
Console.Out.WriteLine((main.Name + " : " + DateTime.Now.ToString("hh:mm:ss")))
' arresto temporaneo
Thread.Sleep(1000)
End While
End Sub
End Module
Risultati sullo schermo:
dos>thread1
Thread courant :
Thread courant : main
main : 06:13:55
main : 06:13:56
main : 06:13:57
main : 06:13:58
main : 06:13:59
L'esempio precedente illustra i seguenti punti:
- la funzione Main viene eseguita correttamente in un thread
- è possibile accedere alle caratteristiche di questo thread tramite Thread.CurrentThread
- il ruolo del metodo Sleep. In questo caso, il thread che esegue Main entra regolarmente in stato di sospensione per 1 secondo tra una visualizzazione e l'altra.
8.2. Creazione di thread di esecuzione
È possibile avere applicazioni in cui parti di codice vengono eseguite in modo "simultaneo" in diversi thread di esecuzione. Quando si dice che i thread vengono eseguiti simultaneamente, spesso si commette un abuso di linguaggio. Se la macchina dispone di un solo processore, come spesso accade ancora oggi, i thread condividono tale processore: ne dispongono, a turno, per un breve istante (alcuni millisecondi). È questo che crea l’illusione del parallelismo di esecuzione. Il tempo assegnato a un thread dipende da vari fattori, tra cui la sua priorità, che ha un valore predefinito ma può essere impostata anche tramite programmazione. Quando un thread ha a disposizione il processore, lo utilizza normalmente per tutto il tempo che gli è stato assegnato. Tuttavia, può liberarlo prima del termine:
- mettendosi in attesa di un evento (wait, join, suspend)
- mettendosi in stato di sospensione per un periodo di tempo determinato (sleep)
- Un thread T viene inizialmente creato dal suo costruttore
ThreadStart è di tipo delegate e definisce il prototipo di una funzione senza parametri:
Una struttura classica è la seguente:
La funzione run passata come parametro verrà eseguita all'avvio del thread.
- L'esecuzione del thread T viene avviata da T.Start(): la funzione [run] passata al costruttore di T verrà quindi eseguita dal thread T. Il programma che esegue l'istruzione T.start() non attende il completamento dell'attività T: passa immediatamente all'istruzione successiva. Si hanno quindi due attività che vengono eseguite in parallelo. Spesso devono potersi comunicare tra loro per sapere a che punto è il lavoro comune da svolgere. Questo è il problema della sincronizzazione dei thread.
- Una volta avviato, il thread thread viene eseguito in modo autonomo. Si interromperà quando la funzione start che sta eseguendo avrà terminato il proprio lavoro.
- È possibile inviare determinati segnali al thread T:
- T.Suspend() gli dice di arrestarsi momentaneamente
- T.Resume() le indica di riprendere il lavoro
- T.Abort() gli dice di arrestarsi definitivamente
- È anche possibile attendere la fine della sua esecuzione tramite T.join(). Si tratta di un'istruzione bloccante: il programma che la esegue rimane bloccato finché il processo T non ha terminato il proprio lavoro. È un metodo di sincronizzazione.
Esaminiamo il seguente programma:
' opzioni
Option Strict On
Option Explicit On
' spazi dei nomi
Imports System
Imports System.Threading
Module thread2
Public Sub Main()
' inizializzazione del thread corrente
Dim main As Thread = Thread.CurrentThread
' assegnazione di un nome al thread
main.Name = "main"
' creazione di thread di esecuzione
Dim tâches(4) As Thread
Dim i As Integer
For i = 0 To tâches.Length - 1
' si crea il thread i
tâches(i) = New Thread(New ThreadStart(AddressOf affiche))
' si imposta il nome del thread
tâches(i).Name = "tache_" & i
' si avvia l'esecuzione del thread i
tâches(i).Start()
Next i
' fine della funzione
Console.Out.WriteLine(("fin du thread " + main.Name))
End Sub
Public Sub affiche()
' visualizzazione dell'inizio dell'esecuzione
Console.Out.WriteLine(("Début d'exécution de la méthode affiche dans le Thread " + Thread.CurrentThread.Name + " : " + DateTime.Now.ToString("hh:mm:ss")))
' sospensione per 1 s
Thread.Sleep(1000)
' visualizzazione della fine dell'esecuzione
Console.Out.WriteLine(("Fin d'exécution de la méthode affiche dans le Thread " + Thread.CurrentThread.Name + " : " + DateTime.Now.ToString("hh:mm:ss")))
End Sub
End Module
Il thread principale, quello che esegue la funzione Main, crea altri 5 thread incaricati di eseguire il metodo statico affiche. I risultati sono i seguenti:
dos>thread2
fin du thread main
Début d'exécution de la méthode affiche dans le Thread tache_0 : 05:27:53
Début d'exécution de la méthode affiche dans le Thread tache_1 : 05:27:53
Début d'exécution de la méthode affiche dans le Thread tache_2 : 05:27:53
Début d'exécution de la méthode affiche dans le Thread tache_3 : 05:27:53
Début d'exécution de la méthode affiche dans le Thread tache_4 : 05:27:53
Fin d'exécution de la méthode affiche dans le Thread tache_0 : 05:27:54
Fin d'exécution de la méthode affiche dans le Thread tache_1 : 05:27:54
Fin d'exécution de la méthode affiche dans le Thread tache_2 : 05:27:54
Fin d'exécution de la méthode affiche dans le Thread tache_3 : 05:27:54
Fin d'exécution de la méthode affiche dans le Thread tache_4 : 05:27:54
Questi risultati sono molto istruttivi:
- si nota innanzitutto che l'avvio dell'esecuzione di un thread non è bloccante. Il metodo Main ha avviato l'esecuzione di 5 thread in parallelo e ha terminato la propria esecuzione prima di loro. L'operazione
avvia l'esecuzione del thread tâches[i], ma una volta fatto ciò, l'esecuzione prosegue immediatamente con l'istruzione successiva senza attendere il completamento dell'esecuzione del thread.
- Tutti i thread creati devono eseguire il metodo affiche. L’ordine di esecuzione è imprevedibile. Anche se nell’esempio l’ordine di esecuzione sembra seguire l’ordine delle richieste di esecuzione, non è possibile trarne conclusioni generali. Il sistema operativo dispone in questo caso di 6 thread e un processore. Distribuirà il processore a questi 6 thread secondo regole proprie.
- Nei risultati si osserva una conseguenza del metodo Sleep. Nell’esempio, è il thread 0 che esegue per primo il metodo affiche. Viene visualizzato il messaggio di inizio esecuzione, dopodiché il thread esegue il metodo Sleep, che lo sospende per 1 secondo. A quel punto perde il processore, che diventa così disponibile per un altro thread. L’esempio mostra che sarà il thread 1 a ottenerlo. Il thread 1 seguirà lo stesso percorso, così come gli altri thread. Al termine del secondo di sospensione del thread 0, la sua esecuzione potrà riprendere. Il sistema gli assegna il processore e il thread 0 può completare l’esecuzione del metodo affiche.
Modifichiamo il nostro programma per terminare il metodo Main con le istruzioni:
L’esecuzione del nuovo programma produce:
I thread creati dalla funzione Main non vengono eseguiti. Si tratta dell'istruzione
a causare questo problema: essa elimina tutti i thread dell'applicazione e non solo il thread Main. La soluzione a questo problema consiste nel far sì che il metodo Main attenda il completamento dell’esecuzione dei thread che ha creato prima di terminare a sua volta. Ciò può essere ottenuto utilizzando il metodo Join della classe Thread:
' si attende la fine dell'esecuzione di tutti i thread
For i = 0 To tâches.Length - 1
' in attesa della fine dell'esecuzione del thread i
tâches(i).Join()
Next i 'for
' fine del ciclo
Console.Out.WriteLine(("fin du thread " + main.Name))
Environment.Exit(0)
Si ottengono quindi i seguenti risultati:
Début d'exécution de la méthode affiche dans le Thread tache_1 : 05:34:48
Début d'exécution de la méthode affiche dans le Thread tache_2 : 05:34:48
Début d'exécution de la méthode affiche dans le Thread tache_3 : 05:34:48
Début d'exécution de la méthode affiche dans le Thread tache_4 : 05:34:48
Début d'exécution de la méthode affiche dans le Thread tache_0 : 05:34:48
Fin d'exécution de la méthode affiche dans le Thread tache_2 : 05:34:50
Fin d'exécution de la méthode affiche dans le Thread tache_1 : 05:34:50
Fin d'exécution de la méthode affiche dans le Thread tache_3 : 05:34:50
Fin d'exécution de la méthode affiche dans le Thread tache_0 : 05:34:50
Fin d'exécution de la méthode affiche dans le Thread tache_4 : 05:34:50
fin du thread main
8.3. Importanza dei thread
Ora che abbiamo evidenziato l'esistenza di un thread predefinito, quello che esegue il metodo Main, e che sappiamo come crearne altri, soffermiamoci sull'utilità dei thread per noi e sul motivo per cui li presentiamo qui. Esiste una tipologia di applicazioni che si presta particolarmente bene all’uso dei thread: si tratta delle applicazioni client-server su Internet. In un’applicazione di questo tipo, un server situato su una macchina S1 risponde alle richieste dei client situati su macchine remote C1, C2, ..., Cn.
![]() |
Utilizziamo quotidianamente applicazioni Internet che corrispondono a questo schema: servizi Web, posta elettronica, consultazione di forum, trasferimento di file... Nello schema sopra riportato, il server S1 deve servire i client Ci in modo simultaneo. Se prendiamo l’esempio di un server FTP (File Transfer Protocol) che fornisce file ai propri clienti, sappiamo che un trasferimento di file può talvolta richiedere diverse ore. È ovviamente fuori discussione che un singolo cliente possa monopolizzare il server per un periodo così lungo. Di solito, il server crea tanti thread di esecuzione quanti sono i clienti. Ogni thread ha quindi il compito di occuparsi di un cliente specifico. Poiché il processore viene condiviso ciclicamente tra tutti i thread attivi della macchina, il server dedica un po’ di tempo a ciascun cliente, garantendo così la simultaneità del servizio.
![]() |
8.4. Accesso alle risorse condivise
Nell’esempio client-server sopra citato, ogni thread serve un cliente in modo sostanzialmente indipendente. Tuttavia, i thread possono essere chiamati a cooperare per fornire il servizio richiesto al proprio cliente, in particolare per l’accesso alle risorse condivise. Lo schema sopra riportato ricorda gli sportelli di un grande ente amministrativo, ad esempio un ufficio postale, dove ad ogni sportello un impiegato serve un cliente. Supponiamo che di tanto in tanto questi impiegati debbano fare delle fotocopie dei documenti portati dai propri clienti e che ci sia una sola fotocopiatrice. Due impiegati non possono utilizzare la fotocopiatrice contemporaneamente. Se l’impiegato i trova la fotocopiatrice occupata dall’impiegato j, dovrà attendere. Questa situazione viene definita «accesso a una risorsa condivisa» e, in informatica, è piuttosto complessa da gestire. Prendiamo il seguente esempio:
- un’applicazione genererà n thread, dove n è passato come parametro
- la risorsa condivisa è un contatore che dovrà essere incrementato da ciascun thread generato
- Al termine dell'applicazione, viene visualizzato il valore del contatore. Dovremmo quindi trovare n.
Il programma è il seguente:
' opzioni
Option Explicit On
Option Strict On
' utilizzo dei thread
Imports System
Imports System.Threading
Public Class thread3
' variabili di classe
Private Shared cptrThreads As Integer = 0
Public Overloads Shared Sub Main(ByVal args() As [String])
' istruzioni per l'uso
Const syntaxe As String = "pg nbThreads"
Const nbMaxThreads As Integer = 100
' verifica del numero di argomenti
If args.Length <> 1 Then
' errore
Console.Error.WriteLine(syntaxe)
' interruzione
Environment.Exit(1)
End If
' verifica della qualità dell'argomento
Dim nbThreads As Integer = 0
Try
nbThreads = Integer.Parse(args(0))
If nbThreads < 1 Or nbThreads > nbMaxThreads Then
Throw New Exception
End If
Catch
' errore
Console.Error.WriteLine("Nombre de threads incorrect (entre 1 et " & nbMaxThreads & ")")
' fine
Environment.Exit(2)
End Try
' creazione e generazione dei thread
Dim threads(nbThreads - 1) As Thread
Dim i As Integer
For i = 0 To nbThreads - 1
' creazione
threads(i) = New Thread(New ThreadStart(AddressOf incrémente))
' denominazione
threads(i).Name = "tache_" & i
' avvio
threads(i).Start()
Next i
' attesa del completamento dei thread
For i = 0 To nbThreads - 1
threads(i).Join()
Next i ' affichage compteur
Console.Out.WriteLine(("Nombre de threads générés : " & cptrThreads))
End Sub
Public Shared Sub incrémente()
' aumenta il contatore dei thread
' lettura contatore
Dim valeur As Integer = cptrThreads
' monitoraggio
Console.Out.WriteLine(("A " + DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a lu la valeur du compteur : " & cptrThreads))
' attesa
Thread.Sleep(1000)
' incremento del contatore
cptrThreads = valeur + 1
' monitoraggio
Console.Out.WriteLine(("A " & DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a écrit la valeur du compteur : " & cptrThreads))
End Sub
End Class
Non ci soffermeremo sulla parte relativa alla generazione dei thread, già studiata in precedenza. Concentriamoci piuttosto sul metodo incrémente, utilizzato da ciascun thread per incrementare il contatore statico cptrThreads.
- il contatore viene letto
- il thread si ferma per 1 s. Perde quindi il controllo del processore
- il contatore viene incrementato
La fase 2 serve solo a costringere il thread a cedere il processore. Questo verrà assegnato a un altro thread. In pratica, nulla garantisce che un thread non venga interrotto tra il momento in cui legge il contatore e quello in cui lo incrementa. Esiste il rischio di perdere il processore tra il momento in cui si legge il valore del contatore e quello in cui si scrive il suo valore incrementato di 1. Infatti, l’operazione di incremento sarà oggetto di diverse istruzioni elementari a livello del processore che possono essere interrotte. La fase 2 di sospensione di un secondo serve quindi solo a sistematizzare questo rischio. I risultati ottenuti sono i seguenti:
dos>thread3 5
A 05:44:34, le thread tache_0 a lu la valeur du compteur : 0
A 05:44:34, le thread tache_1 a lu la valeur du compteur : 0
A 05:44:34, le thread tache_2 a lu la valeur du compteur : 0
A 05:44:34, le thread tache_3 a lu la valeur du compteur : 0
A 05:44:34, le thread tache_4 a lu la valeur du compteur : 0
A 05:44:35, le thread tache_0 a écrit la valeur du compteur : 1
A 05:44:35, le thread tache_1 a écrit la valeur du compteur : 1
A 05:44:35, le thread tache_2 a écrit la valeur du compteur : 1
A 05:44:35, le thread tache_3 a écrit la valeur du compteur : 1
A 05:44:35, le thread tache_4 a écrit la valeur du compteur : 1
Nombre de threads générés : 1
Leggendo questi risultati, si capisce chiaramente cosa sta succedendo:
- un primo thread legge il contatore. Trova 0.
- Si ferma per 1 s, quindi cede il processore
- un secondo thread prende quindi il controllo del processore e legge a sua volta il valore del contatore. È ancora a 0 poiché il thread precedente non lo ha ancora incrementato. Anche lui si ferma per 1 s.
- In 1 s, i 5 thread hanno il tempo di passare tutti e di leggere tutti il valore 0.
- Quando si riattiveranno uno dopo l’altro, incrementeranno il valore 0 che hanno letto e scriveranno il valore 1 nel contatore, come conferma il programma principale (Main).
Da dove deriva il problema? Il secondo thread ha letto un valore errato poiché il primo era stato interrotto prima di aver completato il proprio compito, che consisteva nell’aggiornare il contatore nella finestra. Questo ci porta al concetto di risorsa critica e di sezione critica di un programma:
- una risorsa critica è una risorsa che può essere detenuta da un solo thread alla volta. In questo caso la risorsa critica è il contatore.
- Una sezione critica di un programma è una sequenza di istruzioni nel flusso di esecuzione di un thread durante la quale esso accede a una risorsa critica. È necessario garantire che, durante questa sezione critica, il thread sia l’unico ad avere accesso alla risorsa.
8.5. Accesso esclusivo a una risorsa condivisa
Nel nostro esempio, la sezione critica è il codice compreso tra la lettura del contatore e la scrittura del suo nuovo valore:
' lettura contatore
Dim valeur As Integer = cptrThreads
' in attesa
Thread.Sleep(1000)
' incremento contatore
cptrThreads = valeur + 1
Per eseguire questo codice, è necessario garantire che un thread sia l'unico in esecuzione. Può essere interrotto, ma durante tale interruzione nessun altro thread deve poter eseguire lo stesso codice. La piattaforma .NET offre diversi strumenti per garantire l'accesso unitario alle sezioni critiche del codice. Utilizzeremo la classe Mutex:

In questa sede utilizzeremo solo i seguenti costruttori e metodi:
crea un oggetto di sincronizzazione M | |
Il thread T1 che esegue l'operazione M.WaitOne() richiede la proprietà dell'oggetto di sincronizzazione M. Se il mutex M non è detenuto da alcun thread (come avviene inizialmente), viene “assegnato” al thread T1 che lo ha richiesto. Se poco dopo un thread T2 esegue la stessa operazione, verrà bloccato. Infatti, un mutex può appartenere a un solo thread. Verrà sbloccato quando il thread T1 rilascerà il mutex M che detiene. Diversi thread possono quindi rimanere bloccati in attesa del mutex M. | |
Il thread T1 che esegue l'operazione M.ReleaseMutex() rinuncia al possesso del mutex M.Lorsque; il thread T1 perderà il processore, il sistema potrà assegnarlo a uno dei thread in attesa del mutex M. Solo uno lo otterrà a sua volta, mentre gli altri in attesa di M rimarranno bloccati |
Un mutex M gestisce l'accesso a una risorsa condivisa R. Un thread richiede la risorsa R tramite M.WaitOne() e la restituisce tramite M.ReleaseMutex(). Una sezione critica di codice che deve essere eseguita da un solo thread alla volta è una risorsa condivisa. La sincronizzazione dell’esecuzione della sezione critica può avvenire in questo modo:
dove M è un oggetto Mutex. Ovviamente non bisogna mai dimenticare di liberare un Mutex che non serve più, in modo che un altro thread possa entrare nella sezione critica; altrimenti, i thread in attesa di un mutex mai liberato non avranno mai accesso al processore. Inoltre, occorre evitare la situazione di interblocco (deadlock) in cui due thread si aspettano a vicenda. Consideriamo le seguenti azioni che si susseguono nel tempo:
- un thread T1 acquisisce il controllo di un mutex M1 per accedere a una risorsa condivisa R1
- un thread T2 acquisisce il controllo di un mutex M2 per accedere a una risorsa condivisa R2
- il thread T1 richiede il mutex M2. È bloccato.
- Il thread T2 richiede il mutex M1. È bloccato.
In questo caso, i thread T1 e T2 si aspettano a vicenda. Questo caso si verifica quando i thread necessitano di due risorse condivise: la risorsa R1 controllata dal mutex M1 e la risorsa R2 controllata dal mutex M2. Una possibile soluzione consiste nel richiedere entrambe le risorse contemporaneamente tramite un unico mutex M. Tuttavia, ciò non è sempre possibile se, ad esempio, comporta un lungo blocco di una risorsa costosa. Un’altra soluzione consiste nel far sì che un thread in possesso di M1, non potendo ottenere M2, rilasci M1 per evitare l’interblocco. Se mettiamo in pratica quanto appena visto sull’esempio precedente, la nostra applicazione diventa la seguente:
' opzioni
Option Explicit On
Option Strict On
' utilizzo dei thread
Imports System
Imports System.Threading
Public Class thread4
' variabili di classe
Private Shared cptrThreads As Integer = 0 ' compteur de threads
Private Shared autorisation As Mutex
Public Overloads Shared Sub Main(ByVal args() As [String])
' istruzioni per l'uso
Const syntaxe As String = "pg nbThreads"
Const nbMaxThreads As Integer = 100
' verifica del numero di argomenti
If args.Length <> 1 Then
' errore
Console.Error.WriteLine(syntaxe)
' interruzione
Environment.Exit(1)
End If
' verifica della qualità dell'argomento
Dim nbThreads As Integer = 0
Try
nbThreads = Integer.Parse(args(0))
If nbThreads < 1 Or nbThreads > nbMaxThreads Then
Throw New Exception
End If
Catch
End Try
' inizializzazione dell'autorizzazione di accesso a una sezione critica
autorisation = New Mutex
' creazione e generazione dei thread
Dim threads(nbThreads) As Thread
Dim i As Integer
For i = 0 To nbThreads - 1
' creazione
threads(i) = New Thread(New ThreadStart(AddressOf incrémente))
' denominazione
threads(i).Name = "tache_" & i
' avvio
threads(i).Start()
Next i
' attesa del completamento dei thread
For i = 0 To nbThreads - 1
threads(i).Join()
Next i
' visualizzazione del contatore
Console.Out.WriteLine(("Nombre de threads générés : " & cptrThreads))
End Sub
Public Shared Sub incrémente()
' aumenta il contatore dei thread
' si richiede l'autorizzazione per entrare nella sezione critica
autorisation.WaitOne()
' lettura del contatore
Dim valeur As Integer = cptrThreads
' monitoraggio
Console.Out.WriteLine(("A " & DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a lu la valeur du compteur : " & cptrThreads))
' attesa
Thread.Sleep(1000)
' incremento del contatore
cptrThreads = valeur + 1
' monitoraggio
Console.Out.WriteLine(("A " & DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a écrit la valeur du compteur : " & cptrThreads))
' si concede l'autorizzazione di accesso
autorisation.ReleaseMutex()
End Sub
End Class
I risultati ottenuti sono conformi a quanto previsto:
dos>thread4 5
A 05:51:10, le thread tache_0 a lu la valeur du compteur : 0
A 05:51:11, le thread tache_0 a écrit la valeur du compteur : 1
A 05:51:11, le thread tache_1 a lu la valeur du compteur : 1
A 05:51:12, le thread tache_1 a écrit la valeur du compteur : 2
A 05:51:12, le thread tache_2 a lu la valeur du compteur : 2
A 05:51:13, le thread tache_2 a écrit la valeur du compteur : 3
A 05:51:13, le thread tache_3 a lu la valeur du compteur : 3
A 05:51:14, le thread tache_3 a écrit la valeur du compteur : 4
A 05:51:14, le thread tache_4 a lu la valeur du compteur : 4
A 05:51:15, le thread tache_4 a écrit la valeur du compteur : 5
Nombre de threads générés : 5
8.6. Sincronizzazione basata sugli eventi
Consideriamo la seguente situazione, talvolta denominata situazione produttore-consumatore.
- Abbiamo un array in cui alcuni processi inseriscono dati (i produttori) e altri li leggono (i consumatori).
- I produttori sono equivalenti tra loro ma esclusivi: un solo produttore alla volta può inserire i propri dati nell’array.
- I consumatori sono uguali tra loro ma si escludono a vicenda: un solo lettore alla volta può leggere i dati inseriti nell’array.
- Un consumatore può leggere i dati dell’array solo quando un produttore li ha inseriti al suo interno e un produttore può inserire nuovi dati nell’array solo quando quelli presenti sono stati consumati.
In questa esposizione si possono distinguere due risorse condivise:
- la tabella in scrittura
- l'array in lettura
L'accesso a queste due risorse condivise può essere controllato tramite Mutex, come visto in precedenza, uno per ciascuna risorsa. Una volta che un consumatore ha ottenuto l'accesso in lettura alla tabella, deve verificare che vi siano effettivamente dei dati al suo interno. Si utilizzerà un evento per avvisarlo. Allo stesso modo, un produttore che ha ottenuto l'accesso in scrittura alla tabella dovrà attendere che un consumatore l'abbia svuotata. Anche in questo caso si utilizzerà un evento.
Gli eventi utilizzati faranno parte della classe AutoResetEvent:

Questo tipo di evento è analogo a un valore booleano, ma evita le attese attive o semi-attive. Pertanto, se il diritto di scrittura è controllato da un valore booleano peutEcrire, un produttore, prima di scrivere, eseguirà un codice del tipo:
oppure
Nel primo metodo, il thread occupa inutilmente il processore. Nel secondo, verifica lo stato della variabile booleana peutEcrire ogni 100 ms. La classe AutoResetEvent consente di migliorare ulteriormente la situazione: il thread chiederà di essere riattivato quando si verificherà l'evento che sta aspettando:
AutoEvent peutEcrire=new AutoResetEvent(false) ' peutEcrire=false;
....
peutEcrire.WaitOne() ' le thread attend que l'évt peutEcrire passe à vrai
L'operazione
inizializza il valore booleano peutEcrire a false. L'operazione
eseguita da un thread fa sì che quest'ultimo proceda se la variabile booleana peutEcrire è vera, altrimenti rimane bloccato finché non diventa vera. Un altro thread lo imposterà a vero tramite l'operazione peutEcrire.Set() o a falso tramite l'operazione peutEcrire.Reset().
Il programma produttore-consumatore è il seguente:
' utilizzo di thread di lettura e scrittura
' illustra l'utilizzo simultaneo di risorse condivise e di sincronizzazione
' opzioni
Option Explicit On
Option Strict On
' utilizzo dei thread
Imports System
Imports System.Threading
Public Class lececr
' variabili di classe
Private Shared data(5) As Integer ' ressource partagée entre threads lecteur et threads écrivain
Private Shared lecteur As Mutex ' variable de synchronisation pour lire le tableau
Private Shared écrivain As Mutex ' variable de synchronisation pour écrire dans le tableau
Private Shared objRandom As New Random(DateTime.Now.Second) ' un générateur de nombres aléatoires
Private Shared peutLire As AutoResetEvent ' signale qu'on peut lire le contenu de data
Private Shared peutEcrire As AutoResetEvent
Public Shared Sub Main(ByVal args() As [String])
' il numero di thread da generare
Const nbThreads As Integer = 3
' inizializzazione dei flag
peutLire = New AutoResetEvent(False) ' on ne peut pas encore lire
peutEcrire = New AutoResetEvent(True) ' on peut déjà écrire
' inizializzazione delle variabili di sincronizzazione
lecteur = New Mutex ' synchronise les lecteurs
écrivain = New Mutex ' synchronise les écrivains
' creazione dei thread di lettura
Dim lecteurs(nbThreads) As Thread
Dim i As Integer
For i = 0 To nbThreads - 1
' creazione
lecteurs(i) = New Thread(New ThreadStart(AddressOf lire))
lecteurs(i).Name = "lecteur_" & i
' avvio
lecteurs(i).Start()
Next i
' creazione dei thread di scrittura
Dim écrivains(nbThreads) As Thread
For i = 0 To nbThreads - 1
' creazione
écrivains(i) = New Thread(New ThreadStart(AddressOf écrire))
écrivains(i).Name = "écrivain_" & i
' avvio
écrivains(i).Start()
Next i
'fine mano
Console.Out.WriteLine("fin de Main...")
End Sub
' lettura del contenuto della tabella
Public Shared Sub lire()
' sezione critica
lecteur.WaitOne() ' un seul lecteur peut passer
peutLire.WaitOne() ' on doit pouvoir lire
' lettura tabella
Dim i As Integer
For i = 0 To data.Length - 1
'attesa 1 s
Thread.Sleep(1000)
' visualizzazione
Console.Out.WriteLine((DateTime.Now.ToString("hh:mm:ss") & " : Le lecteur " & Thread.CurrentThread.Name & " a lu le nombre " & data(i)))
Next i
' non è più possibile leggere
peutLire.Reset()
' è possibile scrivere
peutEcrire.Set()
' fine della sezione critica
lecteur.ReleaseMutex()
End Sub
' scrivere nella tabella
Public Shared Sub écrire()
' sezione critica
' può passare un solo autore
écrivain.WaitOne()
' bisogna attendere l'autorizzazione alla scrittura
peutEcrire.WaitOne()
' scrittura nella tabella
Dim i As Integer
For i = 0 To data.Length - 1
'attesa di 1 s
Thread.Sleep(1000)
' visualizzazione
data(i) = objRandom.Next(0, 1000)
Console.Out.WriteLine((DateTime.Now.ToString("hh:mm:ss") & " : L'écrivain " & Thread.CurrentThread.Name & " a écrit le nombre " & data(i)))
Next i
' non è più possibile scrivere
peutEcrire.Reset()
' è possibile leggere
peutLire.Set()
'fine sezione critica
écrivain.ReleaseMutex()
End Sub
End Class
L’esecuzione fornisce i seguenti risultati:
dos>lececr
fin de Main...
05:56:56 : L'écrivain écrivain_0 a écrit le nombre 459
05:56:57 : L'écrivain écrivain_0 a écrit le nombre 955
05:56:58 : L'écrivain écrivain_0 a écrit le nombre 212
05:56:59 : L'écrivain écrivain_0 a écrit le nombre 297
05:57:00 : L'écrivain écrivain_0 a écrit le nombre 37
05:57:01 : L'écrivain écrivain_0 a écrit le nombre 623
05:57:02 : Le lecteur lecteur_0 a lu le nombre 459
05:57:03 : Le lecteur lecteur_0 a lu le nombre 955
05:57:04 : Le lecteur lecteur_0 a lu le nombre 212
05:57:05 : Le lecteur lecteur_0 a lu le nombre 297
05:57:06 : Le lecteur lecteur_0 a lu le nombre 37
05:57:07 : Le lecteur lecteur_0 a lu le nombre 623
05:57:08 : L'écrivain écrivain_1 a écrit le nombre 549
05:57:09 : L'écrivain écrivain_1 a écrit le nombre 34
05:57:10 : L'écrivain écrivain_1 a écrit le nombre 781
05:57:11 : L'écrivain écrivain_1 a écrit le nombre 555
05:57:12 : L'écrivain écrivain_1 a écrit le nombre 812
05:57:13 : L'écrivain écrivain_1 a écrit le nombre 406
05:57:14 : Le lecteur lecteur_1 a lu le nombre 549
05:57:15 : Le lecteur lecteur_1 a lu le nombre 34
05:57:16 : Le lecteur lecteur_1 a lu le nombre 781
05:57:17 : Le lecteur lecteur_1 a lu le nombre 555
05:57:18 : Le lecteur lecteur_1 a lu le nombre 812
05:57:19 : Le lecteur lecteur_1 a lu le nombre 406
05:57:20 : L'écrivain écrivain_2 a écrit le nombre 442
05:57:21 : L'écrivain écrivain_2 a écrit le nombre 83
^C
Si possono notare i seguenti punti:
- c'è effettivamente un solo lettore alla volta, sebbene questo perda il processore nella sezione critica lire
- c'è effettivamente un solo scrittore alla volta, sebbene questo perda il processore nella sezione critica écrire
- un lettore legge solo quando c'è qualcosa da leggere nell'array
- un scrivitore scrive solo quando l’array è stato letto interamente

