Parte 1
Claramente a imutabilidade minimiza a necessidade de bloqueios na programação de vários processadores, mas elimina essa necessidade ou há casos em que a imutabilidade por si só não é suficiente? Parece-me que você só pode adiar o processamento e encapsular o estado tanto tempo antes que a maioria dos programas realmente faça algo (atualizar um repositório de dados, produzir um relatório, lançar uma exceção etc.). Essas ações sempre podem ser realizadas sem bloqueios? A mera ação de jogar fora cada objeto e criar um novo em vez de alterar o original (uma visão grosseira da imutabilidade) fornece proteção absoluta contra a disputa entre processos, ou existem casos de canto que ainda exigem bloqueio?
Conheço muitos programadores e matemáticos funcionais que gostam de falar sobre "sem efeitos colaterais", mas no "mundo real" tudo tem um efeito colateral, mesmo que seja o tempo necessário para executar uma instrução de máquina. Estou interessado na resposta teórica / acadêmica e na resposta prática / do mundo real.
Se a imutabilidade é segura, dados certos limites ou suposições, quero saber exatamente quais são as fronteiras da "zona de segurança". Alguns exemplos de possíveis limites:
- I / O
- Exceções / erros
- Interações com programas escritos em outros idiomas
- Interações com outras máquinas (físicas, virtuais ou teóricas)
Agradecimentos especiais a @JimmaHoffa por seu comentário que iniciou esta pergunta!
Parte 2
A programação multiprocessador é frequentemente usada como uma técnica de otimização - para fazer com que algum código seja executado mais rapidamente. Quando é mais rápido usar bloqueios do que objetos imutáveis?
Dado os limites estabelecidos na Lei de Amdahl , quando você pode obter um desempenho geral melhor (com ou sem o coletor de lixo levado em consideração) com objetos imutáveis versus bloqueio de objetos mutáveis?
Sumário
Estou combinando essas duas perguntas em uma para tentar chegar onde a caixa delimitadora é a Imutabilidade como uma solução para os problemas de segmentação.
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.Respostas:
Essa é uma pergunta estranhamente formulada que é muito, muito ampla se respondida completamente. Vou me concentrar em esclarecer algumas das especificidades sobre as quais você está perguntando.
Imutabilidade é uma troca de design. Isso torna algumas operações mais difíceis (modificar o estado de objetos grandes rapidamente, criar objetos fragmentados, manter um estado de execução etc.) em favor de outros (depuração mais fácil, raciocínio mais fácil sobre o comportamento do programa, sem ter que se preocupar com as coisas que estão mudando embaixo de você ao trabalhar simultaneamente). É com este último que nos preocupamos com essa pergunta, mas quero enfatizar que é uma ferramenta. Uma boa ferramenta que geralmente resolve mais problemas do que causa (na maioria dos programas modernos ), mas não é uma bala de prata ... Não é algo que altera o comportamento intrínseco dos programas.
Agora, o que você recebe? A imutabilidade dá a você uma coisa: você pode ler o objeto imutável livremente, sem se preocupar com a mudança de estado debaixo de você (supondo que ele seja realmente profundamente imutável ... Ter um objeto imutável com membros mutáveis geralmente causa problemas). É isso aí. Isso evita que você precise gerenciar a simultaneidade (por meio de bloqueios, instantâneos, particionamento de dados ou outros mecanismos; o foco da pergunta original nos bloqueios é ... Incorreto, dado o escopo da pergunta).
Acontece que muitas coisas lêem objetos. IO, mas o próprio IO tende a não lidar bem com o uso simultâneo. Quase todo o processamento funciona, mas outros objetos podem ser mutáveis ou o próprio processamento pode usar um estado que não é amigável à simultaneidade. Copiar um objeto é um grande problema oculto em alguns idiomas, pois uma cópia completa nunca é (quase) uma operação atômica. É aqui que objetos imutáveis o ajudam.
Quanto ao desempenho, isso depende do seu aplicativo. Os bloqueios são (geralmente) pesados. Outros mecanismos de gerenciamento de simultaneidade são mais rápidos, mas têm um alto impacto no seu design. Em geral , um design altamente simultâneo que utiliza objetos imutáveis (e evita suas fraquezas) terá um desempenho melhor do que um design altamente simultâneo que bloqueia objetos mutáveis. Se o seu programa é levemente simultâneo, depende e / ou não importa.
Mas o desempenho não deve ser sua maior preocupação. Escrever programas concorrentes é difícil . Depurar programas concorrentes é difícil . Objetos imutáveis ajudam a melhorar a qualidade do seu programa, eliminando oportunidades de erro ao implementar o gerenciamento de simultaneidades manualmente. Eles facilitam a depuração porque você não está tentando rastrear o estado em um programa simultâneo. Eles tornam seu design mais simples e, assim, removem erros.
Para resumir: a imutabilidade ajuda, mas não elimina os desafios necessários para lidar adequadamente com a concorrência. Essa ajuda tende a ser generalizada, mas os maiores ganhos são da perspectiva da qualidade, e não do desempenho. E não, a imutabilidade não o impede magicamente de gerenciar a simultaneidade em seu aplicativo, desculpe.
fonte
MVar
s são uma primitiva de simultaneidade mutável de baixo nível (tecnicamente, uma referência imutável a um local de armazenamento mutável), não muito diferente do que você veria em outros idiomas; impasses e condições de corrida são muito possíveis. O STM é uma abstração de simultaneidade de alto nível para memória compartilhada mutável sem bloqueio (muito diferente da passagem de mensagens) que permite transações composíveis sem possibilidade de conflitos ou condições de corrida. Dados imutáveis são seguros para threads, nada mais a dizer sobre isso.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:
Este exemplo é imutável, prima facie. Como eu sei disso? Porque o
string
objeto é imutável. No entanto, a implementação não é ideal. Porresult
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:
Observe que eu substituí
string
result
um objeto mutávelStringBuilder
,. 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?
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 primeiroAppend()
. 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 meu
result
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.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 .
fonte
List
é mutável, na primeira função que você reivindicou 'pura', outro encadeamento poderia remover todos os elementos da lista ou adicionar muito mais enquanto estiver no loop foreach. Não tenho certeza de como isso aconteceria com oIEnumerator
serwhile(iter.MoveNext())
editado, mas, a menos queIEnumerator
seja imutável (duvidoso), isso ameaçaria esmagar o ciclo foreach.Não tenho certeza se entendi suas perguntas.
IMHO a resposta é sim. Se todos os seus objetos forem imutáveis, você não precisará de bloqueios. Mas se você precisar preservar um estado (por exemplo, implementar um banco de dados ou agregar os resultados de vários encadeamentos), precisará usar a mutabilidade e, portanto, também bloquear. A imutabilidade elimina a necessidade de bloqueios, mas geralmente você não pode se dar ao luxo de ter aplicativos completamente imutáveis.
Resposta à parte 2 - os bloqueios devem ser sempre mais lentos que os sem bloqueios.
fonte
Encapsular um monte de estados relacionados em uma única referência mutável a um objeto imutável pode possibilitar que muitos tipos de modificação de estado sejam executados sem bloqueios usando o padrão:
Se dois threads tentarem atualizar
someObject.state
simultaneamente, os dois objetos lerão o estado antigo e determinarão o que seria o novo estado sem as alterações um do outro. O primeiro thread a executar o CompareExchange armazenará o que acha que deveria ser o próximo estado. O segundo encadeamento descobrirá que o estado não corresponde mais ao que havia lido anteriormente e, assim, recalculará o próximo estado apropriado do sistema com as alterações do primeiro encadeamento em vigor.Esse padrão tem a vantagem de que um encadeamento que é desviado não pode bloquear o progresso de outros encadeamentos. Tem a vantagem adicional de que, mesmo quando há contenção intensa, algum segmento sempre estará progredindo. Porém, ele tem a desvantagem de que, na presença de contenção, muitos threads podem gastar muito tempo realizando um trabalho que acabarão descartando. Por exemplo, se todos os 30 threads em CPUs separadas tentarem alterar um objeto simultaneamente, o primeiro será bem-sucedido em sua primeira tentativa, um na segunda, um no terceiro, etc. para atualizar seus dados. O uso de um bloqueio "consultivo" pode melhorar significativamente as coisas: antes de um encadeamento tentar uma atualização, ele deve verificar se um indicador de "contenção" está definido. Se então, ele deve adquirir um bloqueio antes de fazer a atualização. Se um thread fizer algumas tentativas sem êxito de uma atualização, ele deverá definir o sinalizador de contenção. Se um encadeamento que tenta obter o bloqueio descobrir que não havia mais ninguém esperando, ele deve limpar o sinalizador de contenção. Observe que a trava aqui não é necessária para "correção"; código funcionaria corretamente mesmo sem ele. O objetivo do bloqueio é minimizar a quantidade de tempo gasto pelo código em operações que provavelmente não serão bem-sucedidas.
fonte
Você começa com
Claramente a imutabilidade minimiza a necessidade de bloqueios na programação de vários processadores
Errado. Você precisa ler atentamente a documentação de todas as aulas que utiliza. Por exemplo, const std :: string em C ++ não é seguro para threads. Objetos imutáveis podem ter um estado interno que muda ao acessá-los.
Mas você está vendo isso de um ponto de vista totalmente errado. Não importa se um objeto é imutável ou não, o que importa é se você o altera. O que você está dizendo é como dizer "se você nunca fizer um exame de direção, nunca poderá perder sua carteira de motorista por dirigir embriagado". É verdade, mas falta de entender.
Agora, no código de exemplo, alguém escreveu com uma função chamada "ConcatenateWithCommas": Se a entrada fosse mutável e você usasse um bloqueio, o que você obteria? Se alguém tentar modificar a lista enquanto você tenta concatenar as cadeias, um bloqueio pode impedir que você trate. Mas você ainda não sabe se concatenar as seqüências antes ou depois do outro segmento alterá-las. Portanto, seu resultado é bastante inútil. Você tem um problema que não está relacionado ao bloqueio e não pode ser corrigido com o bloqueio. Mas se você usar objetos imutáveis e o outro encadeamento substituir o objeto inteiro por um novo, você estará usando o objeto antigo e não o novo, portanto seu resultado será inútil. Você precisa pensar nesses problemas em um nível funcional real.
fonte
const std::string
é um péssimo exemplo e um pouco de arenque vermelho. As strings C ++ são mutáveis e, deconst
qualquer maneira, não podem garantir a imutabilidade. Tudo o que faz é dizer que apenasconst
funções podem ser chamadas. No entanto, essas funções ainda podem alterar o estado interno econst
podem ser descartadas. Finalmente, existe o mesmo problema que qualquer outro idioma: apenas porque minha referência éconst
não significa que sua referência também. Não, uma estrutura de dados verdadeiramente imutável deve ser usada.