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 esegue influenza il consumo di energia, questo è un problema diverso.
Suppongo che ti riferisci a una macchina completa di Turing che sta eseguendo una sorta di linguaggio di programmazione ben definito, in cui i dettagli di implementazione sono irrilevanti. In altre parole, non dovrebbe importare ciò che sta facendo lo stack, se la funzione che sto scrivendo nel mio linguaggio di programmazione preferito può garantire l'immutabilità all'interno dei confini del linguaggio. Non penso allo stack quando sto programmando in un linguaggio di alto livello, né dovrei farlo.
Per illustrare come funziona, ho intenzione di offrire alcuni semplici esempi in C #. Affinché questi esempi siano veri, dobbiamo fare un paio di ipotesi. Innanzitutto, il compilatore segue senza errori le specifiche C # e, in secondo luogo, produce programmi corretti.
Diciamo che voglio una semplice funzione che accetta una raccolta di stringhe e restituisce una stringa che è una concatenazione di tutte le stringhe nella raccolta separate da virgole. Un'implementazione semplice e ingenua in C # potrebbe apparire così:
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' string
oggetto è immutabile. Tuttavia, l'implementazione non è l'ideale. Poiché result
è immutabile, un nuovo oggetto stringa deve essere creato ogni volta attraverso il ciclo, sostituendo l'oggetto originale a cui result
punta. Ciò può influire negativamente sulla velocità e fare pressione sul cestino, poiché 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();
}
Si noti 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 ciascuna stringa a una raccolta di caratteri e restituisce il tutto alla fine.
Questa funzione è immutabile, anche se StringBuilder è mutabile?
Sì. Perché? Perché ogni volta che viene chiamata questa funzione, viene creato un nuovo StringBuilder, solo per quella chiamata. Quindi ora abbiamo una funzione pura che è thread-safe, ma contiene componenti mutabili.
E se lo facessi?
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 è thread-safe? No, non lo è. Perché? Perché la classe ora mantiene lo stato da cui dipende il mio metodo. Nel metodo è ora presente una condizione di competizione: un thread può essere modificato IsFirst
, ma un altro thread può eseguire il primo Append()
, nel qual caso ora ho una virgola all'inizio della mia stringa che non dovrebbe essere lì.
Perché dovrei voler farlo 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, chi lo sa?
Ad ogni modo, per sistemarlo, ho messo una lock
dichiarazione intorno alle viscere 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 è di nuovo sicuro per i thread.
L'unico modo in cui i miei metodi immutabili potrebbero non riuscire a essere thread-safe è se il metodo perde in qualche modo parte della sua implementazione. Questo potrebbe succedere? Non se il compilatore è corretto e il programma è corretto. Avrò mai bisogno di blocchi su tali metodi? No.
Per un esempio di come potrebbe essere trapelata l'implementazione in uno scenario di concorrenza, vedere qui .
but everything has a side effect
- Uh, no non lo fa. 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. Non importa che il computer usi l'elettricità. Possiamo parlare dei raggi cosmici che colpiscono anche le celle di memoria, se vuoi, ma manteniamo l'argomento pratico. Se vuoi considerare cose come il modo in cui la funzione esegue influenza il consumo di energia, questo è un problema diverso rispetto alla programmazione thread-safe.