A imutabilidade elimina completamente a necessidade de bloqueios na programação de vários processadores?


39

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.


21
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.
Robert Harvey

5
@RobertHarvey - Talvez eu esteja apenas usando uma definição diferente de efeito colateral e eu deveria ter dito "efeito colateral do mundo real". Sim, matemáticos têm funções sem efeitos colaterais. O código que é executado em uma máquina do mundo real exige recursos da máquina para executá-la, independentemente de alterar ou não dados. A função no seu exemplo coloca seu valor de retorno na pilha na maioria das arquiteturas de máquinas.
GlenPeterson

1
Se você pode realmente passar por isso, eu acho que sua pergunta vai para o coração deste trabalho infame research.microsoft.com/en-us/um/people/simonpj/papers/...
Jimmy Hoffa

6
Para os propósitos de nossa discussão, 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, na qual 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.
Robert Harvey

1
@RobertHarvey Spoerism; Mônadas heh E você pode reunir isso nas primeiras duas páginas. Mencionei isso porque, ao longo do todo, detalha uma técnica para lidar com os efeitos colaterais de uma maneira praticamente pura, tenho certeza de que responderia à pergunta de Glen; o futuro para mais leituras.
Jimmy Hoffa

Respostas:


35

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.


+1 Isso faz sentido, mas você poderia dar um exemplo de onde, em um idioma profundamente imutável, você ainda precisa se preocupar em lidar com a concorrência corretamente? Você afirma que você faz, mas tal cenário não é claro para mim
Jimmy Hoffa

@ JimmyHoffa Em uma linguagem imutável, você ainda precisa atualizar o estado entre os threads. As duas linguagens mais imutáveis ​​que conheço (Clojure e Haskell) fornecem um tipo de referência (átomos e Mvars) que fornecem uma maneira de enviar um estado modificado entre threads. A semântica de seus tipos de referência impede certos tipos de erros de simultaneidade, mas outros ainda são possíveis.
Stonemetal

@stonemetal interessante, nos meus 4 meses com Haskell eu ainda nem tinha ouvido falar de Mvars, sempre ouvi usar o STM para comunicação de estado de concorrência que se comporta mais como a mensagem de Erlang que eu pensava. Embora o exemplo perfeito de imutabilidade não resolva problemas simultâneos que eu possa imaginar seja a atualização de uma interface do usuário, se você tiver 2 threads tentando atualizar uma interface do usuário com versões diferentes de dados, um poderá ser mais novo e, portanto, precisará obter a segunda atualização para ter uma condição de corrida onde você deve garantir o seqüenciamento de alguma forma .. pensamento interessante .. Obrigado pelos detalhes
Jimmy Hoffa

1
@jimmyhoffa - O exemplo mais comum é o IO. Mesmo que o idioma seja imutável, seu banco de dados / site / arquivo não é. Outro é o seu mapa típico / redução. Imutabilidade significa que a agregação do mapa é mais infalível, mas você ainda precisa lidar com a coordenação 'depois que todo o mapa for feito em paralelo, reduza'.
Telastyn 25/10/12

1
@ JimmyHoffa: MVars 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.
CA McCann

13

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 stringobjeto é imutável. No entanto, a implementação não é ideal. Por resultser 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 resultum 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 lockdeclaraçã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 .


2
A menos que eu esteja enganado, porque a 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 o IEnumeratorser while(iter.MoveNext())editado, mas, a menos que IEnumeratorseja imutável (duvidoso), isso ameaçaria esmagar o ciclo foreach.
Jimmy Hoffa

É verdade que você deve assumir que a coleção nunca é gravada enquanto os threads a lêem. Isso seria uma suposição válida, se cada thread que chama o método cria sua própria lista.
Robert Harvey

Eu não acho que você possa chamá-lo de 'puro' quando tiver aquele objeto mutável que ele está usando por referência. Se ele recebeu um IEnumerable, você pode fazer essa reivindicação porque não pode adicionar ou remover elementos de um IEnumerable, mas pode ser uma matriz ou uma lista entregue como IEnumerable para que o contrato IEnumerable não garanta nenhum formulário de pureza. A técnica real para tornar essa função pura seria imutabilidade com a passagem por cópia; o C # não faz isso; portanto, você teria que copiar a lista quando a função a receber; mas a única maneira de fazer isso é com um foreach sobre ele ...
Jimmy Hoffa

1
@JimmyHoffa: Caramba, você me deixou obcecado com esse problema de galinha e ovo! Se você encontrar uma solução em qualquer lugar, entre em contato.
Robert Harvey

1
Acabei de me deparar com esta resposta agora e é uma das melhores explicações sobre o tópico que me deparei, os exemplos são super concisos e realmente facilitam o processo de grok. Obrigado!
Stephen Byrne

4

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.


3
A segunda parte está perguntando "Qual é a troca de desempenho entre bloqueios e estruturas imutáveis?" Provavelmente merece sua própria pergunta, se é que pode responder.
Robert Harvey

4

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:

do
{
   oldState = someObject.State;
   newState = oldState.WithSomeChanges();
} while (Interlocked.CompareExchange(ref someObject.State, newState, oldState) != oldState;

Se dois threads tentarem atualizar someObject.statesimultaneamente, 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.


4

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.


2
const std::stringé um péssimo exemplo e um pouco de arenque vermelho. As strings C ++ são mutáveis ​​e, de constqualquer maneira, não podem garantir a imutabilidade. Tudo o que faz é dizer que apenas constfunções podem ser chamadas. No entanto, essas funções ainda podem alterar o estado interno e constpodem ser descartadas. Finalmente, existe o mesmo problema que qualquer outro idioma: apenas porque minha referência é constnão significa que sua referência também. Não, uma estrutura de dados verdadeiramente imutável deve ser usada.
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.