+ All Categories
Home > Documents > VOL 2 01 - apogeonline.com · ... C# e Visual C++. ... Quando un programma crea un nuovo thread,...

VOL 2 01 - apogeonline.com · ... C# e Visual C++. ... Quando un programma crea un nuovo thread,...

Date post: 27-Jul-2018
Category:
Upload: dinhtuong
View: 215 times
Download: 0 times
Share this document with a friend
38
CAPITOLO 1 Multithreading Obiettivi Comprendere il concetto di multithreading Capire come il multithreading sia in grado di migliorare le prestazioni dei programmi Creare, gestire ed eliminare i thread Conoscere il ciclo di vita di un thread Capire la sincronizzazione dei thread Comprendere il concetto di priorità e programmazione dei thread 1.1 Introduzione Il corpo umano esegue molte operazioni contenporaneamente o in parallelo. La respirazione, la circolazione sanguigna e la digestione, per esempio, avvengono contemporaneamente; anche i sensi (vista, tatto, olfatto, gusto e udito) possono essere tutti attivi nello stesso istante. Anche i computer possono svolgere più operazioni contemporaneamente. Per esem- pio, un PC è in grado di compilare un programma, trasmettere un file a una stampante e ricevere la posta elettronica contemporaneamente. Può sembrare strano che invece molti linguaggi di programmazione non permettano ai programmatori di specificare attività in parallelo. Questi linguaggi forniscono generalmente solo un gruppo di strutture di controllo che consentono ai programmatori di compiere un’azione alla volta, ovvero l’azione successiva può essere svolta dopo che quella precedente è stata completata. Il tipo di attività in parallelo che gli attuali computer eseguono è normalmente implementato sotto forma di “primitive” del sistema operativo accessibili soltanto ad esperti “programmatori di sistema”. Il linguaggio di programmazione Ada, sviluppato dal Dipartimento della Difesa degli Stati Uniti d’America, metteva a disposizione varie primitive alle aziende che si occupavano della realizzazione dei sistemi di controllo e di comando della difesa. Tuttavia, Ada non venne adottato dalle università e dall’industria. Class Library di .NET Framework consente ai programmatori di utilizzare più primitive contemporaneamente per le loro applicazioni. Un programmatore può specificare che un’ap- plicazione contiene dei thread di esecuzione; ogni thread identifica una parte di programma che può essere eseguita parallelamente ad altri thread. Questa caratteristica, chiamata multithreading, è disponibile in tutti i linguaggi di programmazione .NET, inclusi Visual Basic, C# e Visual C++.
Transcript

CAPITOLO 1

Multithreading

Obiettivi• Comprendere il concetto di multithreading

• Capire come il multithreading sia in grado di migliorare le prestazionidei programmi

• Creare, gestire ed eliminare i thread

• Conoscere il ciclo di vita di un thread

• Capire la sincronizzazione dei thread

• Comprendere il concetto di priorità e programmazione dei thread

1.1 IntroduzioneIl corpo umano esegue molte operazioni contenporaneamente o in parallelo. La respirazione,la circolazione sanguigna e la digestione, per esempio, avvengono contemporaneamente;anche i sensi (vista, tatto, olfatto, gusto e udito) possono essere tutti attivi nello stessoistante. Anche i computer possono svolgere più operazioni contemporaneamente. Per esem-pio, un PC è in grado di compilare un programma, trasmettere un file a una stampante ericevere la posta elettronica contemporaneamente.

Può sembrare strano che invece molti linguaggi di programmazione non permettano aiprogrammatori di specificare attività in parallelo. Questi linguaggi forniscono generalmentesolo un gruppo di strutture di controllo che consentono ai programmatori di compiere un’azionealla volta, ovvero l’azione successiva può essere svolta dopo che quella precedente è statacompletata. Il tipo di attività in parallelo che gli attuali computer eseguono è normalmenteimplementato sotto forma di “primitive” del sistema operativo accessibili soltanto ad esperti“programmatori di sistema”.

Il linguaggio di programmazione Ada, sviluppato dal Dipartimento della Difesa degliStati Uniti d’America, metteva a disposizione varie primitive alle aziende che si occupavanodella realizzazione dei sistemi di controllo e di comando della difesa. Tuttavia, Ada nonvenne adottato dalle università e dall’industria.

Class Library di .NET Framework consente ai programmatori di utilizzare più primitivecontemporaneamente per le loro applicazioni. Un programmatore può specificare che un’ap-plicazione contiene dei thread di esecuzione; ogni thread identifica una parte di programmache può essere eseguita parallelamente ad altri thread. Questa caratteristica, chiamatamultithreading, è disponibile in tutti i linguaggi di programmazione .NET, inclusi VisualBasic, C# e Visual C++.

2 CAPITOLO 1

Ingegneria del software 1.1

Il namespace System.ThreadingSystem.ThreadingSystem.ThreadingSystem.ThreadingSystem.Threading della Class Library di .NET Framework supporta lefunzionalità del multithreading. Questo agevola la diffusione del multithreading fra iprogrammatori e le società di software.

In questo capitolo vedrete molte applicazioni di programmazione multithreading. Peresempio, quando i programmi scaricano file di grandi dimensioni, come i file audio o videodal World Wide Web, l’utente non vuole aspettare che sia stato scaricato l’intero file prima dipoterne avviare la riproduzione. Per risolvere questo problema, è possibile utilizzare più thread:uno scarica il clip e un altro lo riproduce; in questo modo, le due attività (o task) possonoessere svolte contemporaneamente. Per evitare che la riproduzione del file avvenga a singhiozzo,i thread vengono coordinati in modo che il thread addetto alla riproduzione non parta finoa che non siano state caricate sufficienti informazioni nella memoria.

Un altro esempio di multithreading è il garbage collector (un meccanismo automatico direcupero della memoria detto “spazzino”) di Visual Basic. Nei linguaggi C e C++ deve essereil programmatore a occuparsi di liberare la memoria allocata che non viene più utilizzata.Visual Basic, invece, dispone di un thread speciale (spazzino automatico) che libera dinamica-mente la memoria allocata quando non è più richiesta dai programmi.

Collaudo e messa a punto 1.1

Con i linguaggi C e C++ i programmatori devono scrivere esplicitamente delle istruzioniper liberare la memoria allocata dinamicamente. Se ciò non avviene (perché un program-matore dimentica di farlo, perché c’è un errore di logica oppure perché un’eccezione hadeviato altrove il flusso del programma), si genera un tipico fenomeno, chiamato perditadi memoria, che col passare del tempo può degenerare provocando l’interruzione del pro-gramma. Grazie al garbage collector di Visual Basic, il rischio della perdita di memoriaè notevolmente ridotto.

Obiettivo efficienza 1.1

Una delle ragioni per le quali i linguaggi C e C++ hanno mantenuto la loro popolaritànegli anni è che le tecniche di gestione della memoria erano più efficienti di quelle deilinguaggi che adottavano il garbage collector. Tuttavia, la gestione della memoria con VisualBasic di solito è più rapida rispetto ai linguaggi C e C++.

Buona abitudine 1.1

Impostate il riferimento di un oggetto a NothingNothingNothingNothingNothing quando il programma non ha più bisognodi questo oggetto. Ciò consente al garbage collector di liberare al più presto possibile lamemoria allocata per l’oggetto. Se il programma mantiene altri riferimenti di questo oggetto,il garbage collector non potrà operare.

Scrivere programmi multithread non è semplice. Sebbene la mente umana sia in gradodi svolgere più funzioni contemporaneamente, tuttavia le persone spesso trovano difficilesaltare fra due treni di pensieri paralleli. Per capire perché sia difficile la programmazionemultithread, fate questo esperimento: aprite tre libri alla prima pagina e tentate di leggerli inparallelo. Leggete alcune parole del primo libro, poi alcune parole del secondo libro e alcuneparole del terzo libro; ritornate a leggere alcune parole del primo libro e così via. Questoesperimento dimostra come sia difficile passare da un libro all’altro, leggendo poche parole

MULTITHREADING 3

alla volta, ricordandosi ogni volta l’ultima parola letta e, soprattutto, come sia praticamenteimpossibile capire il contenuto dei tre libri.

Obiettivo efficienza 1.2

Un problema che può verificarsi con le applicazioni con un solo thread è che, a volte, alcuneattività molto lunghe devono essere completate prima che possano avere inizio altre atti-vità. Nelle applicazioni multithread, invece, i vari thread possono condivedere uno o piùprocessori, consentendo di svolgere più attività in parallelo.

1.2 Stati e ciclo di vita di un threadIn qualsiasi momento, un thread si trova in uno dei vari stati illustrati nella Figura 1.1.Questo paragrafo descrive i vari stati e le transizioni da uno stato all’altro. Due classi chesono essenziali per le applicazioni multithread sono ThreadThreadThreadThreadThread e MonitorMonitorMonitorMonitorMonitor (namespaceSystem.ThreadingSystem.ThreadingSystem.ThreadingSystem.ThreadingSystem.Threading). Descriveremo anche i metodi di queste due classi che permettono letransizioni da uno stato all’altro di un thread.

Quando un programma crea un nuovo thread, questo inizia il suo ciclo di vita nellostato UnstartedUnstartedUnstartedUnstartedUnstarted. Il thread rimane in questo stato finché il programma non chiama il metodoStartStartStartStartStart della classe ThreadThreadThreadThreadThread; quando questo avviene, il thread entra nello stato StartedStartedStartedStartedStarted oReadyReadyReadyReadyReady. A questo punto, il thread che ha chiamato il metodo StartStartStartStartStart, il nuovo thread e altrithread del programma possono essere eseguiti in parallelo.

Il thread StartedStartedStartedStartedStarted che ha un livello di priorità più alto entra nello stato RunningRunningRunningRunningRunning (iniziaa essere eseguito) quando il sistema operativo gli assegna un processore (il Paragrafo 1.3descrive i livelli di priorità dei thread). Quando un thread StartedStartedStartedStartedStarted riceve un processore per

Figura 1.1 Stati e ciclo di vita di un thread

4 CAPITOLO 1

la prima volta e diventa un thread RunningRunningRunningRunningRunning, il thread esegue il suo delegato ThreadStartThreadStartThreadStartThreadStartThreadStart,che specifica le azioni che il thread svolgerà durante il suo ciclo di vita. Quando un programmacrea un nuovo ThreadThreadThreadThreadThread, il programma specifica il delegato ThreadStartThreadStartThreadStartThreadStartThreadStart come argomentodel costruttore ThreadThreadThreadThreadThread. Il delegato deve essere una procedura che non riceve argomenti.

Un thread RunningRunningRunningRunningRunning entra nello stato StoppedStoppedStoppedStoppedStopped quando il suo delegato ThreadStartThreadStartThreadStartThreadStartThreadStarttermina. Notate che un programma può forzare un thread nello stato StoppedStoppedStoppedStoppedStopped chiamando ilmetodo AbortAbortAbortAbortAbort di ThreadThreadThreadThreadThread sull’appropriato oggetto ThreadThreadThreadThreadThread. Il metodo AbortAbortAbortAbortAbort genera un’ecce-zione ThreadAbortExceptionThreadAbortExceptionThreadAbortExceptionThreadAbortExceptionThreadAbortException che, di solito, provoca la fine del thread. Quando un threadsi trova nello stato StoppedStoppedStoppedStoppedStopped e non ci sono altri riferimenti al thread nel programma, il garbagecollector può eliminare l’oggetto dalla memoria.

Un thread entra nello stato BlockedBlockedBlockedBlockedBlocked (bloccato) quando effettua una richiesta di input/output. Il sistema operativo blocca l’esecuzione del thread finché non viene completata l’ope-razione di I/O richiesta dal thread. Una volta completata questa operazione, il thread ritornanello stato StartedStartedStartedStartedStarted e riprende la sua esecuzione. Un thread BlockedBlockedBlockedBlockedBlocked non può usare unprocessore, anche se questo fosse disponibile.

Ci sono tre modi in cui un thread RunningRunningRunningRunningRunning può entrare nello stato WaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoin. Seun thread incontra un codice che non può eseguire (di solito, una condizione che non èsoddisfatta), il thread può chiamare il metodo WaitWaitWaitWaitWait di MonitorMonitorMonitorMonitorMonitor per entrare nello statoWaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoin. Una volta in questo stato, il thread ritorna nello stato StartedStartedStartedStartedStarted quando unaltro thread chiama il metodo PulsePulsePulsePulsePulse o PulseAllPulseAllPulseAllPulseAllPulseAll di MonitorMonitorMonitorMonitorMonitor. Il metodo PulsePulsePulsePulsePulse sposta ilsuccessivo thread in attesa nello stato StartedStartedStartedStartedStarted. Il metodo PulseAllPulseAllPulseAllPulseAllPulseAll sposta tutti i thread inattesa nello stato StartedStartedStartedStartedStarted. In alternativa, un thread RunningRunningRunningRunningRunning può chiamare il metodo SleepSleepSleepSleepSleepdi ThreadThreadThreadThreadThread per entrare nello stato WaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoin per il periodo di tempo (espresso inmillisecondi) che è specificato come argomento di SleepSleepSleepSleepSleep. Un thread SleepingSleepingSleepingSleepingSleeping ritorna nellostato StartedStartedStartedStartedStarted quando il suo tempo di sospensione scade. Come i thread BlockedBlockedBlockedBlockedBlocked, anche ithread SleepingSleepingSleepingSleepingSleeping non possono usare un processore, anche se questo fosse disponibile.

Qualsiasi thread che entra nello stato WaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoin chiamando il metodo WaitWaitWaitWaitWait diMonitorMonitorMonitorMonitorMonitor o il metodo SleepSleepSleepSleepSleep di ThreadThreadThreadThreadThread lascia lo stato WaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoin e ritorna nello statoStartedStartedStartedStartedStarted se viene chiamato il metodo InterruptInterruptInterruptInterruptInterrupt di ThreadThreadThreadThreadThread da un altro thread del program-ma. Se un thread (detto thread dipendente) non può continuare l’esecuzione finché non ter-mina un altro thread, il thread dipendente chiama il metodo JoinJoinJoinJoinJoin dell’altro thread per“unire” (join) i due thread. Quando due thread sono “uniti”, il thread dipendente lascia lostato WaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoin quando l’altro thread finisce l’esecuzione (entra nello stato StoppedStoppedStoppedStoppedStopped).Se viene chiamato il metodo SuspendSuspendSuspendSuspendSuspend di un thread RunningRunningRunningRunningRunning, il thread entra nello statoSuspendedSuspendedSuspendedSuspendedSuspended. Un thread SuspendedSuspendedSuspendedSuspendedSuspended ritorna nello stato StartedStartedStartedStartedStarted quando un altro thread delprogramma chiama il metodo ResumeResumeResumeResumeResume del thread SuspendedSuspendedSuspendedSuspendedSuspended.

1.3 Priorità dei threadOgni thread ha un livello di priorità che varia da un minimo (ThreadPriority.LowestThreadPriority.LowestThreadPriority.LowestThreadPriority.LowestThreadPriority.Lowest) aun massimo (ThreadPriority.HighestThreadPriority.HighestThreadPriority.HighestThreadPriority.HighestThreadPriority.Highest). Questi due livelli provengono dall’enumerazioneThreadPriorityThreadPriorityThreadPriorityThreadPriorityThreadPriority (namespace System.ThreadingSystem.ThreadingSystem.ThreadingSystem.ThreadingSystem.Threading), che contiene i valori LowestLowestLowestLowestLowest, BelowNormalBelowNormalBelowNormalBelowNormalBelowNormal,NormalNormalNormalNormalNormal, AboveNormalAboveNormalAboveNormalAboveNormalAboveNormal e HighestHighestHighestHighestHighest. Per default, ogni nuovo thread ha priorità NormalNormalNormalNormalNormal. Loscheduler di Visual Basic determina quando eseguire i singoli thread in base ai loro livelli dipriorità. La piattaforma Windows supporta un concetto chiamato timeslicing, che consenteai thread di uguale priorità di condividere un processore. Senza il timeslicing, ogni thread

MULTITHREADING 5

all’interno di un gruppo di thread dello stesso livello di priorità viene eseguito fino alcompletamento (a meno che il thread non lasci lo stato RunningRunningRunningRunningRunning ed entri nello statoWaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoin, SuspendedSuspendedSuspendedSuspendedSuspended o BlockedBlockedBlockedBlockedBlocked) prima che gli altri thread possano essere eseguiti.Con il timeslicing, a ogni thread viene assegnato un periodo di tempo limitato, chiamatoquantum o timeslice, durante il quale può essere eseguito. Allo scadere del quantum, anche seil thread non ha ancora finito l’esecuzione, il processore passa al successivo thread di paripriorità (se ce n’è uno disponibile).

Il compito dello scheduler è mantenere sempre in esecuzione il thread con la priorità piùelevata e, nel caso sia disponibile il timeslicing, assicurare che più thread di pari prioritàvengano eseguiti un po’ alla volta. La Figura 1.2 mostra la coda dei thread di vari livelli dipriorità. In questa figura, i thread A e B vengono eseguiti a turno per un quantum, finchéentrambi non saranno completati. Questo significa che A riceve un quantum di tempo peressere eseguito, poi B riceve un quantum, poi A riceve un altro quantum, poi B e così via.Questo processo continua finché un thread non viene completato; a questo punto, il processorededica tutto il suo tempo all’altro thread (a meno che un altro thread di pari priorità nonpassi nello stato StartedStartedStartedStartedStarted). Dopo i thread A e B, viene completato il thread C. Poi vengonoeseguiti a turno i thread D, E e F, e così via, fino a completare tutti gli altri thread. Notate chenuovi thread di priorità elevata possono ritardare (anche per un tempo indefinito) l’esecuzionedei thread di priorità più bassa. Questa condizione estrema di mancanza di risorse è dettastarvation (fame).

La priorità di un thread può essere modificata con la proprietà PriorityPriorityPriorityPriorityPriority, che accetta ivalori dall’enumerazione ThreadPriorityThreadPriorityThreadPriorityThreadPriorityThreadPriority. Se l’argomento non è uno dei valori consentiti,si verifica un’eccezione ArgumentExceptionArgumentExceptionArgumentExceptionArgumentExceptionArgumentException.

Figura 1.2 Esecuzione di thread di vari livelli di priorità

6 CAPITOLO 1

Un thread viene eseguito fino a che muore (stato StoppedStoppedStoppedStoppedStopped), passa allo stato BlockedBlockedBlockedBlockedBlocked peruna richiesta di input/output (o per qualche altro motivo), chiama il metodo SleepSleepSleepSleepSleep, chiamail metodo WaitWaitWaitWaitWait o JoinJoinJoinJoinJoin di MonitorMonitorMonitorMonitorMonitor, viene superato da un thread di priorità più alta o il suoquantum scade. Un thread con priorità più elevata del thread RunningRunningRunningRunningRunning può passare allo statoStartedStartedStartedStartedStarted (e da qui superare il thread RunningRunningRunningRunningRunning) se un thread SleepingSleepingSleepingSleepingSleeping si sveglia, se vienecompletata l’operazione di I/O che teneva bloccato il thread, se viene chiamato il metodoPulsePulsePulsePulsePulse o PulseAllPulseAllPulseAllPulseAllPulseAll su un oggetto per il quale il thread era in attesa o se viene completato unthread al quale era unito il thread di priorità più alta.

Le Figure 1.3 e 1.4 applicano le tecniche di base del threading, quali la costruzione di unoggetto ThreadThreadThreadThreadThread e l’utilizzo del metodo shared SleepSleepSleepSleepSleep della classe ThreadThreadThreadThreadThread. Il modulomodThreadTestermodThreadTestermodThreadTestermodThreadTestermodThreadTester (Figura 1.4) crea tre thread, ciascuno dei quali ha la priorità di defaultNormalNormalNormalNormalNormal. Ogni thread visualizza un messaggio che indica cha sta andando a “dormire” (sleep) perun periodo di tempo casuale compreso fra 0 e 5000 millisecondi. Trascorso questo periodo, ilthread visualizza un messaggio per indicare che si è “svegliato”, poi entra nello stato StoppedStoppedStoppedStoppedStopped.Notate che il metodo MainMainMainMainMain (ovvero il thread di esecuzione principale) finisce prima che terminil’applicazione. Il programma è formato da un modulo, modThreadTestermodThreadTestermodThreadTestermodThreadTestermodThreadTester (Figura 1.4), checrea i tre thread, e da una classe, CMessagePrinterCMessagePrinterCMessagePrinterCMessagePrinterCMessagePrinter (Figura 1.3), che definisce un metodoPrintPrintPrintPrintPrint che contiene le azioni che dovranno essere svolte da ciascun thread.

1 ‘ File: MessagePrinter.vb 2 ‘ un metodo di controllo dei thread che visualizza 3 ‘ dei messaggi per indicare lo stato dei thread 4 5 Imports System.Threading 6 7 Public Class CMessagePrinter 8 9 Private sleepTime As Integer10 Private Shared randomObject As New Random()1112 ‘ costruttore per inizializzare un oggetto CMessagePrinter13 Public Sub New()1415 ‘ imposta la durata casuale di sleepTime fra 0 e 5 secondi16 sleepTime = randomObject.Next(5001)17 End Sub ‘ New1819 ‘ il metodo Print controlla il thread dei messaggi20 Public Sub Print()2122 ‘ ottiene un riferimento al thread in esecuzione23 Dim current As Thread = Thread.CurrentThread2425 ‘ pone il thread nello stato Sleeping per una certa durata26 Console.WriteLine(current.Name & “ going to sleep for “ & _27 sleepTime)2829 Thread.Sleep(sleepTime)3031 ‘ visualizza il nome del thread

MULTITHREADING 7

32 Console.WriteLine(current.Name & “ done sleeping”)33 End Sub ‘ Print3435 End Class ‘ CmessagePrinter

Figura 1.3 Il metodo Print descrive le azioni svolte dai thread

Gli oggetti della classe CMessagePrinterCMessagePrinterCMessagePrinterCMessagePrinterCMessagePrinter (Figura 1.3) controllano il ciclo di vita diciascuno dei tre thread creati dal metodo MainMainMainMainMain di modThreadTestermodThreadTestermodThreadTestermodThreadTestermodThreadTester. La classe CMessagePrinterCMessagePrinterCMessagePrinterCMessagePrinterCMessagePrinterè formata dalla variabile di istanza sleepTimesleepTimesleepTimesleepTimesleepTime (riga 9), dalla variabile shared randomObjectrandomObjectrandomObjectrandomObjectrandomObject(riga 10), da un costruttore (righe 13-17) e da un metodo PrintPrintPrintPrintPrint (righe 20-33). La variabilesleepTimesleepTimesleepTimesleepTimesleepTime contiene un valore casuale di tipo IntegerIntegerIntegerIntegerInteger che è scelto quando viene chiamatoil costruttore di un nuovo oggetto CMessagePrinterCMessagePrinterCMessagePrinterCMessagePrinterCMessagePrinter. Ogni thread controllato da un oggettoCMessagePrinterCMessagePrinterCMessagePrinterCMessagePrinterCMessagePrinter resta nello stato SleepingSleepingSleepingSleepingSleeping per il periodo di tempo specificato dal valoresleepTimesleepTimesleepTimesleepTimesleepTime del corrispondente oggetto CMessagePrinterCMessagePrinterCMessagePrinterCMessagePrinterCMessagePrinter.

Il costruttore CMessagePrinterCMessagePrinterCMessagePrinterCMessagePrinterCMessagePrinter (righe 13-17) inizializza sleepTimesleepTimesleepTimesleepTimesleepTime con un valore in-tero casuale compreso fra 0 e 5000 (l’estremo 5001 è escluso). Il metodo PrintPrintPrintPrintPrint (righe 20-33) inizia ottenendo un riferimento al thread che è in corso di esecuzione (riga 23) attraversola proprietà shared CurrentThreadCurrentThreadCurrentThreadCurrentThreadCurrentThread della classe ThreadThreadThreadThreadThread. Il thread correntemente in esecu-zione è quello che chiama il metodo PrintPrintPrintPrintPrint. Successivamente, le righe 26-27 visualizzano unmessaggio che contiene il nome del thread corrente e la durata in millisecondi dello statoSleepingSleepingSleepingSleepingSleeping del thread. Notate che la riga 26 usa il thread corrente tramite la proprietà NameNameNameNameName,che viene impostata nel metodo MainMainMainMainMain (Figura 1.4, righe 8-35) quando viene creato il thread.La riga 29 chiama il metodo shared SleepSleepSleepSleepSleep per porre il thread nello stato WaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoin. Aquesto punto, il thread perde il processore e il sistema consente l’esecuzione di un altrothread. Quando il thread si sveglia, ritorna nello stato StartedStartedStartedStartedStarted finché non gli viene assegnatoun processore. Quando l’oggetto CMessagePrinterCMessagePrinterCMessagePrinterCMessagePrinterCMessagePrinter entra di nuovo nello stato RunningRunningRunningRunningRunning, lariga 32 visualizza il nome del thread in un messaggio che indica che il thread ha finito didormire; il metodo PrintPrintPrintPrintPrint si conclude.

Il modulo MainMainMainMainMain di modThreadTestermodThreadTestermodThreadTestermodThreadTestermodThreadTester (Figura 1.4, righe 8-35) crea tre oggetti dellaclasse CMessagePrinterCMessagePrinterCMessagePrinterCMessagePrinterCMessagePrinter (righe 11-13). Le righe 17-19 creano e inizializzano tre oggettiThreadThreadThreadThreadThread che corrispondono agli oggetti CMessagePrinterCMessagePrinterCMessagePrinterCMessagePrinterCMessagePrinter creati. Le righe 22-24 impostanola proprietà NameNameNameNameName di ciascun thread. Notate che il costruttore di ogni thread riceve comeargomento un delegato ThreadStartThreadStartThreadStartThreadStartThreadStart. Ricordiamo che un delegato ThreadStartThreadStartThreadStartThreadStartThreadStart specificale azioni che un thread svolge durante il suo ciclo di vita. La riga 17 specifica che il delegatothread1thread1thread1thread1thread1 sarà il metodo PrintPrintPrintPrintPrint dell’oggetto cui fa riferimento printer1printer1printer1printer1printer1. Quando thread1thread1thread1thread1thread1entra nello stato RunningRunningRunningRunningRunning per la prima volta, thread1thread1thread1thread1thread1 chiama il metodo PrintPrintPrintPrintPrint di printer1printer1printer1printer1printer1per svolgere i compiti specificati nel corpo del metodo PrintPrintPrintPrintPrint. Poi, thread1thread1thread1thread1thread1 visualizza ilnome del thread e la durata del periodo di SleepingSleepingSleepingSleepingSleeping; trascorso questo periodo, il threadvisualizza un messaggio per indicare che si è svegliato. Qui termina il metodo PrintPrintPrintPrintPrint. Unthread completa il suo compito quando termina il metodo specificato da un delegatoThreadStartThreadStartThreadStartThreadStartThreadStart di ThreadThreadThreadThreadThread, ponendo il thread nello stato StoppedStoppedStoppedStoppedStopped. Quando thread2thread2thread2thread2thread2 e thread3thread3thread3thread3thread3entrano per la prima volta nello stato RunningRunningRunningRunningRunning, chiamano i metodi PrintPrintPrintPrintPrint, rispettivamente,di printer2printer2printer2printer2printer2 e printer3printer3printer3printer3printer3. I thread thread2thread2thread2thread2thread2 e thread3thread3thread3thread3thread3 svolgono gli stessi compiti di thread1thread1thread1thread1thread1eseguendo i metodi PrintPrintPrintPrintPrint degli oggetti cui fanno riferimento printer2printer2printer2printer2printer2 e printer3printer3printer3printer3printer3 (ciascunocon un valore di sleepTimesleepTimesleepTimesleepTimesleepTime scelto a caso).

Le righe 30-32 chiamano il metodo StartStartStartStartStart dei vari thread per porli nello stato StartedStartedStartedStartedStarted(questo processo è detto anche lancio del thread). La riga 34 visualizza un messaggio che

8 CAPITOLO 1

indica che i thread sono stati avviati; l’esecuzione del thread MainMainMainMainMain termina. Il programmanon termina, invece, perché contiene ancora thread “vivi” (quelli che si trovano nello statoStartedStartedStartedStartedStarted e non hanno ancora raggiunto lo stato StoppedStoppedStoppedStoppedStopped). Il programma termina quando“muore” l’ultimo thread. Quando il sistema assegna un processore a un thread, questo entranello stato RunningRunningRunningRunningRunning e chiama il metodo specificato dal delegato ThreadStartThreadStartThreadStartThreadStartThreadStart. In questoprogramma, ogni thread chiama il metodo PrintPrintPrintPrintPrint dell’oggetto CMessagePrinterCMessagePrinterCMessagePrinterCMessagePrinterCMessagePrinter appro-priato per svolgere i compiti precedentemente descritti. Notate che l’output della Figura 1.4visualizza il nome e la durata dello stato SleepingSleepingSleepingSleepingSleeping di ciascun thread. Il thread con la duratadi SleepingSleepingSleepingSleepingSleeping più breve è quello che di solito si sveglia per primo; poi indica che ha dormitoe la sua esecuzione termina. Il Paragrafo 1.7 descrive i fattori che possono impedire al threadcon durata di SleepingSleepingSleepingSleepingSleeping più breve di svegliarsi per primo.

Collaudo e messa a punto 1.2

I nomi assegnati ai thread agevolano il debugging di un programma multithread. Il debuggerdi Visual Studio .NET ha una finestra ThreadsThreadsThreadsThreadsThreads che visualizza il nome di ogni thread econsente ai programmatori di osservare l’esecuzione di qualsiasi thread del programma.

1 ‘ File: ThreadTester.vb 2 ‘ genera tre thread 3 4 Imports System.Threading 5 6 Module modThreadTester 7 8 Sub Main() 910 ‘ crea le istanze di CMessagePrinter11 Dim printer1 As New CMessagePrinter()12 Dim printer2 As New CMessagePrinter()13 Dim printer3 As New CMessagePrinter()1415 ‘ crea i thread; usa il metodo Print16 ‘ di CMessagePrinter come argomento di ThreadStart17 Dim thread1 As New Thread(AddressOf printer1.Print)18 Dim thread2 As New Thread(AddressOf printer2.Print)19 Dim thread3 As New Thread(AddressOf printer3.Print)2021 ‘ assegna un nome a ogni thread22 thread1.Name = “thread1”23 thread2.Name = “thread2”24 thread3.Name = “thread3”2526 Console.WriteLine(“Starting threads”)2728 ‘ chiama il metodo Start di ciascun thread per29 ‘ metterlo nello stato Started30 thread1.Start()31 thread2.Start()32 thread3.Start()33

MULTITHREADING 9

34 Console.WriteLine(“Threads started” & vbCrLf)35 End Sub ‘ Main3637 End Module ‘ modThreadTester

Starting threadsThreads started

thread1 going to sleep for 1977thread2 going to sleep for 4513thread3 going to sleep for 1261thread3 done sleepingthread1 done sleepingthread2 done sleeping

Starting threadsThreads started

thread1 going to sleep for 1466thread2 going to sleep for 4245thread3 going to sleep for 1929thread1 done sleepingthread3 done sleepingthread2 done sleeping

Figura 1.4 Il modulo modThreadTester crea tre thread

1.4 Sincronizzazione dei threadPuò capitare che più thread in esecuzione manipolino i dati condivisi (shared). Se i threadche hanno accesso ai dati condivisi si limitano a leggere questi dati, non c’è bisogno diimpedire l’accesso ai dati a più thread contemporaneamente. Se, invece, più thread condivi-dono e possono modificare gli stessi dati, allora si corre il rischio di generare risultati indeter-minati. Se un thread sta aggiornando i dati e un altro thread tenta di aggiornare gli stessi dati,i dati risultanti rifletteranno l’aggiornamento più recente. Se i dati sono raccolti in un arrayo in un’altra struttura di cui i thread possono aggiornare contemporaneamente parti distinte,è possibile che una parte dei dati rifletta l’aggiornamento di un thread, mentre un’altra parterifletta l’aggiornamento di un thread differente. Se si verifica questo, è difficile per il pro-gramma stabilire se i dati siano stati aggiornati in modo appropriato.

I programmatori possono risolvere questo problema assegnando a un determinato threadl’accesso esclusivo ai dati durante l’operazione di aggiornamento. In altri termini, mentre unthread sta manipolando i dati, tutti gli altri thread che desiderano accedere ai dati devonoattendere che il primo completi la sua operazione. Dopo che il thread con accesso esclusivoai dati ha completato la sua operazione, il programma può consentire al primo thread che èrimasto in attesa di accedere ai dati. In questo modo, ogni thread che accede ai dati escludeautomaticamente tutti gli altri thread. Questo meccanismo si chiama mutua esclusione osincronizzazione dei thread.

Visual Basic utilizza i monitor di .NET Framework per eseguire la sincronizzazione deithread. La classe MonitorMonitorMonitorMonitorMonitor possiede i metodi per bloccare gli oggetti, consentendo così diimplementare l’accesso sincronizzato ai dati condivisi. Se un oggetto è bloccato (locked), un

10 CAPITOLO 1

solo thread alla volta può accedere a questo oggetto. Quando un thread tenta di acquisire ilcontrollo esclusivo di un oggetto, chiama il metodo EnterEnterEnterEnterEnter di MonitorMonitorMonitorMonitorMonitor per bloccare l’oggetto.Ogni oggetto ha un SyncBlockSyncBlockSyncBlockSyncBlockSyncBlock che mantiene lo stato di blocco dell’oggetto. I metodi dellaclasse MonitorMonitorMonitorMonitorMonitor usano i dati di SyncBlockSyncBlockSyncBlockSyncBlockSyncBlock per determinare lo stato di blocco di un oggetto.Dopo avere bloccato un oggetto, un thread può manipolare i dati dell’oggetto. Mentre l’og-getto è bloccato, tutti gli altri thread sono bloccati (ovvero entrano nello stato BlockedBlockedBlockedBlockedBlocked) enon possono porre un blocco all’oggetto. Quando il thread che ha bloccato l’oggetto condi-viso non ha più bisogno del blocco, chiama il metodo ExitExitExitExitExit di MonitorMonitorMonitorMonitorMonitor per togliere il blocco;questa operazione aggiorna il valore SyncBlockSyncBlockSyncBlockSyncBlockSyncBlock dell’oggetto condiviso per indicare che ilblocco dell’oggetto è di nuovo disponibile. A questo punto, un thread nello stato BlockedBlockedBlockedBlockedBlocked,che in precedenza non poteva bloccare l’oggetto condiviso, adesso può farlo e iniziare amanipolare l’oggetto. Se tutti i thread con accesso a un oggetto devono porre il blocco sul-l’oggetto prima di manipolarlo, ciò potrà essere fatto da un solo thread alla volta. Questoaiuta a garantire l’integrità dei dati.

Errore tipico 1.1

Verificate che il codice che aggiorna un oggetto condiviso blocchi l’oggetto prima di farlo;altrimenti, un thread che chiama un metodo che non blocca l’oggetto può rendere insta-bile l’oggetto, anche quando un altro thread ha posto un blocco sull’oggetto.

Errore tipico 1.2

Un deadlock si verifica quando l’esecuzione di un thread (per esempio, thread1) non puòproseguire, perché è in attesa di un altro thread (thread2); analogamente, thread2 nonpuò proseguire perché, a sua volta, è in attesa di thread1. Poiché i due thread sono in attesal’uno dell’altro, le loro azioni non potranno mai essere svolte.

Visual Basic dispone di altri strumenti per manipolare il blocco di un oggetto: la parolachiave SyncLockSyncLockSyncLockSyncLockSyncLock. Esaminate le seguenti istruzioni:

SyncLock ( riferimentoOggetto ) ‘ qui va scritto il codice che richiede la sincronizzazioneEnd SyncLock

La struttura SyncLockSyncLockSyncLockSyncLockSyncLock … EndEndEndEndEnd SyncLockSyncLockSyncLockSyncLockSyncLock pone un blocco sull’oggetto identificato dalriferimentoOggetto fra parentesi. Il termine riferimentoOggetto è lo stesso riferimento che nor-malmente verrebbe passato ai metodi EnterEnterEnterEnterEnter, ExitExitExitExitExit, PulsePulsePulsePulsePulse e PulseAllPulseAllPulseAllPulseAllPulseAll di MonitorMonitorMonitorMonitorMonitor. Quandouna struttura SyncLockSyncLockSyncLockSyncLockSyncLock termina, Visual Basic libera il blocco posto sull’oggetto identificatoda riferimentoOggetto. Per maggiori informazioni su SyncLockSyncLockSyncLockSyncLockSyncLock, consultate il Paragrafo 1.7.

Se un thread determina che non può svolgere il suo compito sull’oggetto bloccato, puòchiamare il metodo WaitWaitWaitWaitWait di MonitorMonitorMonitorMonitorMonitor, passando come argomento l’oggetto per il quale ilthread resterà in attesa finché potrà svolgere il suo compito. Chiamando il metodo WaitWaitWaitWaitWait daun thread si libera il blocco che il thread ha sull’oggetto che il metodo WaitWaitWaitWaitWait riceve comeargomento. Il thread che chiama il metodo WaitWaitWaitWaitWait passa nello stato WaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoin per l’og-getto. Un thread nello stato WaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoin per un oggetto lascia questo stato quando unaltro thread chiama il metodo PulsePulsePulsePulsePulse o PulseAllPulseAllPulseAllPulseAllPulseAll di MonitorMonitorMonitorMonitorMonitor con tale oggetto come argo-mento. Il metodo PulsePulsePulsePulsePulse passa il primo thread in attesa dallo stato WaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoin allo statoStartedStartedStartedStartedStarted. Il metodo PulseAllPulseAllPulseAllPulseAllPulseAll passa tutti i thread dallo stato WaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoin allo statoStartedStartedStartedStartedStarted. La transizione allo stato StartedStartedStartedStartedStarted consente al thread (o ai thread) di prepararsi acontinuare l’esecuzione.

MULTITHREADING 11

C’è una differenza fra i thread in attesa di porre il blocco su un oggetto e i thread che sonoin attesa nello stato WaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoin per un oggetto. I thread in attesa nello stato WaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoinchiamano il metodo WaitWaitWaitWaitWait di MonitorMonitorMonitorMonitorMonitor con l’oggetto come argomento. I thread che sono inattesa di porre il blocco, invece, passano nello stato BlockedBlockedBlockedBlockedBlocked e restano qui finché non saràliberato il blocco dell’oggetto; successivamente, uno dei thread BlockedBlockedBlockedBlockedBlocked potrà applicare ilblocco all’oggetto. I metodi EnterEnterEnterEnterEnter, ExitExitExitExitExit, WaitWaitWaitWaitWait, PulsePulsePulsePulsePulse e PulseAllPulseAllPulseAllPulseAllPulseAll di MonitorMonitorMonitorMonitorMonitor ricevono comeargomento un riferimento a un oggetto (di solito, la parola chiave MeMeMeMeMe).

Errore tipico 1.3

Un thread nello stato WaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoin non può ritornare nello stato StartedStartedStartedStartedStarted per riprenderel’esecuzione finché un altro thread non chiamerà il metodo PulsePulsePulsePulsePulse o PulseAllPulseAllPulseAllPulseAllPulseAll di MonitorMonitorMonitorMonitorMonitorcon l’oggetto appropriato come argomento. Se questo non accade, il thread resterà in attesaper sempre, generando un deadlock.

Collaudo e messa a punto 1.3

Quando più thread manipolano un oggetto condiviso tramite i monitor, il programma-tore dovrà verificare che, se un thread chiama il metodo WaitWaitWaitWaitWait di MonitorMonitorMonitorMonitorMonitor per passare nellostato WaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoin, un altro thread dovrà chiamare il metodo PulsePulsePulsePulsePulse di MonitorMonitorMonitorMonitorMonitor perriportare il thread nello stato StartedStartedStartedStartedStarted. Se più thread sono in attesa per lo stesso oggettocondiviso, un altro thread può chiamare il metodo PulseAllPulseAllPulseAllPulseAllPulseAll di MonitorMonitorMonitorMonitorMonitor per garantireche tutti i thread abbiano un’altra opportunità di svolgere il loro compito.

Obiettivo efficienza 1.3

La sincronizzazione dei thread nei programmi multithread può rallentare la loro esecu-zione, a causa delle operazioni extra dei monitor e delle frequenti transizioni dei threadfra gli stati RunningRunningRunningRunningRunning, WaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoin e StartedStartedStartedStartedStarted, ma ne garantisce la correttezza.

1.5 Tread non sincronizzatiIn una relazione di tipo produttore/consumatore, un thread produttore chiama un metodo diproduzione per generare i dati e depositarli in un’area condivisa della memoria, chiamatabuffer; successivamente, un thread consumatore chiama un metodo di consumazione per leggerequesti dati. Se il produttore che è in attesa di inserire i nuovi dati nel buffer si accorge che ilconsumatore non ha letto i precedenti dati del buffer, il thread produttore deve chiamare ilmetodo WaitWaitWaitWaitWait; altrimenti, il consumatore non vedrà mai i dati precedenti e l’applicazioneperderà questi dati. Quando il thread consumatore legge i dati, deve chiamare il metodoPulsePulsePulsePulsePulse per consentire di procedere al produttore in attesa. Quando un thread consumatoretrova il buffer vuoto o si accorge di avere già letto i dati, deve chiamare il metodo WaitWaitWaitWaitWait;altrimenti, il consumatore potrebbe leggere “la spazzatura” nel buffer o potrebbe elaborare idati precedenti più di una volta. In entrambi i casi, si verifica un errore di logica nell’applica-zione. Quando un produttore inserisce nuovi dati nel buffer, deve chiamare il metodo PulsePulsePulsePulsePulseper consentire al thread consumatore di procedere.

Esaminiamo come possano verificarsi gli errori di logica se non sincronizziamo l’accessodi più thread che manipolano i dati condivisi. Per esempio, supponiamo di avere una relazioneproduttore/consumatore nella quale un thread produttore scrive una sequenza di numeri (1,2, 3, 4) in un buffer condiviso (una locazione di memoria condivisa fra più thread). Il threadconsumatore legge questi dati dal buffer e li visualizza. In questo esempio, visualizzeremo ciò

12 CAPITOLO 1

che il produttore scrive (produce) e ciò che il consumatore legge (consuma). La Figura 1.8mostra un produttore e un consumatore che accedono a una singola cella condivisa di me-moria (la variabile mBuffermBuffermBuffermBuffermBuffer di tipo IntegerIntegerIntegerIntegerInteger), senza alcuna sincronizzazione. I thread cosumatoree produttore possono accedere a questa singola cella: il produttore per scrivere, il consumato-re per leggere. Vorremmo che ogni valore che il produttore scrive nella cella sia letto una solavolta dal consumatore. Poiché i thread non sono sincronizzati, i dati potrebbero perdersi se ilproduttore inserisse nuovi dati in memoria prima che il consumatore possa consumare quelliprecedenti; inoltre, i dati potrebbero essere erroneamente duplicati, se il consumatore legges-se una seconda volta i dati prima che il produttore possa generare quelli nuovi.

Per mostrare tutte queste possibilità, il thread consumatore di questo esempio calcola iltotale di tutti i valori che legge. Il thread produttore produce valori da 1 a 4; se il consuma-tore è in grado di leggere una sola volta ogni valore prodotto, il totale dovrebbe essere 10;tuttavia, se provate a eseguire più volte questo programma, vedrete che è raro ottenere questototale. I thread produttore e consumatore dell’esempio dormono per periodi di tempo ca-suali (massimo 3 secondi) durante lo svolgimento dei loro compiti. Quindi non sappiamocon esattezza quando il thread produttore scriverà un nuovo valore né quando il threadconsumatore leggerà un valore.

Il programma è formato dal modulo modSharedCellmodSharedCellmodSharedCellmodSharedCellmodSharedCell (Figura 1.8) e da tre classi:CHoldIntegerUnsynchronizedCHoldIntegerUnsynchronizedCHoldIntegerUnsynchronizedCHoldIntegerUnsynchronizedCHoldIntegerUnsynchronized (Figura 1.5), CProducerCProducerCProducerCProducerCProducer (Figura 1.6) e CConsumerCConsumerCConsumerCConsumerCConsumer (Figura 1.7).La classe CHoldIntegerUnsynchronizedCHoldIntegerUnsynchronizedCHoldIntegerUnsynchronizedCHoldIntegerUnsynchronizedCHoldIntegerUnsynchronized (Figura 1.5) è formata dalla variabile di istanzamBuffermBuffermBuffermBuffermBuffer (riga 9) e dalla proprietà BufferBufferBufferBufferBuffer (righe 12-28), che include i metodi di accesso GetGetGetGetGete SetSetSetSetSet. Questi metodi non sincronizzano l’accesso alla variabile di istanza mBuffermBuffermBuffermBuffermBuffer. Notateche ciascuno di questi metodi usa la proprietà condivisa CurrentThreadCurrentThreadCurrentThreadCurrentThreadCurrentThread della classe ThreadThreadThreadThreadThreadper ottenere un riferimento al thread correntemente in esecuzione e la proprietà NameNameNameNameName diquesto thread per ottenere il nome del thread.

1 ‘ File: HoldIntegerUnsynchronized.vb 2 ‘ Definizione di un buffer condiviso senza sincronizzazione 3 4 Imports System.Threading 5 6 Public Class CHoldIntegerUnsynchronized 7 8 ‘ buffer condiviso dai thread produttore e consumatore 9 Private mBuffer As Integer = -11011 ‘ la proprietà Buffer12 Property Buffer() As Integer1314 Get15 Console.WriteLine(Thread.CurrentThread.Name & _16 “ reads “ & mBuffer)1718 Return mBuffer19 End Get2021 Set(ByVal Value As Integer)22 Console.WriteLine(Thread.CurrentThread.Name & _23 “ writes “ & Value)

MULTITHREADING 13

2425 mBuffer = Value26 End Set2728 End Property ‘ Buffer2930 End Class ‘ CHoldIntegerUnsynchronized

Figura 1.5 Il buffer condiviso senza meccanismi di sincronizzazione

La classe CProducerCProducerCProducerCProducerCProducer (Figura 1.6) è formata dalle variabili di istanza sharedLocationsharedLocationsharedLocationsharedLocationsharedLocation (riga 8)e randomSleepTimerandomSleepTimerandomSleepTimerandomSleepTimerandomSleepTime (riga 9), da un costruttore (righe 12-17) per inizializzare le variabili di istanza eda un metodo ProduceProduceProduceProduceProduce (righe 20-33). Il costruttore inizializza la variabile di istanza sharedLocationsharedLocationsharedLocationsharedLocationsharedLocationin modo che faccia riferimento all’oggetto CHoldIntegerUnsynchronizedCHoldIntegerUnsynchronizedCHoldIntegerUnsynchronizedCHoldIntegerUnsynchronizedCHoldIntegerUnsynchronized ricevuto dal metodoMainMainMainMainMain. Il thread produttore in questo programma esegue i compiti specificati nel metodo ProduceProduceProduceProduceProducedella classe CProducerCProducerCProducerCProducerCProducer. Il metodo ProduceProduceProduceProduceProduce contiene una struttura ForForForForFor (righe 25-28) che eseguequattro iterazioni di un ciclo. In ogni iterazione, il metodo SleepSleepSleepSleepSleep di ThreadThreadThreadThreadThread pone il threadproduttore nello stato WaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoin per un intervallo di tempo casuale compreso fra 0 e 3secondi (riga 26). Quando il thread si sveglia, la riga 27 assegna il valore della variabile di controllocountcountcountcountcount alla proprietà BufferBufferBufferBufferBuffer dell’oggetto CHoldIntegerUnsynchronizedCHoldIntegerUnsynchronizedCHoldIntegerUnsynchronizedCHoldIntegerUnsynchronizedCHoldIntegerUnsynchronized, che fa sì che il metodoSetSetSetSetSet di CHoldIntegerUnsynchronizedCHoldIntegerUnsynchronizedCHoldIntegerUnsynchronizedCHoldIntegerUnsynchronizedCHoldIntegerUnsynchronized modifichi la variabile di istanza mBuffermBuffermBuffermBuffermBuffer dell’oggettoCHoldIntegerUnsynchronizedCHoldIntegerUnsynchronizedCHoldIntegerUnsynchronizedCHoldIntegerUnsynchronizedCHoldIntegerUnsynchronized. Alla fine del ciclo, le righe 30-32 visualizzano una riga di testonella finestra di comando per indicare che il thread ha finito di produrre dati. Il metodo ProduceProduceProduceProduceProducetermina ponendo il thread produttore nello stato StoppedStoppedStoppedStoppedStopped.

1 ‘ File: Producer.vb 2 ‘ Genera numeri interi da 1 a 4 e li pone 3 ‘ in un buffer non sincronizzato 4 5 Imports System.Threading 6 7 Public Class CProducer 8 Private sharedLocation As CHoldIntegerUnsynchronized 9 Private randomSleepTime As Random1011 ‘ costruttore12 Public Sub New(ByVal sharedObject As _13 CHoldIntegerUnsynchronized, ByVal randomObject As Random)1415 sharedLocation = sharedObject16 randomSleepTime = randomObject17 End Sub ‘ New1819 ‘ registra i valori 1-4 nell’oggetto sharedLocation20 Public Sub Produce()21 Dim count As Integer2223 ‘ dorme per un periodo di tempo casuale fino a 3 secondi24 ‘ imposta la proprietà Buffer di sharedLocation25 For count = 1 To 426 Thread.Sleep(randomSleepTime.Next(3000))

Figura 1.6 Il produttore pone i valori interi in un buffer non sincronizzato (continua)

14 CAPITOLO 1

27 sharedLocation.Buffer = count28 Next2930 Console.WriteLine(Thread.CurrentThread.Name & _31 “ done producing.” & vbCrLf & “Terminating “ & _32 Thread.CurrentThread.Name & “.”)33 End Sub ‘ Produce3435 End Class ‘ CProducer

Figura 1.6 Il produttore pone i valori interi in un buffer non sincronizzato

La classe CConsumerCConsumerCConsumerCConsumerCConsumer (Figura 1.7) è formata dalle variabili di istanza sharedLocationsharedLocationsharedLocationsharedLocationsharedLocation(riga 7) e randomSleepTimerandomSleepTimerandomSleepTimerandomSleepTimerandomSleepTime (line 8), da un costruttore (righe 11-16) per inizializzare levariabili di istanza e da un metodo ConsumeConsumeConsumeConsumeConsume (righe 19-32). Il costruttore inizializzasharedLocationsharedLocationsharedLocationsharedLocationsharedLocation in modo che faccia riferimento all’oggetto CHoldIntegerUnsynchronizedCHoldIntegerUnsynchronizedCHoldIntegerUnsynchronizedCHoldIntegerUnsynchronizedCHoldIntegerUnsynchronizedricevuto dal metodo MainMainMainMainMain come argomento sharedObjectsharedObjectsharedObjectsharedObjectsharedObject. Il thread consumatore in questoprogramma svolge i compiti specificati nel metodo ConsumeConsumeConsumeConsumeConsume della classe CConsumerCConsumerCConsumerCConsumerCConsumer. Il me-todo contiene una struttura ForForForForFor (righe 24-27) che esegue quattro iterazioni di un ciclo. Inogni iterazione, il metodo SleepSleepSleepSleepSleep di ThreadThreadThreadThreadThread pone il thread consumatore nello statoWaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoin per un intervallo di tempo casuale compreso fra 0 e 3 secondi (riga 25). Lariga 26 acquisisce il valore della proprietà BufferBufferBufferBufferBuffer dell’oggetto CHoldIntegerUnsynchronizedCHoldIntegerUnsynchronizedCHoldIntegerUnsynchronizedCHoldIntegerUnsynchronizedCHoldIntegerUnsynchronizede aggiunge il valore alla variabile sumsumsumsumsum. Alla fine del ciclo, le righe 29-31 visualizzano una rigadi testo nella finestra di comando per indicare la somma di tutti i valori letti. Il metodoConsumeConsumeConsumeConsumeConsume termina ponendo il thread consumatore nello stato StoppedStoppedStoppedStoppedStopped.

1 ‘ File: Consumer.vb 2 ‘ Legge 4 valori interi nel buffer non sincronizzato 3 4 Imports System.Threading 5 6 Public Class CConsumer 7 Private sharedLocation As CHoldIntegerUnsynchronized 8 Private randomSleepTime As Random 910 ‘ costruttore11 Public Sub New(ByVal sharedObject As _12 CHoldIntegerUnsynchronized, ByVal randomObject As Random)1314 sharedLocation = sharedObject15 randomSleepTime = randomObject16 End Sub ‘ New1718 ‘ registra i valori 1-4 nell’oggetto sharedLocation19 Public Sub Consume()20 Dim count, sum As Integer2122 ‘ dorme per un periodo di tempo casuale (max 3 sec); aggiunge23 ‘ a sum il valore della proprietà Buffer di sharedLocation24 For count = 1 To 425 Thread.Sleep(randomSleepTime.Next(3000))26 sum += sharedLocation.Buffer

MULTITHREADING 15

27 Next2829 Console.WriteLine(Thread.CurrentThread.Name & _30 “ read values totaling: “ & sum & “.” & vbCrLf & _31 “Terminating “ & Thread.CurrentThread.Name & “.”)32 End Sub ‘ Consume3334 End Class ‘ CConsumer

Figura 1.7 Il thread consumatore legge i valori nel buffer non sincronizzato

Notate che in questo esempio utilizziamo il metodo SleepSleepSleepSleepSleep per mettere in risalto il fattoche, nelle applicazioni multithreaded, non è chiaro quando un thread svolgerà il suo compitoe quanto tempo occorrerà per svolgerlo. Di norma, è il sistema operativo del computer chegestisce questi problemi. Nel programma in esame, i compiti del nostro thread sono semplici:il produttore deve eseguire un ciclo quattro volte con un’istruzione di assegnazione; il consu-matore deve eseguire un ciclo quattro volte e aggiungere un valore alla variabile sumsumsumsumsum. Seomettiamo la chiamata del metodo SleepSleepSleepSleepSleep e il produttore viene eseguito per primo, il suocompito sarà completato prima che il consumatore possa avere una possibilità di essere ese-guito. Nella stessa situazione, se il consumatore viene eseguito per primo, legge quattro volteil valore –1–1–1–1–1 e termina prima che il produtttore possa generare un valore.

Il metodo MainMainMainMainMain del modulo modSharedCellmodSharedCellmodSharedCellmodSharedCellmodSharedCell (Figura 14.8) instanzia un oggetto condi-viso CHoldIntegerUnsynchronizedCHoldIntegerUnsynchronizedCHoldIntegerUnsynchronizedCHoldIntegerUnsynchronizedCHoldIntegerUnsynchronized (riga 14) e un oggetto RandomRandomRandomRandomRandom (riga 17) per generarevalori casuali del periodo di SleepingSleepingSleepingSleepingSleeping; poi passa questi oggetti come argomenti ai costruttoridegli oggetti delle classi CProducerCProducerCProducerCProducerCProducer (producerproducerproducerproducerproducer, riga 20) e CConsumerCConsumerCConsumerCConsumerCConsumer (consumerconsumerconsumerconsumerconsumer, riga 21).L’oggetto CHoldIntegerUnsynchronizedCHoldIntegerUnsynchronizedCHoldIntegerUnsynchronizedCHoldIntegerUnsynchronizedCHoldIntegerUnsynchronized contiene i dati che saranno condivisi fra i threadproduttore e consumatore. La riga 25 crea producerThreadproducerThreadproducerThreadproducerThreadproducerThread. Il delegato ThreadStartThreadStartThreadStartThreadStartThreadStart perproducerThreadproducerThreadproducerThreadproducerThreadproducerThread specifica che il thread eseguirà il metodo ProduceProduceProduceProduceProduce dell’oggetto producerproducerproducerproducerproducer.La riga 26 crea consumerThreadconsumerThreadconsumerThreadconsumerThreadconsumerThread. Il delegato ThreadStartThreadStartThreadStartThreadStartThreadStart per consumerThreadconsumerThreadconsumerThreadconsumerThreadconsumerThread specificache il thread eseguirà il metodo ConsumeConsumeConsumeConsumeConsume dell’oggetto consumerconsumerconsumerconsumerconsumer. Le righe 29-30 chiamano ithread producerThreadproducerThreadproducerThreadproducerThreadproducerThread e consumerThreadconsumerThreadconsumerThreadconsumerThreadconsumerThread. Infine, le righe 33-34 pongono i due threadnello stato StartedStartedStartedStartedStarted chiamando il metodo StartStartStartStartStart; a questo punto termina il thread MainMainMainMainMain.

Il nostro obiettivo è ottenere che ogni valore prodotto dall’oggetto CProducerCProducerCProducerCProducerCProducer sia con-sumato una sola volta dall’oggetto CConsumerCConsumerCConsumerCConsumerCConsumer. Tuttavia, se analizziamo il primo output dellaFigura 1.8, notiamo che il consumatore legge un valore (–1–1–1–1–1) prima che il produttore pongaun valore nel buffer condiviso; inoltre il valore 11111 è letto tre volte. Il consumatore terminal’esecuzione prima che il produttore abbia l’opportunità di generare i valori 2, 3 e 4. Neconsegue che questi tre valori vengono perduti. Nel secondo output, notiamo che manca ilvalore 11111, perché i valori 11111 e 22222 vengono generati prima che il thread consumatore possa leggereil valore 11111; inoltre il valore 44444 viene letto (consumato) due volte. L’ultimo output dimostra cheè possibile, con un po’ di fortuna, ottenere il risultato appropriato: ogni valore generato dalthread produttore viene letto una sola volta dal thread consumatore. Questo esempio dimo-stra chiaramente che l’accesso ai dati condivisi da più thread deve essere attentamente con-trollato, altrimenti un programma potrebbe produrre risultati sbagliati.

Per risolvere i problemi del precedente esempio (perdita e duplicazione dei dati), sincro-nizziamo l’accesso dei thread produttore e consumatore (Figura 1.9) ai dati condivisi utiliz-zando i metodi EnterEnterEnterEnterEnter, WaitWaitWaitWaitWait, PulsePulsePulsePulsePulse e ExitExitExitExitExit di MonitorMonitorMonitorMonitorMonitor. Quando un thread usa l’accessosincronizzato a un oggetto condiviso, l’oggetto viene bloccato e nessun altro thread puòporre il blocco (lock) a quest’oggetto, finché il primo thread non libererà l’oggetto.

16 CAPITOLO 1

1 ‘ File: SharedCell.vb 2 ‘ Crea i thread produttore e consumatore 3 ‘ che interagiscono fra di loro attraverso 4 ‘ l’oggetto comune CHoldIntegerUnsynchronized 5 6 Imports System.Threading 7 8 Module modSharedCell 910 ‘ crea il produttore e il consumatore11 Sub Main()1213 ‘ crea l’oggetto condiviso dai thread14 Dim holdInteger As New CHoldIntegerUnsynchronized()1516 ‘ oggetto casuale utilizzato dai thread17 Dim randomObject As New Random()1819 ‘ crea gli oggetti CProducer e CConsumer20 Dim producer As New CProducer(holdInteger, randomObject)21 Dim consumer As New CConsumer(holdInteger, randomObject)2223 ‘ crea i thread per il produttore e il consumatore24 ‘ imposta i delegati per i thread25 Dim producerThread As New Thread(AddressOf producer.Produce)26 Dim consumerThread As New Thread(AddressOf consumer.Consume)2728 ‘ assegna i nomi ai thread29 producerThread.Name = “Producer”30 consumerThread.Name = “Consumer”3132 ‘ inizia i thread33 producerThread.Start()34 consumerThread.Start()35 End Sub ‘ Main3637 End Module ‘ modSharedCell

Consumer reads –1Producer writes 1Consumer reads 1Consumer reads 1Consumer reads 1Consumer read values totaling: 2.Terminating Consumer.Producer writes 2Producer writes 3Producer writes 4Producer done producing.Terminating Producer.

MULTITHREADING 17

Producer writes 1Producer writes 2Consumer reads 2Producer writes 3Consumer reads 3Producer writes 4Producer done producing.Terminating Producer.Consumer reads 4Consumer reads 4Consumer read values totaling: 13.Terminating Consumer.

Producer writes 1Consumer reads 1Producer writes 2Consumer reads 2Producer writes 3Consumer reads 3Producer writes 4Producer done producing.Terminating Producer.Consumer reads 4Consumer read values totaling: 10.Terminating Consumer.

Figura 1.8 I thread produttore e consumatore accedono a un oggetto condivisosenza sincronizzazione

1.6 Thread sincronizzatiLa Figura 1.12 mostra un produttore e un consumatore che accedono in modo sincronizzatoa una cella di memoria condivisa. Il consumatore legge un valore soltanto dopo che il pro-duttore lo ha generato; inoltre, il produttore genera un nuovo valore soltanto dopo che ilconsumatore ha letto il valore precedentemente generato. Le classi CProducerCProducerCProducerCProducerCProducer (Figura 1.10),CConsumerCConsumerCConsumerCConsumerCConsumer (Figura 1.11) e il modulo modSharedCellmodSharedCellmodSharedCellmodSharedCellmodSharedCell (Figura 1.12) sono identici, rispetti-vamente, a quelli delle Figure 1.6, 1.7 e 1.8, tranne il fatto che usano la nuova classeCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronized (Figura 1.9). Notate che questo esempio applica la sincroniz-zazione con i metodi EnterEnterEnterEnterEnter e ExitExitExitExitExit della classe MonitorMonitorMonitorMonitorMonitor. Nel prossimo esempio, illustreremogli stessi concetti utilizzando SyncLockSyncLockSyncLockSyncLockSyncLock.

1 ‘ File: HoldIntegerSynchronized.vb 2 ‘ Sincronizza l’accesso a valori di tipo Integer 3 4 Imports System.Threading 5 6 Public Class CHoldIntegerSynchronized 7

Figura 1.9 Il buffer condiviso sincronizzato (continua)

18 CAPITOLO 1

8 ‘ buffer condiviso dai thread produttore e consumatore 9 Private mBuffer As Integer = –1 10 11 ‘ occupiedBufferCount conta i buffer occupati 12 Private occupiedBufferCount As Integer 13 14 Public Property Buffer() As Integer 15 16 Get 17 18 ‘ ottiene il blocco su questo oggetto 19 Monitor.Enter(Me) 20 21 ‘ se non ci sono dati da leggere, il thread 22 ‘ viene posto nello stato WaitSleepJoin 23 If occupiedBufferCount = 0 Then 24 Console.WriteLine(Thread.CurrentThread.Name & _ 25 “ tries to read.”) 26 27 DisplayState(“Buffer empty. “ & _ 28 Thread.CurrentThread.Name & “ waits.”) 29 30 Monitor.Wait(Me) 31 End If 32 33 ‘ indica che il produttore può registrare un altro valore 34 ‘ perché il consumatore ha appena letto il valore del buffer 35 occupiedBufferCount –= 1 36 37 DisplayState(Thread.CurrentThread.Name & “ reads “ & _ 38 mBuffer) 39 40 ‘ indica all’eventuale thread in attesa di 41 ‘ passare nello stato Started 42 Monitor.Pulse(Me) 43 44 ‘ Crea una copia del buffer prima di liberare il blocco 45 ‘ E’ possibile che al produttore possa essere assegnato 46 ‘ il processore subito dopo che il monitor 47 ‘ viene liberato e prima dell’istruzione Return 48 ‘ In questo caso, il produttore dovrebbe assegnare 49 ‘ un nuovo valore al buffer prima che l’istruzione 50 ‘ Return restituisca il valore al consumatore. 51 ‘ Il consumatore riceverà il nuovo valore. 52 ‘ Creando una copia del buffer e restituendo 53 ‘ la copia, è più facile garantire che il 54 ‘ consumatore riceva il valore appropriato 55 Dim bufferCopy As Integer = mBuffer 56 57 ‘ libera il blocco dell’oggetto

MULTITHREADING 19

58 Monitor.Exit(Me) 59 60 Return bufferCopy 61 End Get 62 63 Set(ByVal Value As Integer) 64 65 ‘ acquisisce il blocco per l’oggetto 66 Monitor.Enter(Me) 67 68 ‘ se non ci sono locazioni vuote, 69 ‘ il thread passa nello stato WaitSleepJoin 70 If occupiedBufferCount = 1 Then 71 Console.WriteLine(Thread.CurrentThread.Name & _ 72 “ tries to write.”) 73 74 DisplayState(“Buffer full. “ & _ 75 Thread.CurrentThread.Name & “ waits.”) 76 77 Monitor.Wait(Me) 78 End If 79 80 ‘ imposta il nuovo valore del buffer 81 mBuffer = Value 82 83 ‘ indica al produttore che non può registrare un altro 84 ‘ valore, finché il consumatore non legge il buffer 85 occupiedBufferCount += 1 86 87 DisplayState(Thread.CurrentThread.Name & “ writes “ & _ 88 mBuffer) 89 90 ‘ indica all’eventuale thread in attesa di 91 ‘ passare nello stato Started 92 Monitor.Pulse(Me) 93 94 ‘ libera il blocco dell’oggetto 95 Monitor.Exit(Me) 96 End Set 97 98 End Property ‘ Buffer 99100 Public Sub DisplayState(ByVal operation As String)101 Console.WriteLine(“{0,–35}{1,–9}{2}” & vbCrLf, _102 operation, mBuffer, occupiedBufferCount)103 End Sub ‘ DisplayState104105 End Class ‘ CHoldIntegerSynchronized

Figura 1.9 Il buffer condiviso sincronizzato

20 CAPITOLO 1

1 ‘ File: Producer.vb 2 ‘ Produce 4 interi e li pone nel buffer sincronizzato 3 4 Imports System.Threading 5 6 Public Class CProducer 7 Private sharedLocation As CHoldIntegerSynchronized 8 Private randomSleepTime As Random 910 ‘ costruttore11 Public Sub New(ByVal sharedObject As _12 CHoldIntegerSynchronized, ByVal randomObject As Random)1314 sharedLocation = sharedObject15 randomSleepTime = randomObject16 End Sub ‘ New1718 ‘ registra i valori 1-4 nell’oggetto sharedLocation19 Public Sub Produce()20 Dim count As Integer2122 ‘ dorme per un periodo di tempo casuale fino a 3 secondi23 ‘ imposta la proprietà Buffer di sharedLocation24 For count = 1 To 425 Thread.Sleep(randomSleepTime.Next(3000))26 sharedLocation.Buffer = count27 Next2829 Console.WriteLine(Thread.CurrentThread.Name & _30 “ done producing. “ & vbCrLf & “Terminating “ & _31 Thread.CurrentThread.Name & “.” & vbCrLf)32 End Sub ‘ Produce3334 End Class ‘ CProducer

Figura 1.10 Il produttore pone i valori interi nel buffer condiviso sincronizzato

1 ‘ File: Consumer.vb 2 ‘ Legge 4 valori interi nel buffer sincronizzato 3 4 Imports System.Threading 5 6 Public Class CConsumer 7 Private sharedLocation As CHoldIntegerSynchronized 8 Private randomSleepTime As Random 910 ‘ costruttore11 Public Sub New(ByVal sharedObject As _12 CHoldIntegerSynchronized, ByVal randomObject As Random)1314 sharedLocation = sharedObject15 randomSleepTime = randomObject16 End Sub ‘ New

MULTITHREADING 21

1718 ‘ legge quattro volte il valore di sharedLocation19 Public Sub Consume()20 Dim count, sum As Integer2122 ‘ dorme per un periodo di tempo casuale (max 3 sec); aggiunge23 ‘ a sum il valore della proprietà Buffer di sharedLocation24 For count = 1 To 425 Thread.Sleep(randomSleepTime.Next(3000))26 sum += sharedLocation.Buffer27 Next2829 Console.WriteLine(Thread.CurrentThread.Name & _30 “ read values totaling: “ & sum & “.” & vbCrLf & _31 “Terminating “ & Thread.CurrentThread.Name & “.” & _32 vbCrLf)33 End Sub ‘ Consume3435 End Class ‘ CConsumer

Figura 1.11 Il consumer legge i valori interi nel buffer condiviso sincronizzato

1 ‘ File: SharedCell.vb 2 ‘ Crea i thread produttore e consumatore 3 4 Imports System.Threading 5 6 Module modSharedCell 7 8 Sub Main() 910 ‘ crea l’oggetto condiviso utilizzato dai thread11 Dim holdInteger As New CHoldIntegerSynchronized()1213 ‘ oggetto causale utilizzato dai thread14 Dim randomObject As New Random()1516 ‘ crea gli oggetti CProducer e CConsumer17 Dim producer As New CProducer(holdInteger, randomObject)18 Dim consumer As New CConsumer(holdInteger, randomObject)1920 Console.WriteLine(“{0,–35}{1,–9}{2}” & vbCrLf, _21 “Operation”, “Buffer”, “Occupied Count”)2223 holdInteger.DisplayState(“Initial State”)2425 ‘ crea i thread per il produttore e il consumatore26 ‘ imposta i delegati per i thread27 Dim producerThread As _28 New Thread(AddressOf producer.Produce)

Figura 1.12 I thread produttore e consumatore accedono in modo sincronizzatoa un oggetto condiviso (continua)

22 CAPITOLO 1

2930 Dim consumerThread As _31 New Thread(AddressOf consumer.Consume)3233 ‘ assegna i nomi ai thread34 producerThread.Name = “Producer”35 consumerThread.Name = “Consumer”3637 ‘ avvia i thread38 producerThread.Start()39 consumerThread.Start()40 End Sub ‘ Main4142 End Module ‘ modSharedCell

Operation Buffer Occupied CountInitial state –1 0Producer writes 1 1 1Consumer reads 1 1 0Consumer tries to read.Buffer empty. Consumer waits. 1 0Producer writes 2 2 1Consumer reads 2 2 0Producer writes 3 3 1Producer tries to write.Buffer full. Producer waits. 3 1Consumer reads 3 3 0Producer writes 4 4 1Producer done producing.Terminating Producer.Consumer reads 4 4 0Consumer read values totaling: 10.Terminating Consumer.

Operation Buffer Occupied CountInitial state –1 0Consumer tries to read.Buffer empty. Consumer waits. –1 0Producer writes 1 1 1Consumer reads 1 1 0Producer writes 2 2 1Consumer reads 2 2 0Producer writes 3 3 1Producer tries to write.Buffer full. Producer waits. 3 1Consumer reads 3 3 0Producer writes 4 4 1Producer done producing.Terminating Producer.Consumer reads 4 4 0Consumer read values totaling: 10.Terminating Consumer.

MULTITHREADING 23

Operation Buffer Occupied CountInitial state –1 0Producer writes 1 1 1Consumer reads 1 1 0Producer writes 2 2 1Consumer reads 2 2 0Producer writes 3 3 1Consumer reads 3 3 0Producer writes 4 4 1Producer done producing.Terminating Producer.Consumer reads 4 4 0Consumer read values totaling: 10.Terminating Consumer.

Figura 1.12 I thread produttore e consumatore accedono in modo sincronizzatoa un oggetto condiviso

La classe CHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronized (Figura 1.9) contiene due variabili di istanza:mBuffermBuffermBuffermBuffermBuffer (riga 9) e occupiedBufferCountoccupiedBufferCountoccupiedBufferCountoccupiedBufferCountoccupiedBufferCount (riga 12). GetGetGetGetGet (righe 16-61) e SetSetSetSetSet (righe 63-96)adesso usano i metodi della classe MonitorMonitorMonitorMonitorMonitor per sincronizzare l’accesso alla proprietà BufferBufferBufferBufferBuffer.Quindi, ogni oggetto della classe CHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronized ha un SyncBlockSyncBlockSyncBlockSyncBlockSyncBlock per man-tenere la sincronizzazione. La variabile di istanza occupiedBufferCountoccupiedBufferCountoccupiedBufferCountoccupiedBufferCountoccupiedBufferCount è detta variabile dicondizione: i metodi di accesso della proprietà BufferBufferBufferBufferBuffer usano questo valore IntegerIntegerIntegerIntegerInteger nellecondizioni per determinare se tocca al produttore o al consumatore svolgere un compito. SeoccupiedBufferCountoccupiedBufferCountoccupiedBufferCountoccupiedBufferCountoccupiedBufferCount è 0, il metodo SetSetSetSetSet della proprietà BufferBufferBufferBufferBuffer può inserire un valorenella variabile mBuffermBuffermBuffermBuffermBuffer, in quanto la variabile non contiene informazioni; questo significache il metodo GetGetGetGetGet della proprietà BufferBufferBufferBufferBuffer non può leggere il valore di mBuffermBuffermBuffermBuffermBuffer. SeoccupiedBufferCountoccupiedBufferCountoccupiedBufferCountoccupiedBufferCountoccupiedBufferCount è 1, il metodo GetGetGetGetGet della proprietà BufferBufferBufferBufferBuffer può leggere il valore dellavariabile mBuffermBuffermBuffermBuffermBuffer, in quanto la variabile contiene informazioni; in questo caso, il metodoSetSetSetSetSet della proprietà BufferBufferBufferBufferBuffer non può registrare un valore in mBuffermBuffermBuffermBuffermBuffer.

Analogamente alla Figura 1.6, il thread produttore (Figura 1.10) svolge i compiti speci-ficati nel metodo ProduceProduceProduceProduceProduce dell’oggetto producerproducerproducerproducerproducer. Quando la riga 26 imposta il valore diCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronized della proprietà BufferBufferBufferBufferBuffer, il thread produttore chiama il metodoSetSetSetSetSet (Figura 1.9, righe 63-96). La riga 66 chiama il metodo EnterEnterEnterEnterEnter di MonitorMonitorMonitorMonitorMonitor per bloccarel’oggetto CHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronized. La struttura IfIfIfIfIf (righe 70-78) determina seoccupiedBufferCountoccupiedBufferCountoccupiedBufferCountoccupiedBufferCountoccupiedBufferCount vale 1. Se questa condizione è vera (TrueTrueTrueTrueTrue), le righe 71-72 generanoun messaggio che indica che il thread produttore ha tentato di scrivere un valore e le righe74-75 chiamano il metodo DisplayStateDisplayStateDisplayStateDisplayStateDisplayState (righe 100-103) per generare un altro messaggioche indica che il buffer è pieno e che il thread produttore è in attesa. La riga 77 chiama ilmetodo WaitWaitWaitWaitWait di MonitorMonitorMonitorMonitorMonitor per porre il thread che ha effettuato la chiamata (ovvero il produt-tore) nello stato WaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoin per l’oggetto CHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronized e libera l’og-getto. Lo stato WaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoin di un oggetto è mantenuto da SyncBlockSyncBlockSyncBlockSyncBlockSyncBlock. Adesso un altrothread può chiamare un metodo di accesso della proprietà BufferBufferBufferBufferBuffer dell’oggettoCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronized.

Il thread produttore resta nello stato WaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoin finché il programma non gli indicache può procedere; a questo punto il thread ritorna nello stato StartedStartedStartedStartedStarted e attende che gli vengaassegnato un processore. Quando il thread ritorna nello stato RunningRunningRunningRunningRunning, il thread riprende

24 CAPITOLO 1

implicitamente il blocco sull’oggetto CHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronized e il metodo SetSetSetSetSet riprendel’esecuzione con l’istruzione che segue WaitWaitWaitWaitWait. La riga 81 assegna ValueValueValueValueValue a mBuffermBuffermBuffermBuffermBuffer. La riga 85incrementa occupiedBufferCountoccupiedBufferCountoccupiedBufferCountoccupiedBufferCountoccupiedBufferCount per indicare che il buffer condiviso adesso contiene unvalore (ovvero un consumatore può leggere il valore, ma un produttore non può registrare unvalore nel buffer). Le righe 87-88 chiamano il metodo DisplayStateDisplayStateDisplayStateDisplayStateDisplayState per visualizzare una rigadi testo nella finestra di comando che indica che il produttore sta scrivendo un nuovo valore inmBuffermBuffermBuffermBuffermBuffer . La riga 92 chiama il metodo PulsePulsePulsePulsePulse di MonitorMonitorMonitorMonitorMonitor con l’oggettoCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronized come argomento. Se ci sono thread in attesa nel SyncBlockSyncBlockSyncBlockSyncBlockSyncBlockdell’oggetto, il primo di questi thread entra nello stato StartedStartedStartedStartedStarted; questo thread può ritentare disvolgere il suo compito non appena gli viene assegnato un processore. Il metodo PulsePulsePulsePulsePulse restitu-isce immediatamente il controllo. La riga 95 chiama il metodo ExitExitExitExitExit di MonitorMonitorMonitorMonitorMonitor per liberarel’oggetto CHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronized e il metodo SetSetSetSetSet restituisce il controllo al suo caller.

Errore tipico 1.4

È un errore di logica non rilasciare il blocco di un oggetto quando non serve più. Questo impediscead altri thread che richiedono il blocco di acquisire il blocco e continuare a svolgere i loro compiti.Questi thread saranno forzati a mettersi in attesa (senza motivo, perché il blocco non è piùrichiesto). Questo stato di attesa può generare un deadlock e rinvii indefiniti.

I metodi GetGetGetGetGet e SetSetSetSetSet sono implementati in modo simile. Analogamente alla Figura 1.7, ilthread consumatore (Figura 1.11) svolge i compiti specificati nel metodo ConsumeConsumeConsumeConsumeConsume dell’og-getto consumerconsumerconsumerconsumerconsumer. Il thread consumatore acquisisce il valore della proprietà BufferBufferBufferBufferBuffer dell’oggettoCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronized (Figura 1.11, riga 26) chiamando il metodo GetGetGetGetGet (Figura 1.9,righe 16-61). Nella Figura 1.9, la riga 19 chiama il metodo EnterEnterEnterEnterEnter di MonitorMonitorMonitorMonitorMonitor per porre ilblocco sull’oggetto CHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronized. La struttura IfIfIfIfIf (righe 23-31) determinase occupiedBufferCountoccupiedBufferCountoccupiedBufferCountoccupiedBufferCountoccupiedBufferCount è 0. Se questa condizione è vera (TrueTrueTrueTrueTrue), le righe 24-25 generanoun messaggio che indica che il thread consumatore ha tentato di leggere un valore e le righe27-28 chiamano il metodo DisplayStateDisplayStateDisplayStateDisplayStateDisplayState per creare un altro messaggio che indica che ilbuffer è vuoto e il thread consumatore è in attesa. La riga 30 chiama il metodo WaitWaitWaitWaitWait diMonitorMonitorMonitorMonitorMonitor per porre il thread che ha effettuato la chiamata (il consumatore) nello statoWaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoin per l’oggetto CHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronized e toglie il blocco dall’oggetto.Adesso un altro thread può chiamare un metodo di accesso della proprietà BufferBufferBufferBufferBuffer dell’og-getto CHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronized.

Il thread consumatore resta nello stato WaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoin finché il programma gli indicaal thread che può procedere; a questo punto il thread ritorna nello stato StartedStartedStartedStartedStarted e attendeche gli venga assegnato un processore. Quando il thread ritorna nello stato RunningRunningRunningRunningRunning, il threadriprende implicitamente il blocco sull’oggetto CHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronized e il metodoGetGetGetGetGet continua la sua esecuzione con l’istruzione che segue WaitWaitWaitWaitWait. La riga 35 riduce il valore dioccupiedBufferCountoccupiedBufferCountoccupiedBufferCountoccupiedBufferCountoccupiedBufferCount per indicare che il buffer condiviso adesso è vuoto (ovvero un con-sumatore non può leggere il valore, ma un produttore può registrare un valore nel buffer). Lerighe 37-38 visualizzano una riga di testo nella finestra di comando che indica che il consu-matore sta leggendo; la riga 42 chiama il metodo PulsePulsePulsePulsePulse di MonitorMonitorMonitorMonitorMonitor con l’oggettoCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronized come argomento. Se ci sono thread in attesa nel SyncBlockSyncBlockSyncBlockSyncBlockSyncBlockdell’oggetto, il primo di questi thread entra nello stato StartedStartedStartedStartedStarted; questo thread può ritentaredi svolgere il suo compito non appena gli viene assegnato un processore. Il metodo PulsePulsePulsePulsePulserestituisce immediatamente il controllo. La riga 55 crea una copia di mBuffermBuffermBuffermBuffermBuffer prima diliberare l’oggetto. È possibile che al produttore venga assegnato il processore subito dopo ilrilascio del blocco (riga 58) e prima che venga eseguita l’istruzione ReturnReturnReturnReturnReturn (riga 60). In

MULTITHREADING 25

questo caso, il produttore dovrebbe assegnare un nuovo valore a mBuffermBuffermBuffermBuffermBuffer, prima che l’istru-zione ReturnReturnReturnReturnReturn restituisca il valore al consumatore. Quindi, il consumatore dovrebbe ricevereil nuovo valore. Creando e restituendo una copia di mBuffermBuffermBuffermBuffermBuffer, abbiamo la certezza che ilconsumatore riceva il valore appropriato. La riga 58 chiama il metodo ExitExitExitExitExit di MonitorMonitorMonitorMonitorMonitor perrilasciare il blocco dell’oggetto CHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronized; il metodo GetGetGetGetGet restituiscebufferCopybufferCopybufferCopybufferCopybufferCopy al suo caller.

Analizzate gli output alla fine della Figura 1.12. Notate che ogni valore IntegerIntegerIntegerIntegerInteger prodottoviene letto (consumato) una sola volta; nessun valore viene perduto e nessun valore vieneletto più di una volta. Questo si verifica perché il produttore e il consumatore possonosvolgere i compiti soltanto durante il loro turno. Il produttore deve iniziare per primo; ilconsumatore deve restare in attesa se il produttore non ha prodotto un valore dopo l’ultimovalore letto dal consumatore; il produttore deve restare in attesa se il consumatore non haancora letto l’ultimo valore generato dal produttore. Provate a eseguire questo programmapiù volte per verificare che ogni numero intero prodotto viene letto una sola volta. Notatenel primo e nel secondo output le righe che indicano quando il produttore e il consumatoredevono attendere per svolgere i loro rispettivi compiti. Il terzo output indica che il produttoree il consumatore sono stati in grado di svolgere i loro compiti senza alcuna attesa.

1.7 Buffer circolareIl programma della Figura 1.9 usa la sincronizzazione dei thread per garantire che i duethread accedano correttamente ai dati di un buffer condiviso; tuttavia, l’applicazione potrebbenon essere eseguita in modo ottimale. Se i due thread operano a velocità differenti, uno deithread potrebbe impiegare più del suo tempo di attesa. Per esempio, nella Figura 1.12 i duethread condividono lo stesso valore IntegerIntegerIntegerIntegerInteger. Il thread produttore non può produrre valoripiù velocemente di quelli che il consumatore è in grado di consumare, perché non ci sonoaltre locazioni di memoria in cui porre il successivo valore. Analogamente, il consumatorenon può consumare valori a una velocità maggiore di quella del produttore. Anche quandoi thread operano alla stessa velocità relativa in un determinato periodo di tempo, potrebberoperdere il sincronismo, costringendo un thread ad aspettare l’altro. Non possiamo fare ipotesisulle velocità relative dei thread asincroni. Troppi fattori interagiscono con il sistema opera-tivo, la rete, l’utente e altri componenti; queste interazioni possono portare i thread a operarea velocità differenti. In questo caso, i thread devono mettersi in attesa; quando i threadrestano in attesa, i programmi perdono efficienza e le applicazioni in rete subiscono notevoliritardi.

Per ridurre al minimo l’attesa dei thread che condividono le risorse e operano alla stessavelocità relativa, è possibile implementare un buffer circolare, che permette al produttore e alconsumatore di utilizzare dei buffer supplementari per svolgere i loro compiti. Supponiamoche il buffer sia implementato come un array. Il produttore e il consumatore iniziano dal primoelemento dell’array; quando raggiungono la fine dell’array, ritornano al primo elemento persvolgere il compito successivo. Se il produttore genera temporaneamente più valori di quelliche il consumatore riesce a consumare, il produttore può scrivere i valori addizionali in bufferextra (se ci sono celle disponibili). Questo consente al produttore di svolgere il suo compito,anche se il consumatore non è pronto a ricevere il valore correntemente prodotto. In modoanalogo, se il consumatore consuma più velocemente del produttore, può leggere i valori extradal buffer (se ci sono). Questo meccanismo consente al consumatore di svolgere il suo compito,anche quando il produttore non è pronto a generare nuovi valori.

26 CAPITOLO 1

È importante notare che il buffer circolare non sarebbe appropriato se produttore econsumatore operassero a velocità differenti. Se il consumatore operasse sempre più veloce-mente del produttore, sarebbe sufficiente un buffer con una locazione; utilizzare altre loca-zioni sarebbe uno spreco di memoria. Se il produttore operasse sempre più velocemente delconsumatore, occorrerebbe un buffer con un numero infinito di locazioni per assorbire laproduzione extra.

Per sfruttare correttamente un buffer circolare occorre definirlo con un numero di celleextra sufficienti a gestire la produzione “extra”. Se, in un certo periodo di tempo, determiniamoche il produttore genera fino a tre valori extra che il consumatore non riesce a consumare,possiamo definire un buffer con almeno tre celle per gestire la produzione extra. Il buffernon deve essere troppo piccolo, altrimenti costringiamo i thread a restare in attesa. D’altraparte, il buffer non può essere troppo grande per evitare sprechi di memoria.

Obiettivo efficienza 1.4

Anche quando si usa un buffer circolare, è possibile che un thread produttore riempia ilbuffer; questo forzerebbe il produttore a restare in attesa finché il consumatore non con-sumerà un valore per liberare un elemento del buffer. Analogamente, se il buffer è vuotoin un certo istante di tempo, il consumatore deve attendere che il produttore generi un altrovalore. Per sfruttare un buffer circolare occorre ottimizzare la sua dimensione, riducendoi tempi di attesa dei thread.

La Figura 1.16 illustra un produttore e un consumatore che accedono in modo sincro-nizzato a un buffer circolare (in questo caso, un array condiviso di tre celle). In questa varian-te della relazione produttore/consumatore, il consumatore consuma un valore soltanto quandol’array non è vuoto e il produttore genera un valore soltanto quando l’array non è pieno.Questo programma è implementato come applicazione Windows che invia il suo output aun TextBoxTextBoxTextBoxTextBoxTextBox. Le classi CProducerCProducerCProducerCProducerCProducer (Figura 1.14) e CConsumerCConsumerCConsumerCConsumerCConsumer (Figura 1.15) svolgono glistessi compiti, rispettivamente, delle Figure 1.10 e 1.11, con la differenza che i messaggivengono visualizzati nel TextBoxTextBoxTextBoxTextBoxTextBox della finestra dell’applicazione. Le istruzioni che hannocreato e avviato i thread nei metodi MainMainMainMainMain del modulo modSharedCellmodSharedCellmodSharedCellmodSharedCellmodSharedCell (Figure 1.8 e 1.12)adesso appaiono nel modulo modCircularBuffermodCircularBuffermodCircularBuffermodCircularBuffermodCircularBuffer (Figura 1.16), dove il gestore degli eventiLoadLoadLoadLoadLoad (righe 15-50) esegue le istruzioni.

Le differenze più significative fra questo e i precedenti esempi sincronizzati si hannonella classe CHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronized (Figura 1.13), che adesso contiene cinque variabilidi istanza. L’array mBuffermBuffermBuffermBuffermBuffer è formato da tre elementi di tipo IntegerIntegerIntegerIntegerInteger e rappresenta il buffercircolare. La variabile occupiedBufferCountoccupiedBufferCountoccupiedBufferCountoccupiedBufferCountoccupiedBufferCount è la variabile di condizione utilizzata per de-terminare se un produttore può scrivere nel buffer circolare (occupiedBufferCountoccupiedBufferCountoccupiedBufferCountoccupiedBufferCountoccupiedBufferCount è mi-nore del numero di elementi dell’array mBuffermBuffermBuffermBuffermBuffer) e se un consumatore può leggere il buffercircolare (occupiedBufferCountoccupiedBufferCountoccupiedBufferCountoccupiedBufferCountoccupiedBufferCount è maggiore di 0). La variabile readLocationreadLocationreadLocationreadLocationreadLocation indica laposizione dove il consumatore può leggere il successivo valore. La variabile writeLocationwriteLocationwriteLocationwriteLocationwriteLocationindica la successiva locazione dove il produttore può inserire un valore. Il programma visualizzal’output in txtOutputtxtOutputtxtOutputtxtOutputtxtOutput (un controllo TextBoxTextBoxTextBoxTextBoxTextBox).

Il metodo di accesso SetSetSetSetSet (righe 73-115) della proprietà BufferBufferBufferBufferBuffer svolge gli stessi compitiche svolgeva nella Figura 1.9, a parte alcune varianti. Anziché applicare i metodi EnterEnterEnterEnterEnter eExitExitExitExitExit di MonitorMonitorMonitorMonitorMonitor per mettere e togliere il blocco sull’oggetto CHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronized,utilizziamo un gruppo di istruzioni preceduto dalla parola chiave SyncLockSyncLockSyncLockSyncLockSyncLock (riga 77) perbloccare l’oggetto CHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronized. Quando il controllo del programma rag-

MULTITHREADING 27

giunge SyncLockSyncLockSyncLockSyncLockSyncLock, il thread correntemente in esecuzione pone il blocco (supponendo che ilblocco sia disponibile) sull’oggetto CHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronized (ovvero MeMeMeMeMe). Completatele istruzioni di SyncLockSyncLockSyncLockSyncLockSyncLock, il thread libera automaticamente il blocco.

Errore tipico 1.5

Quando usiamo i metodi EnterEnterEnterEnterEnter e ExitExitExitExitExit della classe MonitorMonitorMonitorMonitorMonitor per gestire il blocco di un oggetto,ExitExitExitExitExit deve essere chiamato esplicitamente per rilasciare il blocco. Se si verifica un’eccezionein un metodo prima che ExitExitExitExitExit possa essere chiamato e l’eccezione venga intercettata, il metodopuò terminare senza chiamare ExitExitExitExitExit. In questo caso, il blocco non viene rilasciato. Per evi-tare questo errore, inserite il codice che potrebbe generare delle eccezioni in un blocco TryTryTryTryTry ela chiamata di ExitExitExitExitExit nel corrispondente blocco FinallyFinallyFinallyFinallyFinally. Questo garantisce che il blocco siarilasciato.

Ingegneria del software 1.2

Se utilizziamo SyncLockSyncLockSyncLockSyncLockSyncLock per gestire il blocco di un oggetto sincronizzato, eliminiamo il rischiodi dimenticarci di rilasciare il blocco con una chiamata del metodo ExitExitExitExitExit di MonitorMonitorMonitorMonitorMonitor. Quandole istruzioni di SyncLockSyncLockSyncLockSyncLockSyncLock terminano, Visual Basic chiama implicitamente il metodo ExitExitExitExitExitdi MonitorMonitorMonitorMonitorMonitor. Quindi, anche se si verifica un’eccezione, il blocco sarà rilasciato.

La struttura IfIfIfIfIf (righe 81-88) del metodo SetSetSetSetSet determina se il produttore deve attendere(tutti i buffer sono pieni). Se il produttore deve restare in attesa, le righe 82-83 accodano iltesto a txtOutputtxtOutputtxtOutputtxtOutputtxtOutput per indicare che il produttore è in attesa di svolgere il suo compito; la riga87 chiama il metodo WaitWaitWaitWaitWait di MonitorMonitorMonitorMonitorMonitor per inserire il produttore nello stato WaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoindell’oggetto CHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronized. Quando l’esecuzione riprende nella riga 92, dopola struttura IfIfIfIfIf, il valore scritto dal produttore viene posto nella locazione writeLocationwriteLocationwriteLocationwriteLocationwriteLocationdel buffer circolare. Le righe 94-96 accodano a TextBoxTextBoxTextBoxTextBoxTextBox un messaggio che contiene il valoreprodotto. La riga 100 incrementa occupiedBufferCountoccupiedBufferCountoccupiedBufferCountoccupiedBufferCountoccupiedBufferCount, perché il buffer adesso contienealmeno un valore che può essere letto dal consumatore. Le righe 104-105 aggiornanowriteLocationwriteLocationwriteLocationwriteLocationwriteLocation per la successiva chiamata del metodo SetSetSetSetSet della proprietà BufferBufferBufferBufferBuffer. Nellariga 107, il metodo CreateStateOutputCreateStateOutputCreateStateOutputCreateStateOutputCreateStateOutput (righe 120-165) genera un output che indica ilnumero di buffer occupati, il contenuto dei buffer e i valori correnti di writeLocationwriteLocationwriteLocationwriteLocationwriteLocation ereadLocationreadLocationreadLocationreadLocationreadLocation. Infine, la riga 112 chiama il metodo PulsePulsePulsePulsePulse di MonitorMonitorMonitorMonitorMonitor per indicare che unthread in attesa per l’oggetto CHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronized dovrà passare nello stato StartedStartedStartedStartedStarted.Notate che, quando viene raggiunta l’ultima istruzione di SyncLockSyncLockSyncLockSyncLockSyncLock (EndEndEndEndEnd SyncLockSyncLockSyncLockSyncLockSyncLock) nellariga 113, il thread rilascia il blocco dell’oggetto CHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronized.

Il metodo GetGetGetGetGet (righe 29-71) della proprietà BufferBufferBufferBufferBuffer svolge gli stessi compiti dell’esempiodella Figura 1.9, a parte alcune piccole varianti. La struttura IfIfIfIfIf (righe 37-43) del metodo GetGetGetGetGetdetermina se il consumatore deve attendere (tutti i buffer sono vuoti). Se il consumatore deverestare in attesa, le righe 38-39 accodano il testo a txtOutputtxtOutputtxtOutputtxtOutputtxtOutput per indicare che il consumatoreè in attesa di svolgere il suo compito; la riga 42 chiama il metodo WaitWaitWaitWaitWait di MonitorMonitorMonitorMonitorMonitor per porre ilconsumatore nello stato WaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoin dell’oggetto CHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronized. Anchequi utilizziamo SyncLockSyncLockSyncLockSyncLockSyncLock per mettere e togliere il blocco dell’oggetto CHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronized,anziché i metodi EnterEnterEnterEnterEnter e ExitExitExitExitExit di MonitorMonitorMonitorMonitorMonitor. Quando l’esecuzione riprende nella riga 47, dopola struttura IfIfIfIfIf, readValuereadValuereadValuereadValuereadValue assume il valore della locazione readLocationreadLocationreadLocationreadLocationreadLocation del buffer circolare.Le righe 49-51 accodano il valore consumato a TextBoxTextBoxTextBoxTextBoxTextBox. La riga 55 riduce il valore dioccupiedBufferCountoccupiedBufferCountoccupiedBufferCountoccupiedBufferCountoccupiedBufferCount, in quanto il buffer contiene almeno una posizione libera dove il pro-duttore può inserire un valore. La riga 59 aggiorna readLocationreadLocationreadLocationreadLocationreadLocation per la successiva chiamatadel metodo GetGetGetGetGet di BufferBufferBufferBufferBuffer. La riga 61 chiama il metodo CreateStateOutputCreateStateOutputCreateStateOutputCreateStateOutputCreateStateOutput per visualizzare

28 CAPITOLO 1

il numero di buffer occupati, il contenuto dei buffer e i valori correnti di writeLocationwriteLocationwriteLocationwriteLocationwriteLocation ereadLocationreadLocationreadLocationreadLocationreadLocation. Infine, la riga 66 chiama il metodo PulsePulsePulsePulsePulse per passare nello stato StartedStartedStartedStartedStarted ilprossimo thread che si trova in attesa per l’oggetto CHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronizedCHoldIntegerSynchronized; la riga 68restituisce il valore consumato al metodo che ha effettuato la chiamata.

Gli output in fondo alla Figura 1.16 includono il valore corrente di occupiedBufferCountoccupiedBufferCountoccupiedBufferCountoccupiedBufferCountoccupiedBufferCount,i contenuti dei buffer e i valori di writeLocationwriteLocationwriteLocationwriteLocationwriteLocation e readLocationreadLocationreadLocationreadLocationreadLocation. Le lettere WWWWW e RRRRR rappre-sentano, rispettivamente, i valori correnti di writeLocationwriteLocationwriteLocationwriteLocationwriteLocation e readLocationreadLocationreadLocationreadLocationreadLocation. Notate che,dopo che il terzo valore è stato posto nel terzo elemento del buffer, il quarto valore vieneinserito all’inizio dell’array. Questo genera l’effetto del buffer circolare.

1 ‘ File: HoldIntegerSynchronized.vb 2 ‘ Sincronizza l’accesso al buffer circolare di numeri interi 3 4 Imports System.Threading 5 Imports System.Windows.Forms 6 7 Public Class CHoldIntegerSynchronized 8 9 ‘ ogni elemento dell’array è un buffer 10 Private mBuffer As Integer() = {-1, -1, -1} 11 12 ‘ occupiedBufferCount conta i buffer occupati 13 Private occupiedBufferCount As Integer 14 15 ‘ mantiene le locazioni di lettura e scrittura del buffer 16 Private readlocation, writeLocation As Integer 17 18 ‘ componente GUI per visualizzare l’output 19 Private txtOutput As TextBox 20 21 ‘ costruttore 22 Public Sub New(ByVal output As TextBox) 23 txtOutput = output 24 End Sub ‘ New 25 26 ‘ proprietà Buffer 27 Property Buffer() As Integer 28 29 Get 30 31 ‘ blocca questo oggetto mentre ottiene 32 ‘ il valore dall’array mBuffer 33 SyncLock (Me) 34 35 ‘ se non ci sono dati da leggere, 36 ‘ pone il thread nello stato WaitSleepJoin 37 If occupiedBufferCount = 0 Then 38 txtOutput.Text &= vbCrLf & “All buffers empty. “ & _ 39 Thread.CurrentThread.Name & “ waits.” 40 41 txtOutput.ScrollToCaret()

MULTITHREADING 29

42 Monitor.Wait(Me) 43 End If 44 45 ‘ ottiene il valore dalla posizione corrente readLocation 46 ‘ aggiunge una stringa che indica il valore letto 47 Dim readValue As Integer = mBuffer(readlocation) 48 49 txtOutput.Text &= vbCrLf & _ 50 Thread.CurrentThread.Name & “ reads “ & _ 51 mBuffer(readlocation) & “ “ 52 53 ‘ un valore letto, quindi riduce il numero 54 ‘ di buffer occupati 55 occupiedBufferCount -= 1 56 57 ‘ aggiorna readLocation per la prossima operazione di 58 ‘ lettura aggiunge lo stato corrente all’output 59 readlocation = (readlocation + 1) Mod mBuffer.Length 60 61 txtOutput.Text &= CreateStateOutput() 62 txtOutput.ScrollToCaret() 63 64 ‘ riporta l’eventuale thread in attesa 65 ‘ nello stato Started 66 Monitor.Pulse(Me) 67 68 Return readValue 69 End SyncLock 70 71 End Get 72 73 Set(ByVal Value As Integer) 74 75 ‘ blocca l’oggetto mentre imposta 76 ‘ il valore nell’array mBuffer 77 SyncLock (Me) 78 79 ‘ se non ci sono locazioni vuote, 80 ‘ pone il thread nello stato WaitSleepJoin 81 If occupiedBufferCount = mBuffer.Length Then 82 txtOutput.Text &= vbCrLf & “All buffers full. “ & _ 83 Thread.CurrentThread.Name & “ waits.” 84 85 txtOutput.ScrollToCaret() 86 87 Monitor.Wait(Me) 88 End If 89 90 ‘ pone il valore in writeLocation di mBuffer, poi 91 ‘ aggiunge la stringa che indica il valore prodotto

Figura 1.13 Buffer circolare condiviso (continua)

30 CAPITOLO 1

92 mBuffer(writeLocation) = Value 93 94 txtOutput.Text &= vbCrLf & _ 95 Thread.CurrentThread.Name & “ writes “ & _ 96 mBuffer(writeLocation) & “ “ 97 98 ‘ un valore prodotto, quindi aumenta il numero 99 ‘ di elementi mBuffer occupati100 occupiedBufferCount += 1101102 ‘ aggiorna writeLocation per la prossima operazione103 ‘ di scrittura; aggiunge lo stato corrente all’output104 writeLocation = (writeLocation + 1) Mod _105 mBuffer.Length106107 txtOutput.Text &= CreateStateOutput()108 txtOutput.ScrollToCaret()109110 ‘ riporta l’eventuale thread in attesa111 ‘ nello stato Started112 Monitor.Pulse(Me)113 End SyncLock114115 End Set116117 End Property ‘ Buffer118119 ‘ crea l’output di stato120 Public Function CreateStateOutput() As String121122 Dim i As Integer123124 ‘ visualizza la prima riga delle informazioni di stato125 Dim output As String = “(buffers occupied: “ & _126 occupiedBufferCount & “)” & vbCrLf & “buffers: “127128 For i = 0 To mBuffer.GetUpperBound(0)129 output &= “ “ & mBuffer(i) & “ “130 Next131132 output &= vbCrLf133134 ‘ visualizza la seconda riga delle informazioni di stato135 output &= “ “136137 For i = 0 To mBuffer.GetUpperBound(0)138 output &= “—— “139 Next140141 output &= vbCrLf142

MULTITHREADING 31

143 ‘ visualizza la terza riga delle informazioni di stato144 output &= “ “145146 For i = 0 To mBuffer.GetUpperBound(0)147148 If (i = writeLocation AndAlso _149 writeLocation = readlocation) Then150151 output &= “ WR “152 ElseIf i = writeLocation Then153 output &= “ W “154 ElseIf i = readlocation Then155 output &= “ R “156 Else157 output &= “ “158 End If159160 Next161162 output &= vbCrLf163164 Return output165 End Function ‘ CreateStateOutput166167 End Class ‘ CHoldIntegerSynchronized

Figura 1.13 Buffer circolare condiviso

1 ‘ File: Producer.vb 2 ‘ Inserisce 10 valori interi in un buffer sincronizzato 3 4 Imports System.Threading 5 Imports System.Windows.Forms 6 7 Public Class CProducer 8 Private sharedLocation As CHoldIntegerSynchronized 9 Private randomSleepTime As Random10 Private txtOutput As TextBox1112 ‘ costruttore13 Public Sub New(ByVal sharedObject As CHoldIntegerSynchronized, _14 ByVal randomObject As Random, ByVal output As TextBox)1516 sharedLocation = sharedObject17 randomSleepTime = randomObject18 txtOutput = output19 End Sub ‘ New2021 ‘ registra i valori 11-20 e li pone22 ‘ nel buffer di sharedLocation23 Public Sub Produce()

Figura 1.14 Il thread produttore pone dei valori interi nel buffer circolare (continua)

32 CAPITOLO 1

24 Dim count As Integer2526 ‘ dorme per un periodo di tempo casuale fino a 3 secondi27 ‘ imposta la proprietà Buffer di sharedLocation28 For count = 11 To 2029 Thread.Sleep(randomSleepTime.Next(1, 3000))30 sharedLocation.Buffer = count31 Next3233 txtOutput.Text &= vbCrLf & Thread.CurrentThread.Name & _34 “ done producing. “ & vbCrLf & _35 Thread.CurrentThread.Name & “ terminated.” & vbCrLf36 End Sub ‘ Produce3738 End Class ‘ CProducer

Figura 1.14 Il thread produttore pone dei valori interi nel buffer circolare

1 ‘ File: Consumer.vb 2 ‘ Legge 10 valori interi nel buffer circolare sincronizzato 3 4 Imports System.Threading 5 Imports System.Windows.Forms 6 7 Public Class CConsumer 8 Private sharedLocation As CHoldIntegerSynchronized 9 Private randomSleepTime As Random10 Private txtOutput As TextBox1112 ‘ costruttore13 Public Sub New(ByVal sharedObject As CHoldIntegerSynchronized, _14 ByVal randomObject As Random, ByVal output As TextBox)1516 sharedLocation = sharedObject17 randomSleepTime = randomObject18 txtOutput = output19 End Sub ‘ New2021 ‘ legge 10 numeri interi nel buffer22 Public Sub Consume()23 Dim count, sum As Integer2425 ‘ ciclo di 10 iterazioni; dorme per un periodo26 ‘ di tempo casuale (max 3 secondi); aggiunge a27 ‘ sum il valore della proprietà Buffer di sharedLocation28 For count = 1 To 1029 Thread.Sleep(randomSleepTime.Next(1, 3000))30 sum += sharedLocation.Buffer31 Next3233 txtOutput.Text &= vbCrLf & “Total “ & _34 Thread.CurrentThread.Name & “ consumed: “ & sum & vbCrLf & _

MULTITHREADING 33

35 Thread.CurrentThread.Name & “ terminated.” & vbCrLf3637 txtOutput.ScrollToCaret()38 End Sub ‘ Consume3940 End Class ‘ CConsumer

Figura 1.15 Il consumatore legge i valori interi nel buffer circolare sincronizzato

1 ‘ File: FrmCircularBuffer.vb 2 ‘ Crea il modello di visualizzazione e avvia i thread 3 4 Imports System.Threading 5 Imports System.Windows.Forms 6 7 Public Class FrmCircularBuffer 8 Inherits Form 910 Friend WithEvents txtOutput As TextBox1112 ‘ codice generato da Visual Studio .NET1314 ‘ inizializza i thread15 Private Sub FrmCircularBuffer_Load(ByVal sender As Object, _16 ByVal e As System.EventArgs) Handles MyBase.Load1718 ‘ crea l’oggetto condiviso19 Dim sharedLocation As _20 New CHoldIntegerSynchronized(txtOutput)2122 ‘ visualizza lo stato di sharedLocation prima23 ‘ dell’esecuzione dei thread produttore e consumatore24 txtOutput.Text = sharedLocation.CreateStateOutput()2526 ‘ oggetto casuale utilizzato dai thread27 Dim randomObject As New Random()2829 ‘ crea gli oggetti CProducer e CConsumer30 Dim producer As New CProducer(sharedLocation, _31 randomObject, txtOutput)3233 Dim consumer As New CConsumer(sharedLocation, _34 randomObject, txtOutput)3536 ‘ crea i thread37 Dim producerThread As _38 New Thread(AddressOf producer.Produce)3940 Dim consumerThread As _41 New Thread(AddressOf consumer.Consume)42

Figura 1.16 I thread produttore e consumatore accedono al buffer circolare (continua)

34 CAPITOLO 1

43 ‘ assegna i nomi ai thread44 producerThread.Name = “Producer”45 consumerThread.Name = “Consumer”4647 ‘ avvia i thread48 producerThread.Start()49 consumerThread.Start()50 End Sub ‘ FrmCircularBuffer_Load5152 End Class ‘ FrmCircularBuffer

Il valore posto nell’ultimo buffer.

Il valore successivo sarà postonel primo buffer a sinistra.

Effetto del buffer circolare:il quarto valore è posto nelprimo buffer a sinistra.

Il valore posto nell’ultimo buffer.

Il valore successivo sarà postonel primo buffer a sinistra.

Effetto del buffer circolare:il settimo valore è posto nelprimo buffer a sinistra.

MULTITHREADING 35

Figura 1.16 I thread produttore e consumatore accedono al buffer circolare

Esercizi di autovalutazione1.1 Completate le seguenti frasi:

a) I metodi _____________ e _____________ mettono e tolgono un blocco in un oggetto.b) In un gruppo di thread di uguale priorità, ogni thread riceve una piccola quantità di tempo,

detta _____________, durante la quale può svolgere il suo compito.c) Visual Basic dispone di uno spazzino automatico, detto _____________, che libera la

memoria allocata quando non è più richiesta dal programma.d) Un thread può essere vivo, ma non nello stato StartedStartedStartedStartedStarted, perché si trova negli stati

_____________, _____________ o _____________.e) Un thread entra nello stato _____________ quando termina il metodo che controlla il suo

ciclo di vita.

Il valore posto nell’ultimo buffer.

Il valore successivo sarà postonel primo buffer a sinistra.

Effetto del buffer circolare:il decimo valore è posto nelprimo buffer a sinistra.

36 CAPITOLO 1

f ) La priorità di un thread deve essere una delle seguenti costanti di ThreadPriorityThreadPriorityThreadPriorityThreadPriorityThreadPriority:_____________, _____________, _____________, _____________, _____________.

g) Per attendere un determinato numero di millisecondi e poi riprendere l’esecuzione, unthread deve chiamare il metodo _____________ della classe ThreadThreadThreadThreadThread.

h) Il metodo _____________ della classe MonitorMonitorMonitorMonitorMonitor porta un thread dallo stato WaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoin

allo stato StartedStartedStartedStartedStarted.i) Un gruppo di istruzioni _____________ pone automaticamente un blocco su un oggetto

e lo rilascia quando viene eseguita l’ultima istruzione del gruppo.j) La classe MonitorMonitorMonitorMonitorMonitor fornisce i metodi che _____________ l’accesso ai dati condivisi.

1.2 Determinate se le seguenti affermazioni sono vere o false. Se sono false, spiegate il perché.a) Un thread non è può essere eseguito se è nello stato StoppedStoppedStoppedStoppedStopped.b) In Visual Basic, un thread con priorità più alta che entra nello stato StartedStartedStartedStartedStarted sorpassa i

thread con priorità più bassa.c) Il codice eseguito da un thread è definito nel suo metodo MainMainMainMainMain.d) Un thread nello stato WaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoin ritorna sempre nello stato StartedStartedStartedStartedStarted quando viene

chiamato il metodo PulsePulsePulsePulsePulse di MonitorMonitorMonitorMonitorMonitor.e) Il metodo SleepSleepSleepSleepSleep della classe ThreadThreadThreadThreadThread non consuma il tempo del processore mentre un

thread dorme.f ) Un thread nello stato BlockedBlockedBlockedBlockedBlocked può essere posto nello stato StartedStartedStartedStartedStarted dal metodo PulsePulsePulsePulsePulse di

MonitorMonitorMonitorMonitorMonitor.g) I metodi WaitWaitWaitWaitWait, PulsePulsePulsePulsePulse e PulseAllPulseAllPulseAllPulseAllPulseAll della classe MonitorMonitorMonitorMonitorMonitor possono essere utilizzati in qualsiasi

blocco di codice.h) Il programmatore deve inserire una chiamata del metodo ExitExitExitExitExit di MonitorMonitorMonitorMonitorMonitor in un gruppo di

istruzioni SyncLockSyncLockSyncLockSyncLockSyncLock per liberare il blocco.

Risposte agli esercizi di autovalutazione1.1 a) EnterEnterEnterEnterEnter, ExitExitExitExitExit. b) quantum o timeslice. c) garbage collector. d) WaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoin, SuspendedSuspendedSuspendedSuspendedSuspended

o BlockedBlockedBlockedBlockedBlocked. e) StoppedStoppedStoppedStoppedStopped. f ) LowestLowestLowestLowestLowest, BelowNormalBelowNormalBelowNormalBelowNormalBelowNormal, NormalNormalNormalNormalNormal, AboveNormalAboveNormalAboveNormalAboveNormalAboveNormal e HighestHighestHighestHighestHighest. g) SleepSleepSleepSleepSleep. h)PulsePulsePulsePulsePulse. i) SyncLockSyncLockSyncLockSyncLockSyncLock. j) sincronizzano.

1.2 a) Vero. b) Vero. c) Falso. Il codice eseguito da un thread è definito nel metodo specificato daldelegato ThreadStartThreadStartThreadStartThreadStartThreadStart del thread. d) Falso. Un thread può trovarsi nello stato WaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoin per varimotivi. Il metodo PulsePulsePulsePulsePulse passa un thread dallo stato WaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoin allo stato StartedStartedStartedStartedStarted soltanto se ilthread era entrato nello stato WaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoinWaitSleepJoin in seguito a una chiamata del metodo WaitWaitWaitWaitWait di MonitorMonitorMonitorMonitorMonitor.e) Vero. f ) Falso. Un thread è bloccato dal sistema operativo e ritorna allo stato StartedStartedStartedStartedStarted quando ilsistema operativo determina che il thread può riprendere l’esecuzione (ovvero quando viene completatauna richiesta di I/O o quando viene rilasciato il blocco che il thread ha tentato di acquisire). g) Falso.I metodi della classe MonitorMonitorMonitorMonitorMonitor possono essere chiamati soltanto se il thread che effettua la chiamatapossiede il blocco sull’oggetto che ogni metodo riceve come argomento. h) Falso. Un gruppo SyncLockSyncLockSyncLockSyncLockSyncLock

libera automaticamente il blocco quando il thread completa l’esecuzione delle istruzioni di SyncLockSyncLockSyncLockSyncLockSyncLock.

Esercizi1.3 Il codice che manipola il buffer circolare nella Figura 1.13 dovrà funzionare con un buffer didue o più elementi. Provate a cambiare la dimensione del buffer per vedere l’effetto di questa modificasui thread produttore e consumatore.

1.4 Scrivete un programma per dimostrare che, quando viene eseguito un thread con alta priorità,questo ritarda l’esecuzione dei thread con priorità più bassa.

MULTITHREADING 37

1.5 Scrivete un programma che dimostra il timeslicing fra vari thread di uguale priorità. Mostratecome l’esecuzione di un thread con bassa priorità venga ritardata dal timeslicing di thread con prioritàpiù elevata.

1.6 Scrivete un programma per mostrare un thread di alta priorità che usa SleepSleepSleepSleepSleep per dare a unthread di bassa priorità la possibilità di essere eseguito.

1.7 Due problemi che possono verificarsi nei linguaggi come Visual Basic che permettono ai threaddi rimanere in attesa sono: i deadlock (dove uno o più thread restano in attesa di un evento che nonpotrà verificarsi) e i ritardi indefiniti (dove uno o più thread vengono ritardati per un lungo periodo ditempo che non può essere determinato in anticipo). Fornite un esempio di come ciascuno di questi dueproblemi possa verificarsi in un programma multithread di Visual Basic.

1.8 (Lettori e scrittori) Questo esercizio prevede lo sviluppo di un monitor Visual Basic per risolvereil noto problema del controllo dell’accesso in parallelo. Con il multithreading, molti thread possonoaccedere ai dati condivisi; come è stato detto in questo capitolo, l’accesso ai dati deve essere attenta-mente sincronizzato, per evitare di danneggiarli.

Considerate un sistema di prenotazioni aeree dove molti clienti cercano di prenotare dei posti suun particolare volo. Tutte le informazioni relative ai voli e ai posti sono conservate in un databasecomune; il database è formato da molte voci, ciascuna delle quali rappresenta un posto su un partico-lare volo in un particolare giorno. In un tipico sistema di prenotazioni aeree, il cliente cerca all’internodel database il volo “ottimale” che soddisfa le sue esigenze. Un posto disponibile può essere prenotatoda qualsiasi cliente; in questo caso, se il nostro cliente tenta di effettuare la prenotazione di quel posto,scopre che i dati sono cambiati e che il volo non è più disponibile.

Il cliente che esegue la ricerca all’interno del database è chiamato lettore (reader), mentre quelloche cerca di prenotare il volo è chiamato scrittore (writer). Un numero qualsiasi di lettori può effettuaredelle ricerche contemporaneamente, ma soltanto uno scrittore ha diritto all’accesso ai dati condivisi perevitare che questi vengano danneggiati.

Scrivete un programma multithread che lancia più thread di lettura e scrittura, ciascuno dei qualicerca di accedere a un unico record del database. Un thread di scrittura ha due possibili transazioni:MakeReservationMakeReservationMakeReservationMakeReservationMakeReservation e CancelReservationCancelReservationCancelReservationCancelReservationCancelReservation. Un lettore ha una sola transazione: QueryReservationQueryReservationQueryReservationQueryReservationQueryReservation.Per prima cosa implementate una versione del programma che consente l’accesso non sincronizzato airecord delle prenotazioni. Mostrate come l’integrità del database può essere danneggiata. Poi, imple-mentate una versione del programma che usa i metodi di sincronizzazione Visual Basic, con WaitWaitWaitWaitWait ePulsePulsePulsePulsePulse che impongono un protocollo di regole ai lettori e agli scrittori che vogliono accedere ai daticondivisi. In particolare, il programma dovrà permettere a più lettori di accedere contemporaneamenteai dati quando non c’è alcuno scrittore attivo. Se, invece, c’è uno scrittore attivo, nessun lettore èautorizzato ad accedere ai dati condivisi.

Fate attenzione, perché questo problema nasconde varie insidie. Per esempio, che cosa succedequando ci sono vari lettori attivi e uno scrittore vuole scrivere? Se consentite a un flusso costante dilettori di arrivare e condividere i dati, lo scrittore potrebbe essere ritardato all’infinito e, stanco diattendere, potrebbe rinunciare a fare la prenotazione. Per risolvere questo problema, potreste decideredi dare la precedenza agli scrittori rispetto ai lettori. Ma, anche in questo caso, c’è una trappola, inquanto un flusso costante di scrittori potrebbe ritardare all’infinito i lettori in attesa (che potrebberorinunciare a effettuare le ricerche nel database). Implementate il monitor con i seguenti metodi:StartReadingStartReadingStartReadingStartReadingStartReading, che viene chiamato da un lettore che intende accedere al sistema di prenotazioni;StopReadingStopReadingStopReadingStopReadingStopReading, che viene chiamato da un lettore che ha finito di leggere una prenotazione; StartWritingStartWritingStartWritingStartWritingStartWriting,che viene chiamato da uno scrittore che vuole effettuare una prenotazione; StopWritingStopWritingStopWritingStopWritingStopWriting, che vienechiamato da qualsiasi scrittore che ha finito di effettuare una prenotazione.


Recommended