Atores do Scala: receber vs reagir


110

Deixe-me primeiro dizer que tenho bastante experiência em Java, mas apenas recentemente me interessei por linguagens funcionais. Recentemente comecei a estudar Scala, que parece ser uma linguagem muito legal.

No entanto, tenho lido sobre a estrutura de Ator do Scala em Programação em Scala e há uma coisa que não entendo. No capítulo 30.4 ele diz que usar em reactvez de receivetorna possível reutilizar threads, o que é bom para o desempenho, uma vez que threads são caros na JVM.

Isso significa que, desde que me lembre de ligar em reactvez de receive, posso iniciar quantos atores eu quiser? Antes de descobrir Scala, estive brincando com Erlang, e o autor de Programming Erlang se orgulha de gerar mais de 200.000 processos sem suar a camisa. Eu odiaria fazer isso com threads Java. Que tipo de limites estou considerando no Scala em comparação com Erlang (e Java)?

Além disso, como essa reutilização de thread funciona no Scala? Vamos supor, para simplificar, que tenho apenas um segmento. Todos os atores que eu iniciar serão executados sequencialmente neste tópico ou ocorrerá algum tipo de troca de tarefas? Por exemplo, se eu iniciar dois atores que trocam mensagens de pingue-pongue, correrei o risco de um deadlock se eles forem iniciados no mesmo thread?

De acordo com a Programação em Scala , escrever atores para usar reacté mais difícil do que com receive. Isso parece plausível, já reactque não retorna. No entanto, o livro continua mostrando como você pode colocar reactum loop dentro de um usando Actor.loop. Como resultado, você obtém

loop {
    react {
        ...
    }
}

que, para mim, parece muito semelhante a

while (true) {
    receive {
        ...
    }
}

que é usado anteriormente no livro. Ainda assim, o livro diz que “na prática, os programas precisarão de pelo menos alguns receive”. Então, o que estou perdendo aqui? O que pode receivefazer isso reactnão pode, além de retornar? E por que eu me importo?

Finalmente, voltando ao cerne do que eu não entendo: o livro continua mencionando como o uso reacttorna possível descartar a pilha de chamadas para reutilizar o thread. Como isso funciona? Por que é necessário descartar a pilha de chamadas? E por que a pilha de chamadas pode ser descartada quando uma função termina lançando uma exceção ( react), mas não quando termina retornando ( receive)?

Tenho a impressão de que Programação em Scala tem passado por cima de algumas das questões-chave aqui, o que é uma pena, porque de outra forma é um livro realmente excelente.


Respostas:


78

Primeiro, cada ator aguardando receiveestá ocupando um segmento. Se ele nunca receber nada, esse segmento nunca fará nada. Um ator em reactnão ocupa nenhum segmento até que receba algo. Depois de receber algo, um thread é alocado para ele e inicializado nele.

Agora, a parte de inicialização é importante. Espera-se que um thread de recebimento retorne algo, um thread de reação não. Portanto, o estado da pilha anterior no final do último reactpode ser, e é, totalmente descartado. Não precisar salvar ou restaurar o estado da pilha torna o início do thread mais rápido.

Existem vários motivos de desempenho pelos quais você pode querer um ou outro. Como você sabe, ter muitos threads em Java não é uma boa ideia. Por outro lado, como você precisa anexar um ator a um thread antes que isso aconteça react, é mais rápido para receiveuma mensagem do que reactpara ela. Portanto, se você tiver atores que recebem muitas mensagens, mas fazem muito pouco com elas, o atraso adicional de reactpode torná-lo muito lento para seus objetivos.


21

A resposta é "sim" - se seus atores não estiverem bloqueando nada em seu código e você estiver usando react, então você pode executar seu programa "simultâneo" em um único thread (tente definir a propriedade do sistema actors.maxPoolSizepara descobrir).

Uma das razões mais óbvias pelas quais é necessário descartar a pilha de chamadas é que, caso contrário, o loopmétodo terminaria em a StackOverflowError. Como está, o framework termina habilmente a reactlançando a SuspendActorException, que é capturado pelo código de loop que então executa reactnovamente por meio do andThenmétodo.

Dê uma olhada no mkBodymétodo em Actore, em seguida, no seqmétodo para ver como o loop se reprograma - coisas terrivelmente inteligentes!


20

Essas declarações de "descartar a pilha" me confundiram também por um tempo e acho que entendi agora e este é o meu entendimento agora. No caso de "receber", há um bloqueio de thread dedicado na mensagem (usando object.wait () em um monitor) e isso significa que a pilha de threads completa está disponível e pronta para continuar a partir do ponto de "espera" ao receber um mensagem. Por exemplo, se você tivesse o seguinte código

  def a = 10;
  while (! done)  {
     receive {
        case msg =>  println("MESSAGE RECEIVED: " + msg)
     }
     println("after receive and printing a " + a)
  }

o thread esperaria na chamada de recepção até que a mensagem fosse recebida e então continuaria e imprimiria a mensagem "após receber e imprimir uma mensagem 10" e com o valor de "10" que está no frame da pilha antes do thread ser bloqueado.

No caso de react não existir tal thread dedicado, todo o corpo do método react é capturado como um encerramento e executado por algum thread arbitrário no ator correspondente que recebe uma mensagem. Isso significa que apenas as instruções que podem ser capturadas apenas como um encerramento serão executadas e é aí que o tipo de retorno de "Nada" entra em ação. Considere o seguinte código

  def a = 10;
  while (! done)  {
     react {
        case msg =>  println("MESSAGE RECEIVED: " + msg)
     }
     println("after react and printing a " + a) 
  }

Se react tivesse um tipo de retorno de void, significaria que é legal ter declarações após a chamada "react" (no exemplo a declaração println que imprime a mensagem "after react e impressão de 10"), mas na realidade que nunca seria executado, pois apenas o corpo do método "react" é capturado e sequenciado para execução posterior (na chegada de uma mensagem). Uma vez que o contrato de react tem o tipo de retorno "Nothing", não pode haver nenhuma declaração após react e, portanto, não há razão para manter a pilha. No exemplo acima, a variável "a" não teria que ser mantida, já que as instruções após as chamadas de reação não são executadas. Observe que todas as variáveis ​​necessárias pelo corpo de react já foram capturadas como um fechamento, portanto, pode ser executado perfeitamente.

A estrutura de ator java Kilim realmente faz a manutenção da pilha, salvando a pilha que é desenrolada ao receber uma mensagem.


Obrigado, isso foi muito informativo. Mas você não quis dizer +anos trechos de código, em vez de +10?
jqno

Ótima resposta. Eu também não entendi.
santiagobasulto


0

Não fiz nenhum trabalho importante com o scala / akka, mas entendo que há uma diferença muito significativa na forma como os atores são escalados. Akka é apenas um threadpool inteligente que corta o tempo de execução dos atores ... Cada fatia de tempo será uma execução de mensagem até a conclusão por um ator, ao contrário de Erlang, que poderia ser por instrução ?!

Isso me leva a pensar que react é melhor, pois sugere que o encadeamento atual considere outros atores para agendamento onde, como recebimento, "pode" envolver o encadeamento atual para continuar executando outras mensagens para o mesmo ator.

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.