Codice(C#,VS2008,.NET 3.5):
public class LoopInvariant
{
int count = 0;
public void Cicla()
{
while (this.count == 0) {}
}
public void Cambia()
{
this.count = 1;
}
}
fino a qua, nulla di particolare, questa classe non fa un bel niente, semplicemente abbiamo 2 metodi, un metodo Cicla() che esegue una while verificando che count sia uguale a 0, poi abbiamo Cambia() un metodo che cambia il contatore e lo porta a 1.
Ora usiamo questa classe(console application):
static void Main(string[] args)
{
LoopInvariant var = new LoopInvariant();
ThreadPool.QueueUserWorkItem(o =>
{
System.Threading.Thread.Sleep(5000);
((LoopInvariant)o).Cambia();
Console.WriteLine("Valore modificato");
}, var);
var.Cicla();
Console.WriteLine("Fine test");
Console.ReadKey();
}
qua cosa succede; niente di particolare: istanzio la classe LoopInvariant, faccio partire in un thread separato(attraverso il threadpool di .NET) un pezzo di codice che si blocca per 5 secondi(Sleep(5000)) dopo di che chiama il metodo Cambia() sulla mia istanza e stampa che lo ha fatto.
Bene, compiliamo(in release). Lanciamo(CTRL+F5)…passano i 5 secondi…6…7….8….9….?!?!?!?!?
cavolo succede?perchè non stampa “Fine test”?ho cambiato il valore di count…dovrebbe stampare “Fine test” (visto che la condizione count == 0 è falsa) e fermarsi li ad aspettare la pressione di un tasto…
Il problema è che il Jit compiler analizzando il metodo Cicla() applica una ottimizzazione chiamata Loop-invariant code motion, ovvero dal fatto che la variabile count non viene “toccata” all’interno del corpo del while, il compilatore può concludere che count è “loop invariant” e così metterlo in una variabile temporanea prima di iniziare il loop(in un registro del processore magari, così evitiamo di dover risolvere l’indirizzo in ram della variabile ad ogni ciclo migliorando le prestazioni) , questo comporta che quanto l’altro thread chiama Cambia() sulla stessa istanza la modifica non viene “vista” dal nostro metodo Cicla() portanto ad un loop infinito. Cosa che genera ancora più confusione è che se facciamo il debug(F5) di questo codice tutto funziona correttamente in quanto le ottimizzazioni sono disabilitate in configurazione DEBUG.
Per disabilitare questa ottimizzazione occorre dichiarare la nostra variabile count come volatile, questo rende il nostro count esplicitamente non “loop invariant” , come Joe Duffy spiega nel suo libro:
“Load and stores of volatile variables can never be introduced or removed, both in .NET and VC++, because they are assumed to be constantly changing. As such, they aren’t eligible for being considered loop invariant and hoisted outside of loops…”
quindi basta cambiare il nostro codice in questo modo:
public class LoopInvariant
{
volatile int count = 0;
public void Cicla()
{
while (this.count == 0) {}
}
public void Cambia()
{
this.count = 1;
}
}
a questo punto se compiliamo(sempre in release altrimenti le ottimizzazioni vengono soppresse per supporto al debug) e lanciamo(CTRL+F5)…1…2….3..4..5…e
adesso il programma funziona come ci aspettavamo…
Attenzione questo non è il solo effetto della keyword volatile, ricordo che leggere da un campo volatile(o usare Thread.VolatileRead) equivale logicamente ad una acquire fence, mentre scrivere un campo volatile(o usare Thread.VolatileWrite) equivale logicamente ad una release fence.
La classe LoopInvariant chiaramente non serve ad un piffero ed è li solo per didattica, ma questo ci fa capire che la programmazione multithread “aggiunge” una dimensione che oggi non è gestita in modo automatico dai compilatori, nel caso della loop invariant fare quella ottimizzazione non cambiava la semantica del programma, ma il danno lo abbiamo visto tutti.
Occhio…
Fonti: Concurrent Programming on Windows