- Come sincronizzare il lavoro tra due o più thread tramite Monitor -
 
COSA SERVE PER QUESTO TUTORIAL
Download | Chiedi sul FORUM | Glossario cognizioni basiche di VB .Net sul threading
Sincronia tra thread con Monitor.Wait e Monitor.Pulse

PROBLEMA: SOSPENDERE UN'OPERAZIONE TEMPORANEAMENTE
Approcci per interrompere e riattivare un thread.

Nel precedente articolo avevamo introdotto l'oggetto Monitor e avevamo visto come il suo utilizzo poteva essere sostituito da un blocco SyncLock. In questo tutorial ci occuperemo sempre dell'oggetto Monitor, ma prestando attenzione ai metodi di sincronizzazione Wait, Pulse e PulseAll. Grazie ad essi è possibile mettere in attesa un thread (Monitor.Wait) fino a quando un altro thread non gli comunica che può riprendere l'esecuzione (Monitor.Pulse).
Immaginiamo di avere un thread che sta eseguendo una computazione pesante, nel nostro caso semplicemente conterà i numeri interi progressivamente, e immaginiamo di voler avere la possibilità di interrompere temporaneamente quel thread. Potremmo servirci del metodo Thread.Suspend ma questo è un metodo assai poco elegante, poiché il thread in questione non verrebbe notificato della sua sospensione. Vediamo dunque come si potrebbe risolvere questa situazione con l'utilizzo di Monitor.
Per prima cosa prendiamo in considerazione il codice per il working thread, estremamente semplice nella sua versione iniziale:


Public Sub Counter()
    Dim C As Decimal = 0
    Do
        C += 1
        If C = Decimal.MaxValue Then C = 0
    Loop

    Console.WriteLine("Thread terminato")
End Sub

Facciamo poi un piccolo sistema per avviare e controllare il thread che riceve tre parametri da linea di comando per uscire (quit), interrompere (pause) e riprendere (resume) l'esecuzione:


Dim syncObject As New Object()
Dim pauseFlag As Boolean = False
Dim stopFlag As Boolean = False

Sub SingleThread()

    Dim counterThread As New Thread(AddressOf Counter)
    counterThread.Start()

    Dim command As String
    command = Console.ReadLine()

    Do While command <> "quit"
        Select Case command
            Case "pause"
                ' ...
            Case "resume"
                ' ...
            Case Else
                Console.WriteLine("Unknown command")
        End Select

        command = Console.ReadLine()
    Loop

    ' ...
    
End Sub

Per effettuare la sincronizzazione ci serviremo di tre variabili:

  • syncObject: un oggetto di tipo Object su cui ci metteremo in attesa e su cui effettueremo i pulse;
  • pauseFlag: valore booleano per indicare se il thread è (o dovrebbe essere) in pausa;
  • stopFlag: valore booleano per indicare che il thread deve essere interrotto per completare l'uscita dal programma;

Vediamo dunque come implementare questo comportamento nel thread di Counter:


Public Sub Counter()
    Dim C As Decimal = 0
    Do
        SyncLock syncObject
            If stopFlag Then
                Return
            End If
            If pauseFlag Then
                Console.WriteLine(C)
                Monitor.Wait(syncObject)
                Continue Do
            End If
        End SyncLock

        C += 1
        If C = Decimal.MaxValue Then C = 0
    Loop

    Console.WriteLine("Thread terminato")
End Sub

Abbiamo aggiunto un blocco SyncLock su syncObject in modo da poter usare le variabili stopFlag e pauseFlag in maniera sicura. Se si verifica che stopFlag è vero semplicemente usciamo dal ciclo e interrompiamo il thread, se invece viene richiesto di entrare in uno stato di pausa, mostriamo a schermo il numero a cui siamo arrivati a contare e poi effettuiamo una chiamata a Monitor.Wait su syncObject (su cui abbiamo un lock): così facendo il thread si interromperà fino a quando qualcuno non effettuerà una Pulse su syncObject.
Vediamo a questo punto il codice di gestione completo:


Do While command <> "quit"
    Select Case command
        Case "pause"
            SyncLock syncObject
                pauseFlag = True
            End SyncLock
        Case "resume"
            SyncLock syncObject
                pauseFlag = False
                Monitor.Pulse(syncObject)
            End SyncLock
        Case Else
            Console.WriteLine("Unknown command")
    End Select

    command = Console.ReadLine()
Loop

SyncLock syncObject
    stopFlag = True
    If pauseFlag Then
        pauseFlag = False
        Monitor.Pulse(syncObject)
    End If
End SyncLock

Nel caso l'utente richieda di interrompere temporaneamente il conteggio, semplicemente acquisiamo un lock su syncObject e impostiamo il pauseFlag su vero. Quando poi viene richiesto di riattivare il thread acquisiamo il lock, impostiamo pauseFlag su falso ed effettuiamo il Pulse su syncObject (su cui anche in questo caso abbiamo un lock): in questo modo il thread che si trovava nello stato di wait riprenderà l'esecuzione da dove era stata interrotta (Monitor.Wait) e il conteggio proseguirà.
Infine nel caso venga richiesto di uscire, si acquisisce il lock su syncObject, impostiamo stopFlag su vero e, nel caso fossimo in stato di pausa, riattiviamo il thread per farlo terminare in maniera elegante.

INTERRUZIONE DI PIÙ THREAD
PulseAll per sbloccare tutti i thread in attesa. 

Proviamo ora a vedere come potrebbe funzionare la sincronizzazione in presenza di molti thread che lavorano contemporaneamente. Il codice del thread rimane esattamente lo stesso, semplicemente ne creiamo molti:


Dim ThreadCount As Integer
Console.Write("Number of threads: ")
ThreadCount = CInt(Console.ReadLine())

Dim counterThreads(ThreadCount - 1) As Thread
For C1 As Integer = 0 To counterThreads.Length - 1
    counterThreads(C1) = New Thread(AddressOf Counter)
    counterThreads(C1).Start()
Next C1

In questa maniera quando verrà dato il comando di pause, ci ritroveremo ad avere diversi thread in attesa e una chiamata a Pulse ne sbloccherebbe solamente uno, per questo motivo esiste il metodo PulseAll che effettua un Pulse su tutti i thread attualmente in attesa sbloccandoli tutti:


Select Case command
    Case "pause"
        SyncLock syncObject
            pauseFlag = True
        End SyncLock
    Case "resume one"
        SyncLock syncObject
            pauseFlag = False
            Monitor.Pulse(syncObject)
        End SyncLock
    Case "resume all", "resume"
        SyncLock syncObject
            pauseFlag = False
            Monitor.PulseAll(syncObject)
        End SyncLock
    Case Else
        Console.WriteLine("Unknown command")
End Select


 

 

<< INDIETRO by VeNoM00