Analizziamo ora le operazioni compiute da ciascun singolo
thread/giocatore. È opportuno ricordare che, al momento della
creazione di un thread, tutti i dati (scalari, array, funzioni, e
quant'altro) presenti nel programma in quell'istante vengono
clonati: di fatto viene creato un programma uguale all'originale,
che gira come un thread dipendente dal programma principale.
Dunque, tutti i dati sono separati, fatta eccezione per quelli
esplicitamente condivisi. Nel nostro caso viene condivisa solo
la variabile $palline, che indica le palline che il tennis
club ha a disposizione in un determinato istante: questo valore
è infatti unico, ed ogni thread deve potervi accedere
in scrittura, secondo la modalità che verrà spiegata
in seguito.
La subroutine che costituisce il codice principale di ciascun
thread è giocatore(). In essa viene avviato un loop infinito,
ogni iterazione del quale rappresenta una partita del giocatore,
divisa in tre fasi: richiesta delle palline, gioco, restituzione
delle palline. Il numero di palline è determinato estraendo
un numero casuale compreso tra 1 e $pallinemax, in
modo da rendere diversa ogni richiesta da quella precedente. A questo punto
le palline vengono richieste tramite la funzione chiedi_palline(),
che ritorna il numero delle palline che sono state effettivamente
assegnate al thread/giocatore, oppure 0 in caso di mancata
assegnazione. In caso appunto l'assegnazione non sia stata
possibile (per via della mancanza delle palline necessarie)
viene atteso un certo tempo (definito dalla variabile molto
opportunamente chiamata $tempobar), e poi esse vengono richieste
di nuovo, finché esse non vengono assegnate. Chiaramente, durante
questi secondi, un altro thread/giocatore ha il tempo di restituire le
sue palline, e forse un altro ancora riesce a richiederle
prima che tale tempo sia trascorso. Una volta ottenute le
palline, il programma si ferma per un tempo determinato in maniera
casuale tra un secondo ed il contenuto di $tempomax.
Trascorso questo intervallo, le palline vengono restituite
con una chiamata a restituisci_palline(). Dopo una pausa di
$tempoidl secondi, il ciclo infinito riparte.
Il clou del programma risiede nella funzione chiedi_palline(),
che si occupa di assegnare un certo numero di palline a ciascun
thread/giocatore. Anzitutto, è necessario effettuare il lock
della variabile condivisa $palline, cioè richiederne l'utilizzo
esclusivo. Questa operazione è strettamente necessaria per
evitare delle race condition, cioè situazioni in cui più
di un thread accede allo stesso dato in contemporanea. Prendiamo
infatti in esame la linea di codice in cui viene decrementato il
numero di palline disponibili:
$palline -= $pallineass
L'operatore svolge fondamentalmente due operazioni su $palline:
ne legge il valore e poi lo decrementa del valore di $pallineass.
Tra queste due operazioni, in caso non vi fosse un lock sulla
variabile, un altro thread (o più di uno) potrebbe effettuare
a sua volta un'operazione. Ad esempio, poniamo che $palline
valga 27, e che un giocatore ne richieda 4 e l'altro 5; alla fine
di ciò $palline dovrebbe assumere valore 18. Senza un lock
potrebbe configurarsi una situazione del genere:
THREAD1: legge $palline
THREAD2: legge $palline
THREAD1: decrementa $palline => 23
THREAD2: decrementa $palline => 22
Alla fine $palline assumerebbe valore 22, annullando di fatto il
decremento effettuato dal primo thread. Un modo per prevenire queste
situazioni è, appunto, l'utilizzo di un lock sulla variabile
che si deve gestire:
lock ($palline);
Con la chiamata a lock() viene richiesta l'aasegnazione di un lock
sulla variabile passata come parametro. Se non esiste un precedente
lock su di essa, esso viene concesso e l'esecuzione del programma
va avanti; se è presente un lock da parte di un altro thread, il
thread corrente si blocca in attesa che tale lock venga rilasciato.
È fondamentale tenere a mente che lock() non previene in alcun
modo accessi alla variabile passatagli come parametro da parte di
altri thread, ma semplicemente causa il blocco delle chiamate a
lock() da parte di altri thread sulla stessa variabile. È dunque
indispensabile che tutti i thread che utilizzano tale variabile
condivisa ne chiedano il lock prima di utilizzarla.
Ottenuto il lock, le palline vengono assegnate in base ad una policy.
Nel programma sono incluse due funzioni, policy1() e policy2(): è
possibile utilizzare una oppure l'altra; la prima assegna semplicemente
le palline richieste ad ogni thread/giocatore (se ve ne sono a disposizione),
l'altra è un po' più complessa e, a seconda delle palline
rimaste, ne può assegnare un numero inferiorie rispetto a quelle
richieste. Se l'assegnazione va a buon fine, può essere effettuato
il decremento della variabile. Il lock su $palline sparisce quando la
chiamata a lock() esce dallo scope, in questo caso al termine della
funzione. L'uscita dallo scope rappresenta l'unica via per togliere un lock
ad una variabile, in quanto non esiste alcuna funzione unlock(). Notate che
è necessario chiamare lock() prima dell'assegnazione delle palline
secondo la policy scelta, poiché tale operazione implica la lettura
del numero di palline correnti, che poi non deve essere variato da altri
thread fino a che esso non viene eventualmente decrementato dal
thread corrente.
La funzione restituisci_palline() è molto più semplice: dopo
aver ottenuto l'obbligatorio lock su $palline, essa semplicemente
incrementa $palline del numero di palline restituire (passato
come parametro). |