Uma função que aceita algum valor e retorna outro valor, e não perturba nada fora da função, não tem efeitos colaterais e, portanto, é segura para threads. Se você quiser considerar coisas como a maneira como a função é executada afeta o consumo de energia, esse é um problema diferente.
Suponho que você esteja se referindo a uma máquina completa de Turing que esteja executando algum tipo de linguagem de programação bem definida, onde os detalhes da implementação são irrelevantes. Em outras palavras, não importa o que a pilha esteja fazendo, se a função que estou escrevendo na minha linguagem de programação preferida puder garantir imutabilidade dentro dos limites da linguagem. Não penso na pilha quando estou programando em uma linguagem de alto nível, nem preciso.
Para ilustrar como isso funciona, vou oferecer alguns exemplos simples em C #. Para que esses exemplos sejam verdadeiros, precisamos fazer algumas suposições. Primeiro, que o compilador siga a especificação C # sem erros e, segundo, que produz programas corretos.
Digamos que eu queira uma função simples que aceite uma coleção de strings e retorne uma string que seja uma concatenação de todas as strings da coleção separadas por vírgulas. Uma implementação simples e ingênua em C # pode ser assim:
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;
}
Este exemplo é imutável, prima facie. Como eu sei disso? Porque o string
objeto é imutável. No entanto, a implementação não é ideal. Por result
ser imutável, um novo objeto de seqüência de caracteres deve ser criado a cada vez no loop, substituindo o objeto original queresult
aponta para. Isso pode afetar negativamente a velocidade e pressionar o coletor de lixo, pois ele precisa limpar todas essas seqüências extras.
Agora, digamos que eu faça isso:
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();
}
Observe que eu substituí string
result
um objeto mutável StringBuilder
,. Isso é muito mais rápido que o primeiro exemplo, porque uma nova string não é criada a cada vez no loop. Em vez disso, o objeto StringBuilder simplesmente adiciona os caracteres de cada sequência a uma coleção de caracteres e gera a coisa toda no final.
Essa função é imutável, mesmo que StringBuilder seja mutável?
Sim. Por quê? Como toda vez que essa função é chamada, um novo StringBuilder é criado, apenas para essa chamada. Portanto, agora temos uma função pura que é segura para threads, mas contém componentes mutáveis.
Mas e se eu fizesse isso?
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();
}
}
Este método é seguro para threads? Não é não. Por quê? Porque a classe agora está mantendo o estado do qual meu método depende. Uma condição de corrida agora está presente no método: um thread pode ser modificado IsFirst
, mas outro pode executar o primeiro Append()
. Nesse caso, agora tenho uma vírgula no início da minha string que não deveria estar lá.
Por que eu poderia querer fazer assim? Bem, eu quero que os threads acumulem as strings no meuresult
sem levar em conta a ordem ou a ordem em que os threads entrassem. Talvez seja um logger, quem sabe?
Enfim, para consertar, coloquei uma lock
declaração em torno das entranhas do método.
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();
}
}
}
Agora é seguro para threads novamente.
A única maneira pela qual meus métodos imutáveis podem falhar na segurança de threads é se o método vazar parte de sua implementação. Isso poderia acontecer? Não se o compilador estiver correto e o programa estiver correto. Vou precisar de bloqueios em tais métodos? Não.
Para um exemplo de como a implementação poderia vazar em um cenário de simultaneidade, consulte aqui .
but everything has a side effect
- Não, não faz. Uma função que aceita algum valor e retorna outro valor, e não perturba nada fora da função, não tem efeitos colaterais e, portanto, é segura para threads. Não importa que o computador use eletricidade. Podemos falar sobre raios cósmicos atingindo células de memória também, se você preferir, mas vamos manter o argumento prático. Se você quiser considerar coisas como a maneira como a função é executada afeta o consumo de energia, isso é um problema diferente da programação segura para threads.