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 react
vez de receive
torna 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 react
vez 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á react
que não retorna. No entanto, o livro continua mostrando como você pode colocar react
um 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 receive
fazer isso react
nã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 react
torna 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.
fonte
Respostas:
Primeiro, cada ator aguardando
receive
está ocupando um segmento. Se ele nunca receber nada, esse segmento nunca fará nada. Um ator emreact
nã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
react
pode 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 parareceive
uma mensagem do quereact
para ela. Portanto, se você tiver atores que recebem muitas mensagens, mas fazem muito pouco com elas, o atraso adicional dereact
pode torná-lo muito lento para seus objetivos.fonte
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 sistemaactors.maxPoolSize
para descobrir).Uma das razões mais óbvias pelas quais é necessário descartar a pilha de chamadas é que, caso contrário, o
loop
método terminaria em aStackOverflowError
. Como está, o framework termina habilmente areact
lançando aSuspendActorException
, que é capturado pelo código de loop que então executareact
novamente por meio doandThen
método.Dê uma olhada no
mkBody
método emActor
e, em seguida, noseq
método para ver como o loop se reprograma - coisas terrivelmente inteligentes!fonte
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
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
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.
fonte
+a
nos trechos de código, em vez de+10
?Só para ter aqui:
Programação baseada em eventos sem inversão de controle
Esses artigos estão vinculados ao scala api for Actor e fornecem a estrutura teórica para a implementação do ator. Isso inclui porque o react pode nunca mais voltar.
fonte
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.
fonte