Skip to content

8. Thread di esecuzione

8.1. Introduzione

Quando un'applicazione viene avviata, viene eseguita in un flusso di esecuzione chiamato thread. La classe .NET che modella un thread è la classe System.Threading.Thread e ha la seguente definizione:

Image

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

CurrentThread - proprietà statica
restituisce il thread attualmente in esecuzione
Nome - proprietà dell'oggetto
nome del thread
isAlive - proprietà dell'oggetto
indica se il thread è attivo (true) o meno (false)
Start - metodo dell'oggetto
avvia l'esecuzione di un thread
Abort - metodo dell'oggetto
interrompe definitivamente l'esecuzione di un thread
Sleep(n) - metodo statico
mette in pausa un thread per n millisecondi
Suspend() - metodo dell'oggetto
sospende temporaneamente l'esecuzione di un thread
Resume() - metodo dell'oggetto
riprende l'esecuzione di un thread sospeso
Join() - metodo dell'oggetto
operazione di blocco - attende il completamento del thread prima di procedere all'istruzione successiva

Diamo un'occhiata a una semplice applicazione che dimostra l'esistenza di un thread di esecuzione principale, quello in cui viene eseguita la funzione Main di una classe:


' use of threads
Imports System
Imports System.Threading
 
Public Module thread1
    Public Sub Main()
        ' init current thread
        Dim main As Thread = Thread.CurrentThread
        ' display
        Console.Out.WriteLine(("Thread courant : " + main.Name))
        ' we change the name
        main.Name = "main"
        ' check
        Console.Out.WriteLine(("Thread courant : " + main.Name))
        ' infinite loop
        While True
            ' display
            Console.Out.WriteLine((main.Name + " : " + DateTime.Now.ToString("hh:mm:ss")))
            ' temporary shutdown
            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 in un thread
  • possiamo accedere alle proprietà di questo thread tramite Thread.CurrentThread
  • il ruolo del metodo Sleep. In questo caso, il thread che esegue Main rimane inattivo 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 "simultaneamente" in thread di esecuzione diversi. Quando diciamo che i thread vengono eseguiti simultaneamente, spesso usiamo il termine in modo approssimativo. Se la macchina ha un solo processore, come spesso accade, i thread condividono questo processore: ciascuno di essi vi ha accesso, a turno, per un breve istante (pochi millisecondi). Questo è ciò che crea l'illusione dell'esecuzione parallela. La quantità di tempo assegnata a un thread dipende da vari fattori, tra cui la sua priorità, che ha un valore predefinito ma può anche essere impostata a livello di programmazione. Quando un thread ha il processore, normalmente lo utilizza per tutto il tempo a sua disposizione. Tuttavia, può rilasciarlo in anticipo:

  • attendendo un evento (wait, join, suspend)
  • mettendosi in standby per un periodo specificato (sleep)
  1. Un thread T viene creato inizialmente dal suo costruttore
Public Sub New(ByVal start As ThreadStart)

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

Public Delegate Sub ThreadStart()

Una struttura tipica è 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 del task T: procede immediatamente all'istruzione successiva. Ora abbiamo due task in esecuzione in parallelo. Spesso hanno bisogno di comunicare tra loro per conoscere lo stato del lavoro condiviso da svolgere. Questo è il problema della sincronizzazione dei thread.
  1. Una volta avviato, il thread viene eseguito in modo autonomo. Si fermerà quando la funzione di avvio che sta eseguendo avrà terminato il proprio lavoro.
  1. Possiamo inviare determinati segnali al thread T:
    1. T.Suspend() gli dice di mettersi temporaneamente in pausa
    2. T.Resume() gli dice di riprendere il suo lavoro
    3. T.Abort() gli dice di fermarsi definitivamente
  1. È anche possibile attendere che finisca di essere eseguito utilizzando T.join(). Si tratta di un'istruzione di blocco: il programma che la esegue rimane bloccato finché il task T non ha terminato il proprio lavoro. È un mezzo di sincronizzazione.

Esaminiamo il seguente programma:


' options
Option Strict On
Option Explicit On 
 
' namespaces
Imports System
Imports System.Threading
 
Module thread2
    Public Sub Main()
        ' init Current thread
        Dim main As Thread = Thread.CurrentThread
        ' name the Thread
        main.Name = "main"
 
        ' creation of execution threads
        Dim tâches(4) As Thread
        Dim i As Integer
        For i = 0 To tâches.Length - 1
            ' create thread i
            tâches(i) = New Thread(New ThreadStart(AddressOf affiche))
            ' set the thread name
            tâches(i).Name = "tache_" & i
            ' start execution of thread i
            tâches(i).Start()
        Next i
        ' end of hand
        Console.Out.WriteLine(("fin du thread " + main.Name))
    End Sub
 
    Public Sub affiche()
        ' display start of execution
        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")))
        ' sleep for 1 s
        Thread.Sleep(1000)
        ' display end of run
        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 responsabili dell'esecuzione del metodo statico display. 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 significativi:

  • Innanzitutto, vediamo 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 l'esecuzione prima di loro. Il
            ' on lance l'exécution du thread i
            tâches(i).Start()

avvia l'esecuzione del thread tasks[i], ma una volta fatto ciò, l'esecuzione prosegue immediatamente con l'istruzione successiva senza attendere il completamento del thread.

  • Tutti i thread creati devono eseguire il metodo display. 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 in questo caso dispone di 6 thread e un processore. Assegnerà il processore a questi 6 thread secondo le proprie regole.
  • I risultati mostrano l'effetto del metodo Sleep. Nell'esempio, il thread 0 è il primo ad eseguire il metodo display. Viene visualizzato il messaggio di inizio esecuzione, poi esegue il metodo Sleep, che lo sospende per 1 secondo. A quel punto perde il processore, che diventa disponibile per un altro thread. L'esempio mostra che il thread 1 lo otterrà. Il thread 1 seguirà lo stesso percorso, così come gli altri thread. Quando il periodo di sospensione di 1 secondo per il thread 0 termina, la sua esecuzione può riprendere. Il sistema gli assegna il processore e può completare l'esecuzione del metodo display.

Modifichiamo il nostro programma per terminare il metodo Main con le seguenti istruzioni:

        ' fin de main
        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. È l'istruzione

        Environment.Exit(0)

a farlo: termina tutti i thread dell'applicazione, non solo il thread Main. La soluzione a questo problema consiste nel far sì che il metodo Main attenda che i thread da esso creati terminino l'esecuzione prima di terminare se stesso. Ciò può essere fatto utilizzando il metodo Join della classe Thread:


        ' on attend la fin d'exécution de tous les threads
        For i = 0 To tâches.Length - 1
            ' attente de la fin d'exécution du thread i
            tâches(i).Join()
        Next i        'for
        ' fin de main
        Console.Out.WriteLine(("fin du thread " + main.Name))
        Environment.Exit(0)

Questo produce 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. I vantaggi dei thread

Ora che abbiamo evidenziato l'esistenza di un thread predefinito — quello che esegue il metodo Main — e sappiamo come crearne altri, consideriamo i vantaggi dei thread per noi e il motivo per cui li stiamo presentando qui. Esiste un tipo di applicazione che si presta bene all'uso dei thread: le applicazioni client-server su Internet. In un'applicazione di questo tipo, un server situato sulla macchina S1 risponde alle richieste provenienti da client situati su macchine remote C1, C2, ..., Cn.

Utilizziamo ogni giorno applicazioni Internet che seguono questo modello: servizi web, e-mail, navigazione nei forum, trasferimento di file... Nel diagramma sopra, il server S1 deve servire contemporaneamente i client C1, C2, ..., Cn. Se prendiamo l'esempio di un server FTP (File Transfer Protocol) che distribuisce file ai propri client, 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. Ciò che si fa di solito è che il server crei tanti thread di esecuzione quanti sono i clienti. Ogni thread è quindi responsabile della gestione di un cliente specifico. Poiché il processore è condiviso ciclicamente tra tutti i thread attivi sulla macchina, il server dedica un po' di tempo a ciascun cliente, garantendo così la concorrenza del servizio.

8.4. Accesso alle risorse condivise

Nell'esempio client-server sopra menzionato, ogni thread serve un cliente in modo largamente indipendente. Tuttavia, i thread potrebbero dover cooperare per fornire il servizio richiesto al proprio cliente, in particolare quando accedono a risorse condivise. Il diagramma sopra riportato ricorda gli sportelli di un grande ufficio pubblico, come un ufficio postale, dove un impiegato a ogni sportello serve un cliente. Supponiamo che di tanto in tanto questi impiegati debbano fare fotocopie di 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 in uso dall'impiegato j, dovrà attendere. Questa situazione è chiamata accesso a una risorsa condivisa e, nell'informatica, è piuttosto complessa da gestire. Si consideri il seguente esempio:

  • un'applicazione genererà n thread, dove n viene passato come parametro
  • la risorsa condivisa è un contatore che deve essere incrementato da ogni thread generato
  • alla fine dell'applicazione, viene visualizzato il valore del contatore. Dovremmo quindi ottenere n.

Il programma è il seguente:


' options
Option Explicit On 
Option Strict On
 
' use of threads
Imports System
Imports System.Threading
 
Public Class thread3
    ' class variables
    Private Shared cptrThreads As Integer = 0
 
    Public Overloads Shared Sub Main(ByVal args() As [String])
        ' instructions for use
        Const syntaxe As String = "pg nbThreads"
        Const nbMaxThreads As Integer = 100
 
        ' verification no. of arguments
        If args.Length <> 1 Then
            ' error
            Console.Error.WriteLine(syntaxe)
            ' stop
            Environment.Exit(1)
        End If
        ' argument quality check
        Dim nbThreads As Integer = 0
        Try
            nbThreads = Integer.Parse(args(0))
            If nbThreads < 1 Or nbThreads > nbMaxThreads Then
                Throw New Exception
            End If
        Catch
            ' error
            Console.Error.WriteLine("Nombre de threads incorrect (entre 1 et " & nbMaxThreads & ")")
            ' end
            Environment.Exit(2)
        End Try
        ' thread creation and generation
        Dim threads(nbThreads - 1) As Thread
        Dim i As Integer
        For i = 0 To nbThreads - 1
            ' creation
            threads(i) = New Thread(New ThreadStart(AddressOf incrémente))
            ' naming
            threads(i).Name = "tache_" & i
            ' launch
            threads(i).Start()
        Next i
        ' waiting for threads to finish
        For i = 0 To nbThreads - 1
            threads(i).Join()
        Next i        ' counter display
        Console.Out.WriteLine(("Nombre de threads générés : " & cptrThreads))
    End Sub
 
    Public Shared Sub incrémente()
        ' increases thread counter
        ' meter reading
        Dim valeur As Integer = cptrThreads
        ' follow-up
        Console.Out.WriteLine(("A " + DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a lu la valeur du compteur : " & cptrThreads))
        ' waiting
        Thread.Sleep(1000)
        ' counter incrementation
        cptrThreads = valeur + 1
        ' follow-up
        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 creazione dei thread, che abbiamo già trattato. Concentriamoci invece sul metodo Increment, utilizzato da ciascun thread per incrementare il contatore statico cptrThreads.

  1. Il contatore viene letto
  2. il thread si mette in pausa per 1 secondo. Perde quindi la CPU
  3. il contatore viene incrementato

Il passaggio 2 serve solo a costringere il thread a cedere il processore. Il processore verrà assegnato a un altro thread. In pratica, non vi è alcuna garanzia 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 la CPU tra il momento in cui viene letto il valore del contatore e il momento in cui viene scritto il suo valore, incrementato di 1. Infatti, l'operazione di incremento comporterà diverse istruzioni di base a livello di processore che possono essere interrotte. Il passaggio 2, la pausa di un secondo, serve quindi solo a tenere conto di 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

Osservando questi risultati, è chiaro cosa sta succedendo:

  • Un primo thread legge il contatore. Trova 0.
  • Si mette in pausa per 1 secondo, cedendo così la CPU
  • Un secondo thread prende quindi la CPU e legge a sua volta il valore del contatore. È ancora 0 poiché il thread precedente non l'ha ancora incrementato. Anche questo si mette in pausa per 1 secondo.
  • In 1 secondo, tutti e 5 i thread hanno il tempo di essere eseguiti e di leggere il valore 0.
  • Quando si riattivano uno dopo l'altro, incrementano il valore 0 che hanno letto e scrivono il valore 1 nel contatore, il che viene confermato dal programma principale (Main).

Da dove viene il problema? Il secondo thread ha letto un valore errato perché il primo thread è stato interrotto prima di aver completato il suo compito, che era quello di aggiornare il contatore nella finestra. Questo ci porta al concetto di risorse critiche e sezioni critiche in 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. Dobbiamo assicurarci che durante questa sezione critica, sia l'unico ad avere accesso alla risorsa.

8.5. Accesso esclusivo a una risorsa condivisa

Nel nostro esempio, la sezione critica è il codice situato tra la lettura del contatore e la scrittura del suo nuovo valore:


        ' meter reading
        Dim valeur As Integer = cptrThreads
        ' waiting
        Thread.Sleep(1000)
        ' counter incrementation
        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 single-threaded alle sezioni critiche del codice. Useremo la classe Mutex:

Image

Qui useremo 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 (il caso iniziale), viene "assegnato" al thread T1, che lo ha richiesto. Se, poco dopo, il thread T2 esegue la stessa operazione, verrà bloccato. Infatti, un mutex può appartenere a un solo thread. Verrà rilasciato quando il thread T1 rilascerà il mutex M che detiene. Diversi thread possono quindi essere bloccati in attesa del mutex M.
public void
ReleaseMutex()
Il thread T1 che esegue l'operazione M.ReleaseMutex() rinuncia alla proprietà del mutex M. Quando il thread T1 perde il processore, il sistema può assegnarlo a uno dei thread in attesa del mutex M. Solo uno lo otterrà a turno; gli altri in attesa di M rimangono bloccati

Un mutex M gestisce l'accesso a una risorsa condivisa R. Un thread richiede la risorsa R tramite M.WaitOne() e la rilascia 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ò essere ottenuta come segue:

M.WaitOne()
' le thread est seul à entrer ici
' section critique
....
M.ReleaseMutex()

dove M è un oggetto Mutex. Ovviamente, non bisogna mai dimenticare di rilasciare un Mutex che non è più necessario, in modo che un altro thread possa entrare nella sezione critica; altrimenti, i thread in attesa di un Mutex che non viene mai rilasciato non avranno mai accesso al processore. Inoltre, bisogna evitare una situazione di deadlock in cui due thread si aspettano a vicenda. Consideriamo le seguenti azioni che si verificano in sequenza:

  • un thread T1 acquisisce la proprietà di un Mutex M1 per accedere a una risorsa condivisa R1
  • un thread T2 acquisisce un Mutex M2 per accedere a una risorsa condivisa R2
  • Il thread T1 richiede il mutex M2. Viene bloccato.
  • Il thread T2 richiede il mutex M1. Viene bloccato.

In questo caso, i thread T1 e T2 si aspettano a vicenda. Questa situazione si verifica quando i thread richiedono due risorse condivise: la risorsa R1 controllata dal mutex M1 e la risorsa R2 controllata dal mutex M2. Una possibile soluzione consiste nell'acquisire entrambe le risorse contemporaneamente utilizzando un unico mutex M. Tuttavia, ciò non è sempre fattibile se, ad esempio, comporta un blocco prolungato su una risorsa costosa. Un'altra soluzione consiste nel far sì che un thread che detiene M1 e non può ottenere M2 rilasci M1 per evitare il deadlock. Se applichiamo quanto appena visto all'esempio precedente, la nostra applicazione diventa la seguente:


' options
Option Explicit On 
Option Strict On
 
' use of threads
Imports System
Imports System.Threading
 
Public Class thread4
    ' class variables
    Private Shared cptrThreads As Integer = 0    ' thread counter
    Private Shared autorisation As Mutex
 
    Public Overloads Shared Sub Main(ByVal args() As [String])
        ' instructions for use
        Const syntaxe As String = "pg nbThreads"
        Const nbMaxThreads As Integer = 100
 
        ' verification no. of arguments
        If args.Length <> 1 Then
            ' error
            Console.Error.WriteLine(syntaxe)
            ' stop
            Environment.Exit(1)
        End If
        ' argument quality check
        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
 
        ' initialize access authorization to a critical section
        autorisation = New Mutex
 
        ' thread creation and generation
        Dim threads(nbThreads) As Thread
        Dim i As Integer
        For i = 0 To nbThreads - 1
            ' creation
            threads(i) = New Thread(New ThreadStart(AddressOf incrémente))
            ' naming
            threads(i).Name = "tache_" & i
            ' launch
            threads(i).Start()
        Next i
        ' waiting for threads to finish
        For i = 0 To nbThreads - 1
            threads(i).Join()
        Next i
        ' counter display
        Console.Out.WriteLine(("Nombre de threads générés : " & cptrThreads))
    End Sub
 
    Public Shared Sub incrémente()
        ' increases thread counter
        ' we request permission to enter the critical secton
        autorisation.WaitOne()
        ' meter reading
        Dim valeur As Integer = cptrThreads
        ' follow-up
        Console.Out.WriteLine(("A " & DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a lu la valeur du compteur : " & cptrThreads))
        ' waiting
        Thread.Sleep(1000)
        ' counter incrementation
        cptrThreads = valeur + 1
        ' follow-up
        Console.Out.WriteLine(("A " & DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a écrit la valeur du compteur : " & cptrThreads))
        ' access authorization is returned
        autorisation.ReleaseMutex()
    End Sub
End Class

I risultati ottenuti sono quelli previsti:

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 definita scenario 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 si escludono a vicenda: solo un produttore alla volta può inserire dati nell'array.
  3. I consumatori sono uguali tra loro ma mutuamente esclusivi: solo un lettore alla volta può leggere i dati memorizzati nell'array.
  4. Un consumatore può leggere i dati dalla tabella solo dopo che un produttore li ha scritti, e un produttore può scrivere nuovi dati nella tabella solo dopo che i dati esistenti sono stati consumati.

In questa spiegazione, possiamo distinguere due risorse condivise:

    1. la tabella scrivibile
    2. l'array di sola lettura

L'accesso a queste due risorse condivise può essere controllato dai mutex, come visto in precedenza, uno per ciascuna risorsa. Una volta che un consumatore ha ottenuto l'array di sola lettura, deve verificare che vi siano effettivamente dei dati al suo interno. Per comunicarglielo verrà utilizzato un evento. Allo stesso modo, un produttore che ha ottenuto l'array di sola scrittura deve attendere che un consumatore lo abbia svuotato. Anche in questo caso verrà utilizzato un evento.

Gli eventi utilizzati saranno della classe AutoResetEvent:

Image

Questo tipo di evento è analogo a un booleano ma evita le attese attive o semi-attive. Pertanto, se l'accesso in scrittura è controllato da un booleano *canWrite*, un produttore eseguirà un codice simile al seguente prima di scrivere:

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 blocca inutilmente il processore. Nel secondo, controlla lo stato del booleano canWrite ogni 100 ms. La classe AutoResetEvent consente un ulteriore miglioramento: il thread richiederà di essere riattivato quando si verifica l'evento che sta aspettando:

AutoEvent peutEcrire=new AutoResetEvent(false)        ' peutEcrire=false;
....
peutEcrire.WaitOne() ' thread waits for peutEcrire event to change to true

L'operazione

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

inizializza il valore booleano canWrite a false. L'operazione

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

eseguita da un thread fa sì che quel thread proceda se il booleano canWrite* è vero; altrimenti, viene bloccato finché non diventa vero. Un altro thread lo imposterà a vero utilizzando l'operazione canWrite.Set() o a falso utilizzando l'operazione canWrite.Reset()*.

Il programma produttore-consumatore è il seguente:


' use of reader and writer threads
' illustrates the simultaneous use of shared resources and synchronization
 
' options
Option Explicit On 
Option Strict On
 
' use of threads
Imports System
Imports System.Threading
 
Public Class lececr
 
    ' class variables
    Private Shared data(5) As Integer    ' resource shared between reader and writer threads
    Private Shared lecteur As Mutex    ' synchronization variable to read the table
    Private Shared écrivain As Mutex    ' synchronization variable to write to the table
    Private Shared objRandom As New Random(DateTime.Now.Second)    ' a random number generator
    Private Shared peutLire As AutoResetEvent    ' indicates that you can read the contents of data
    Private Shared peutEcrire As AutoResetEvent
 
    Public Shared Sub Main(ByVal args() As [String])
 
        ' number of threads to generate
        Const nbThreads As Integer = 3
 
        ' flag initialization
        peutLire = New AutoResetEvent(False)        ' cannot be read yet
        peutEcrire = New AutoResetEvent(True)        ' we can already write
 
        ' initialization of synchronization variables
        lecteur = New Mutex         ' synchronizes drives
        écrivain = New Mutex         ' synchronizes writers
 
        ' creation of reader threads
        Dim lecteurs(nbThreads) As Thread
        Dim i As Integer
        For i = 0 To nbThreads - 1
            ' creation
            lecteurs(i) = New Thread(New ThreadStart(AddressOf lire))
            lecteurs(i).Name = "lecteur_" & i
            ' launch
            lecteurs(i).Start()
        Next i
 
        ' creating writer threads
        Dim écrivains(nbThreads) As Thread
        For i = 0 To nbThreads - 1
            ' creation
            écrivains(i) = New Thread(New ThreadStart(AddressOf écrire))
            écrivains(i).Name = "écrivain_" & i
            ' launch
            écrivains(i).Start()
        Next i
 
        'end of hand
        Console.Out.WriteLine("fin de Main...")
    End Sub
 
    ' read the contents of the table
    Public Shared Sub lire()
        ' review section
        lecteur.WaitOne()        ' a single reader can pass
        peutLire.WaitOne()        ' you must be able to read
 
        ' table reading
        Dim i As Integer
        For i = 0 To data.Length - 1
            'wait 1 s
            Thread.Sleep(1000)
            ' display
            Console.Out.WriteLine((DateTime.Now.ToString("hh:mm:ss") & " : Le lecteur " & Thread.CurrentThread.Name & " a lu le nombre " & data(i)))
        Next i
 
        ' we can no longer read
        peutLire.Reset()
        ' we can write
        peutEcrire.Set()
        ' end of critical section
        lecteur.ReleaseMutex()
    End Sub
 
    ' write in the table
    Public Shared Sub écrire()
        ' review section
        ' only one writer can pass
        écrivain.WaitOne()
        ' we have to wait for write authorization
        peutEcrire.WaitOne()
 
        ' writing table
        Dim i As Integer
        For i = 0 To data.Length - 1
            'wait 1 s
            Thread.Sleep(1000)
            ' display
            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
 
        ' we can no longer write
        peutEcrire.Reset()
        ' you can read
        peutLire.Set()
        'end of critical section
        écrivain.ReleaseMutex()
    End Sub
End Class

L'esecuzione produce 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, anche se questo perde la CPU nella sezione critica di lettura
  • c'è effettivamente un solo scrittore alla volta, anche se perde la CPU nella sezione critica di scrittura
  • Un lettore legge solo quando c'è qualcosa da leggere nella tabella
  • Uno scrittore scrive solo quando l'array è stato letto completamente