Simultaneidade composicional em Java ou qualquer outra linguagem de programação


8

Enquanto eu lia um trabalho de pesquisa sobre concorrência, chamado Software and the Concurrency Revolution ( versão html ). Me deparei com as seguintes linhas:

Infelizmente, embora os bloqueios funcionem, eles apresentam sérios problemas para o desenvolvimento moderno de software. Um problema fundamental dos bloqueios é que eles não são compostáveis . Você não pode pegar dois pedaços de código corretos baseados em bloqueio, combiná-los e saber que o resultado ainda está correto. O desenvolvimento de software moderno depende da capacidade de compor bibliotecas em programas maiores e, portanto, é uma dificuldade séria que não podemos construir componentes baseados em bloqueio sem examinar suas implementações.

  1. Eu estava pensando, como o Java garante simultaneidade composível ou mesmo existe uma maneira de produzir esses cenários.

  2. E como podemos sincronizar dados em uma ou mais bibliotecas? Um programador pode fazer isso a partir do seu programa ou depende da biblioteca sincronizar as coisas.

  3. Se não for Java, existe alguma outra linguagem que use simultaneidade baseada em bloqueio e garanta simultaneidade composível?

Também é retirado do mesmo trabalho:

Há pelo menos três grandes problemas com métodos sincronizados. Primeiro, eles não são apropriados para tipos cujos métodos chamam funções virtuais em outros objetos (por exemplo, Vector de Java e SyncHashTable de Java), porque chamar código de terceiros enquanto mantém um bloqueio abre a possibilidade de conflito . Segundo, os métodos sincronizados podem executar muito bloqueio, adquirindo e liberando bloqueios em todas as instâncias do objeto, mesmo aquelas nunca compartilhadas entre os threads (geralmente a maioria). Terceiro, os métodos sincronizados também podem executar muito pouco bloqueio, não preservando a atomicidade quando um programa chama vários métodos em um objeto ou em objetos diferentes. Como um exemplo simples deste último, considere uma transferência bancária: account1.Credit (amount); account2.Debit (amount) ...

Nota: O artigo foi publicado em setembro de 2005



@ErikEidt paper possui 94 Citações

3
Não é uma resposta para a pergunta, mas você obtém uma concorrência muito mais bem comportada quando restringe as interações à passagem de mensagens: Processos (processos completos, não apenas threads) se comunicando através de filas de mensagens assíncronas. É verdade que não é tão eficiente quanto o modelo baseado em ameaças, mas se o envio de uma mensagem não puder bloquear, você não poderá travar, a menos que tenha uma dependência real de dados circulares codificada em suas mensagens. Mais importante, qualquer mensagem é trivialmente composta / interpretada como uma operação atômica, de modo que todas as dores de cabeça do estado consistente desaparecem.
cmaster - restabelecer monica 26/03

1
Memória transacional de software é uma construção de simultaneidade que pode ser composta. Haskell tem uma biblioteca STM.
comingstorm 28/03

1
@penguin: simultaneidade! = paralelismo. Javascript (node.js) é um bom exemplo de um idioma com simultaneidade muito alta (na verdade todas as E / S são simultâneas), mas é de thread único. Java possui uma estrutura bastante boa para isso na forma de futuros (CompletionStage). Obviamente, o quanto seu código é livre de bloqueios depende da implementação, mas as ferramentas existem para você desenvolver.
slebetman

Respostas:


7

Não é a linguagem Java. É a natureza dos bloqueios (mutexes).

Existem maneiras melhores de obter simultaneidade aprimorada e, ao mesmo tempo, garantir a correção, formas que são independentes do idioma:

  1. Usando objetos imutáveis, para que você não precise de bloqueios.
  2. Usando promessas e estilo de passagem de continuação.
  3. Usando estruturas de dados sem bloqueio.
  4. Usando a memória transacional de software para compartilhar o estado com segurança.

Todas essas técnicas permitem simultaneidade aprimorada sem usar bloqueios. Nenhum deles depende especificamente da linguagem Java.


3

Eu estava pensando, como o Java garante simultaneidade composível ou mesmo existe uma maneira de produzir esses cenários.

Como o artigo diz, não é possível garantir a composição ao usar bloqueios junto com métodos virtuais (ou outro mecanismo semelhante, como passar funções como parâmetros). Se um trecho de código tiver acesso a métodos virtuais provenientes de outro trecho de código e ambos potencialmente usarem bloqueios, para compor com segurança (ou seja, sem o risco de um impasse) compor os dois trechos de código, será necessário inspecionar o código fonte de ambos.

E como podemos sincronizar dados em uma ou mais bibliotecas? Um programador pode fazer isso a partir do seu programa ou depende da biblioteca sincronizar as coisas.

Geralmente, cabe ao programador usar as bibliotecas para fazer a sincronização. Dessa forma, o programador sabe onde estão todos os bloqueios e pode garantir que eles não entrem em conflito.

Se não for Java, existe alguma outra linguagem que use simultaneidade baseada em bloqueio e garanta simultaneidade composível?

Novamente, o objetivo do artigo é que isso não é possível.


O artigo foi publicado em 2005. Sua resposta é baseada no mesmo artigo. Os idiomas evoluíram bastante desde 2005. Você pode dar qualquer referência mais recente ou pode reconfirmar sua resposta.

2
Esse documento não fala sobre nada específico de 2005. Ainda será verdade em 2105 que as bibliotecas baseadas em bloqueio não são compostas.
svick

Java está constantemente atualizando suas construções de bloqueio, é possível comparar o pacote de simultaneidade Java em Java5 até Java8.

Isso não importa. O Java 8 certamente não eliminou a possibilidade de um conflito ao usar bloqueios, então nada mudou sobre isso.
svick

2
@penguin - É impossível tornar uma linguagem totalmente compostável, uma vez que possui recursos não composíveis, porque você não pode garantir que os módulos fechados não os usem de maneira perigosa. Você pode tornar seu código compostável, mesmo se estiver usando bloqueios (embora seja necessário ter cuidado com a forma de usá-los nesse caso), mas não tenha certeza de que bibliotecas arbitrárias e / ou componentes do sistema não o são.
Jules

1

Mecanismos de bloqueio de baixo nível são inerentemente incontroláveis. Isso ocorre principalmente porque os bloqueios chegam ao mundo para afetar a máquina que executa as instruções.

As bibliotecas Java subsequentes adicionaram mecanismos de nível cada vez mais alto para garantir a operação multithread correta. Eles fazem isso restringindo o uso lock()e volatilea certas circunstâncias conhecidas e controláveis. Por exemplo, uma implementação de fila simultânea tem um comportamento muito localizado e permite raciocinar sobre estados antes e depois. O uso de mecanismos de nível superior significa que você precisa ler menos da especificação ou código para fazer a correção. Mas, e esse é um grande, mas você ainda precisa entender o modelo de bloqueio de qualquer subsistema e como ele interage. Além disso, as alterações no Java para simultaneidade após o Java 5 estão quase exclusivamente relacionadas às bibliotecas e não à linguagem.

O principal problema de qualquer mecanismo de bloqueio é que ele afeta o estado e opera no domínio do tempo. Nem humanos nem computadores raciocinam bem sobre estado ou tempo. É a capacidade de raciocinar sobre valor e estrutura que permitiu aos cientistas da computação criar mônadas, a primeira coisa que me vem à mente em relação à composibilidade em uma linguagem.

O mais próximo que chegamos é de processos sequenciais de comunicação . Isso ainda requer um mecanismo de alto nível, como caixas de correio e passagem de mensagens. Na minha humilde opinião, o CSP ainda não lida adequadamente com os grandes sistemas (o objetivo final do software compositável) ou com o raciocínio baseado no tempo.


1
Isso não responde à minha pergunta. Eu queria saber se o Java é composable ou existe alguma outra linguagem que seja? De acordo com as duas respostas que recebi, o Java não é compostável, mas as duas respostas não fornecem nenhuma evidência concreta.

Você queria uma prova de que o Java usando apenas bloqueios não pode ser composto? Além disso, se você ler o artigo CSP acima, verá vários idiomas mencionados que são composíveis. Meus comentários se referem a qualquer idioma onde lock()e volatilesão a granularidade da sincronização de threads ou processos.
BobDalgleish

Ok, há algo que me interessa. Na sua resposta, você disse: "Quanto mais chegamos ...", o que você quer dizer com isso. Como você descobriu que o CSP é o mais próximo?

1

Antes de tudo, agradeço a todos os membros que responderam a essa pergunta, especialmente a Robert Harvey, cuja resposta parece muito próxima da minha.

Eu pesquisei sobre conceitos de simultaneidade por dois anos e, de acordo com minhas descobertas, nenhuma linguagem fornece uma garantia de que suas construções de simultaneidade sejam compostas. Código de execução perfeitamente bom usando estruturas de dados imutáveis ​​e o STM também pode produzir resultados inesperados porque, sob o capô, o STM usa bloqueios. O STM é muito bom para operações atômicas, mas se falarmos sobre composibilidade de contrastes de simultaneidade entre módulos, há uma chance (muito pequena) de que o STM possa não funcionar como esperado.

Ainda assim, podemos minimizar a incerteza usando o seguinte (técnicas / métodos / construções):

  1. Evitando bloqueios e sincronização
  2. Usando STM
  3. Usando estruturas de dados persistentes (imutáveis)
  4. Evitando compartilhar estado
  5. Funções puras
  6. Estilo de programação assíncrona
  7. Evitando a alternância frequente de contexto
  8. O paradigma multiencadeamento e a natureza de seu ambiente desempenham papel importante

Talvez a objeção mais fundamental seja [...] que os programas baseados em bloqueio não sejam compostos: fragmentos corretos podem falhar quando combinados. - Tim Harris et al., "Transações de memória compostas", Seção 2: Histórico, pág.2

Atualizar

Graças a Jules, estou corrigido. O STM usa várias implementações e a maioria delas é livre de bloqueios. Mas ainda acredito que o STM é uma boa solução aqui, mas não a perfeita, e tem desvantagens:

Cada leitura (ou gravação) de um local de memória de dentro de uma transação é realizada por uma chamada para uma rotina STM para leitura (ou gravação) de dados. Com código sequencial, esses acessos são executados por uma única instrução da CPU. As rotinas de leitura e gravação do STM são significativamente mais caras que as instruções correspondentes da CPU, pois geralmente precisam manter os dados da contabilidade sobre cada acesso. A maioria dos STMs verifica se há conflitos com outras transações simultâneas, registra o acesso e, no caso de uma gravação, registra o valor atual (ou antigo) dos dados, além de ler ou gravar o local da memória acessada. Algumas dessas operações usam instruções caras de sincronização e acessam metadados compartilhados, o que aumenta ainda mais seus custos. Tudo isso reduz o desempenho de thread único quando comparado ao código seqüencial. - Por que o STM pode ser mais do que um brinquedo de pesquisa - página 1

Veja também estes documentos:

Esses documentos têm alguns anos. As coisas podem ter mudado / melhorado, mas não todas.


"Um código de execução perfeitamente bom usando estruturas de dados imutáveis ​​e o STM também pode produzir resultados inesperados porque, sob o capô, o STM usa bloqueios". Você pode fornecer um exemplo de um resultado tão inesperado? Tanto quanto sei, o STM é considerado confiável e compostável, mas talvez você esteja ciente de algo que não sou ...?
Jules

Além disso, você afirma que o STM "usa bloqueios", mas isso é apenas um detalhe de implementação de algumas versões dele. O STM pode ser implementado em um sistema totalmente livre de bloqueios usando atualizações atômicas; consulte dl.acm.org/citation.cfm?id=1941579
Jules

1

Ouvi dizer por pesquisadores respeitados que qualquer mecanismo de sincronização útil pode ser usado para criar um impasse. Memória transacional (seja hardware ou software) não é diferente. Por exemplo, considere esta abordagem para escrever uma barreira de encadeamento:

transaction {
    counter++;
}

while (true) {
    transaction {
        if (counter == num_threads)
            break;
    }
}

(Nota: o exemplo é retirado de um artigo de Yannis Smaragdakis no PACT 2009)

Se ignorarmos o fato de que essa não é uma boa maneira de sincronizar um grande número de threads, parece estar correto. Mas não é compostável. A decisão de colocar a lógica em duas transações é essencial. Se chamássemos isso de outra transação, de modo que tudo achatasse em uma transação, provavelmente nunca concluiríamos.

O mesmo se aplica aos canais de passagem de mensagens: os ciclos de comunicação podem causar conflitos. A sincronização ad-hoc com atômicos C ++ pode levar a conflitos. RCU, bloqueios de sequência, bloqueios de leitores / gravadores, variáveis ​​de condição e semáforos podem ser usados ​​para criar bloqueios.

Isso não quer dizer que transações ou canais (ou bloqueios ou RCU) sejam ruins. Em vez disso, é para dizer que algumas coisas simplesmente não parecem possíveis. Mecanismos de controle de simultaneidade escaláveis, compostáveis ​​e sem patologia provavelmente não são possíveis.

A melhor maneira de evitar problemas não é procurar um mecanismo de bala de prata, mas usar rigorosamente bons padrões. No mundo da computação paralela, um bom ponto de partida é a Programação Paralela Estruturada: Padrões para Computação Eficiente , de Arch Robison, James Reinders e Michael McCool. Para programação simultânea, existem alguns bons padrões (consulte o comentário de @ gardenhead), mas é improvável que os programadores de C ++ e Java os usem. Um padrão que mais pessoas poderiam começar a usar as maneiras corretas é substituir as sincronizações ad-hoc nos programas por uma fila de múltiplos consumidores multiprodutor. E a TM é definitivamente melhor do que bloqueios, pois aumenta o nível de abstração, para que os programadores se concentrem no que precisa ser atômico , nãocomo implementar um protocolo de bloqueio inteligente para garantir a atomicidade. Felizmente, à medida que a TM de hardware melhorar e os idiomas adicionarem mais suporte à TM, chegaremos a um ponto em que a TM substitui os bloqueios no caso comum.


1
"Para programação simultânea, os padrões não são tão bem desenvolvidos." Isso é evidentemente falso. Modelos concorrentes de computação remontam a décadas. O modelo de ator foi inventado no início dos anos 70, e várias formas de cálculo de processos foram inventadas e estudadas desde então. A questão não está nos bons modelos de computação; cabe aos programadores que não desejam usar esses modelos e possíveis ineficiências em sua implementação.
precisa saber é o seguinte

Excelente ponto, @gardenhead. Os modelos foram desenvolvidos, mas ninguém os usa. Vou editar minha resposta.
Mike lança
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.