Una funzione che accetta un valore e restituisce un altro valore, e non disturba nulla al di fuori della funzione, non ha effetti collaterali ed è quindi thread-safe. Se vuoi considerare cose come il modo in cui la funzione viene eseguita influisce sul consumo di energia, questo è un problema diverso.
Suppongo che tu ti stia riferendo a una macchina completa di Turing che sta eseguendo una sorta di linguaggio di programmazione ben definito, in cui i dettagli dell'implementazione sono irrilevanti. In altre parole, non dovrebbe importare cosa sta facendo la pila, se la funzione che sto scrivendo nel mio linguaggio di programmazione di scelta può garantire l'immutabilità entro i confini del linguaggio. Non penso allo stack quando sto programmando in un linguaggio di alto livello, né dovrei farlo.
Per illustrare come funziona, offrirò alcuni semplici esempi in C #. Affinché questi esempi siano veri, dobbiamo fare un paio di ipotesi. Innanzitutto, il compilatore segue le specifiche C # senza errori e, in secondo luogo, produce programmi corretti.
Diciamo che voglio una funzione semplice che accetta una collezione di stringhe e restituisce una stringa che è una concatenazione di tutte le stringhe nella collezione separate da virgole. Un'implementazione semplice e ingenua in C # potrebbe essere simile a questa:
public string ConcatenateWithCommas(ImmutableList<string> list)
{
string result = string.Empty;
bool isFirst = false;
foreach (string s in list)
{
if (isFirst)
result += s;
else
result += ", " + s;
}
return result;
}
Questo esempio è immutabile, prima facie. Come lo so? Perché l'oggetto string
è immutabile. Tuttavia, l'implementazione non è l'ideale. Poiché result
è immutabile, è necessario creare un nuovo oggetto stringa ogni volta attraverso il ciclo, sostituendo l'oggetto originale a cui result
punta. Questo può influire negativamente sulla velocità e mettere pressione sul garbage collector, dato che deve ripulire tutte quelle stringhe extra.
Ora, diciamo che faccio questo:
public string ConcatenateWithCommas(ImmutableList<string> list)
{
var result = new StringBuilder();
bool isFirst = false;
foreach (string s in list)
{
if (isFirst)
result.Append(s);
else
result.Append(", " + s);
}
return result.ToString();
}
Tieni presente che ho sostituito string
result
con un oggetto mutabile, StringBuilder
. Questo è molto più veloce del primo esempio, perché una nuova stringa non viene creata ogni volta attraverso il ciclo. Invece, l'oggetto StringBuilder aggiunge semplicemente i caratteri di ogni stringa a una raccolta di caratteri e alla fine restituisce l'intero oggetto.
Questa funzione è immutabile, anche se StringBuilder è modificabile?
Sì, lo è. Perché? Poiché ogni volta che viene chiamata questa funzione, viene creato un nuovo StringBuilder, solo per quella chiamata. Quindi ora abbiamo una funzione pura che è sicura per i thread, ma contiene componenti mutabili.
Ma cosa succede se l'ho fatto?
public class Concatenate
{
private StringBuilder result = new StringBuilder();
bool isFirst = false;
public string ConcatenateWithCommas(ImmutableList<string> list)
{
foreach (string s in list)
{
if (isFirst)
result.Append(s);
else
result.Append(", " + s);
}
return result.ToString();
}
}
Questo metodo è sicuro per i thread? No, non lo è. Perché? Perché la classe ora ha uno stato da cui dipende il mio metodo. Nel metodo è presente una condizione di competizione: un thread può modificare IsFirst
, ma un altro thread può eseguire il primo Append()
, in nel qual caso ora ho una virgola all'inizio della mia stringa che non dovrebbe essere lì.
Perché potrei volerlo fare in questo modo? Bene, potrei volere che i thread accumulino le stringhe nel mio result
senza riguardo all'ordine, o nell'ordine in cui i thread entrano. Forse è un logger, chissà?
Ad ogni modo, per risolvere il problema, ho inserito un'istruzione lock
attorno alle parentesi del metodo.
public class Concatenate
{
private StringBuilder result = new StringBuilder();
bool isFirst = false;
private static object locker = new object();
public string AppendWithCommas(ImmutableList<string> list)
{
lock (locker)
{
foreach (string s in list)
{
if (isFirst)
result.Append(s);
else
result.Append(", " + s);
}
return result.ToString();
}
}
}
Ora è sicuro per i thread di nuovo.
L'unico modo in cui i miei metodi immutabili potrebbero non riuscire a essere thread-safe è se il metodo in qualche modo perde parte della sua implementazione. Potrebbe succedere? Non se il compilatore è corretto e il programma è corretto. Avrò mai bisogno di serrature su questi metodi? No.
Per un esempio di come l'implementazione potrebbe essere trapelata in uno scenario di concorrenza, vedi qui .