Skip to content

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:

Image

Utilizzeremo solo alcune delle proprietà e dei metodi di questa classe:

CurrentThread - propriété statique
restituisce il thread attualmente in esecuzione
Name - propriété d'objet
nome del thread
isAlive - propriété d'objet
indica se il thread è attivo (true) o meno (false)
Start - méthode d'objet
avvia l'esecuzione di un thread
Abort - méthode d'objet
interrompe definitivamente l'esecuzione di un thread
Sleep(n) - méthode statique
sospende l'esecuzione di un thread per n millisecondi
Suspend() - méthode d'objet
sospende temporaneamente l'esecuzione di un thread
Resume() - méthode d'objet
riprende l'esecuzione di un thread sospeso
Join() - méthode d'objet
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)
  1. Un thread T viene inizialmente creato dal suo costruttore
Public Sub New(ByVal start As ThreadStart)

ThreadStart è di tipo delegate e definisce il prototipo di una funzione senza parametri:

Public Delegate Sub ThreadStart()

Una struttura classica è la seguente:

dim T as Thread=new Thread(new ThreadStart(run));

La funzione run passata come parametro verrà eseguita all'avvio del thread.

  1. 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.
  1. 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.
  1. È possibile inviare determinati segnali al thread T:
    1. T.Suspend() gli dice di arrestarsi momentaneamente
    2. T.Resume() le indica di riprendere il lavoro
    3. T.Abort() gli dice di arrestarsi definitivamente
  1. È 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
             ' si avvia l'esecuzione del thread i
            tâches(i).Start()

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:

         ' fine del thread
        Console.Out.WriteLine(("fin du thread " + main.Name))
        Environment.Exit(0)

L’esecuzione del nuovo programma produce:

fin du thread main

I thread creati dalla funzione Main non vengono eseguiti. Si tratta dell'istruzione

        Environment.Exit(0)

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.

  1. il contatore viene letto
  2. il thread si ferma per 1 s. Perde quindi il controllo del processore
  3. 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:

Image

In questa sede utilizzeremo solo i seguenti costruttori e metodi:

public Mutex()
crea un oggetto di sincronizzazione M
public bool WaitOne()
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.
public void
ReleaseMutex()
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:

M.WaitOne()
' solo questo thread può entrare qui
' sezione critica
....
M.ReleaseMutex()

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.

  1. Abbiamo un array in cui alcuni processi inseriscono dati (i produttori) e altri li leggono (i consumatori).
  2. I produttori sono equivalenti tra loro ma esclusivi: un solo produttore alla volta può inserire i propri dati nell’array.
  3. I consumatori sono uguali tra loro ma si escludono a vicenda: un solo lettore alla volta può leggere i dati inseriti nell’array.
  4. 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:

    1. la tabella in scrittura
    2. 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:

Image

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:

while(peutEcrire==false)        ' attente active

oppure

while(peutEcrire==false) ' attente semi-active
    Thread.Sleep(100)                ' attente de 100ms
end while

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

AutoEvent peutEcrire=new AutoResetEvent(false)        ' peutEcrire=false;

inizializza il valore booleano peutEcrire a false. L'operazione

peutEcrire.WaitOne() ' le thread attend que l'évt peutEcrire passe à vrai

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