Você está na página 1de 50

13Compartilhando estados mutáveis ​com

atores

Nissocapítulo

Entendendo o modelo de ator


Usando mensagens assíncronas
Construindo uma estrutura de ator
Colocando os atores para trabalhar
Otimizando o desempenho do ator

Nos capítulos anteriores, você aprendeu muitas técnicas para ajudá-lo a escrever
programas mais seguros. A maioria dessas técnicas vem da programação funcio-
nal. Uma dessas técnicas consiste em usar dados imutáveis ​para evitar mutações
de estado. Programas sem estados mutáveis ​são mais seguros, confiáveis ​e fáceis
de projetar e dimensionar.

Você aprendeu como os estados mutáveis ​podem ser manipulados de maneira


funcional passando o estado adiante como um argumento para as funções. Você
viu vários exemplos dessa técnica. Você aprendeu a gerar fluxos de dados pas-
sando o estado de um gerador junto com cada novo valor. (Se você não se lembra
disso, revise o exercício 9.29 onde você implementou a unfold funçãopassando
cada valor gerado junto com o novo estado do gerador.) No capítulo 12, você tam-
bém aprendeu como passar o console como um parâmetro para enviar saída para
a tela e receber entrada do teclado. Essas técnicas podem ser amplamente aplica-
das em muitos domínios. Mas isso é frequentementeentendido como significando
que as técnicas de programação funcional ajudam a compartilhar com segurança
o estado mutável. Isso está completamente errado.

O uso de estruturas de dados imutáveis, por exemplo, não ajuda no compartilha-


mento do estado mutável. Ele evita o compartilhamento não intencional de um es-
tado mutável removendo a mutação do estado. Passar o estado como parte de um
parâmetro de função e retornar um novo estado (imutável) como parte do resul-
tado (na forma de um par contendo o resultado e o novo estado) é perfeitamente
adequado ao lidar com um único thread. Mas enquanto você precisar comparti-
lhar a mutação de estado entre threads, o que é quase sempre o caso em aplicati-
vos modernos, estruturas de dados imutáveis ​não ajudam. Para compartilhar esse
tipo de dado, é preciso uma referência mutável a ele para que os novos dados
imutáveis ​possam substituir os anteriores.

Imagine que você queira contar quantas vezes uma função é chamada. Em um
aplicativo de thread único, você pode fazer isso adicionando o contador ao argu-
mento da função e retornando o contador incrementado como parte do resultado.
Mas a maioria dos programadores imperativos prefere incrementar o contador
como um efeito colateral. Isso funcionaria perfeitamente porque há apenas um
único thread, portanto, nenhum bloqueio é necessário para impedir o possível
acesso simultâneo. Isso é o mesmo que viver em uma ilha deserta. Se você é o
único habitante, não há necessidade de trancar as portas. Mas em um programa
multithreaded, como incrementar o contador de forma segura, evitando o acesso
concorrente? A resposta geralmente é usar bloqueios ou tornar as operações atô-
micas, ou ambos.

Na programação funcional, o compartilhamento de recursos tem que ser feito


como um efeito, o que significa, mais ou menos, que cada vez que você acessa um
recurso compartilhado, você tem que sair da segurança funcional e tratar esse
acesso como fez para entrada/saída (I/ O) no capítulo 12. Isso significa que você
deve gerenciar bloqueios e sincronização sempre que precisar compartilhar um
estado mutável? De jeito nenhum.

Como você aprendeu nos capítulos anteriores, a programação funcional também


é sobre levar a abstração ao limite. O compartilhamento de um estado mutável
pode ser abstraído de forma que você possa usá-lo sem se preocupar com os deta-
lhes. Uma maneira de conseguir isso é usar uma estrutura de ator.

Neste capítulo, você não desenvolverá uma estrutura de ator real e completa.
Criar uma estrutura de ator completa é um trabalho tão grande que você deve
usar uma já existente. Aqui você desenvolverá uma estrutura de ator mínima que
lhe dará a sensação do que uma estrutura de ator traz para a programação
funcional.

13.1 O modelo do ator

DentroNo modelo de ator, um aplicativo multithread é dividido em componentes


de thread único, chamados atores . Se cada ator for de thread único, ele não preci-
sará compartilhar dados usando bloqueios ou sincronização.

Atores se comunicam com outros atores por meio de efeitos, como se essa comu-
nicação fosse a E/S de mensagens. Isso significa que os atores contam com um me-
canismo para serializar as mensagens que recebem. (Aqui, serialização significali-
dando com uma mensagem após a outra. Isso não deve ser confundido com a seri-
alização de objetos.) Devido a esse mecanismo, os atores podem processar mensa-
gens uma por vez sem ter que se preocupar com o acesso simultâneoaos seus re-
cursos internos. Como resultado, um sistema de atores pode ser visto como uma
série de programas funcionais que se comunicam entre si por meio de efeitos.
Cada ator pode ser de thread único, portanto, não há acesso simultâneo aos recur-
sos internos. A simultaneidade é abstraída dentro da estrutura.
13.1.1 Noções básicas sobre mensagens assíncronas

Comoparte do processamento de mensagens, os atores podem enviar mensagens


para outros atores. As mensagens são enviadas de forma assíncrona , o que signi-
fica que um ator não precisa esperar por uma resposta - não há uma. Assim que
uma mensagem é enviada, o remetente pode continuar seu trabalho, que consiste
principalmente em processar uma de cada vez uma fila de mensagens que recebe.
Lidar com a fila de mensagens significa que precisa haver alguns acessos simultâ-
neos à fila. Mas esse gerenciamento é abstraído no framework do ator, então
você, o programador, não precisa se preocupar com isso.

Respostas a mensagens podem ser necessárias em alguns casos. Suponha que um


ator seja responsável por uma longa computação. O cliente pode tirar proveito da
comunicação assíncrona continuando seu próprio trabalho enquanto a computa-
ção é feita para ele. Mas uma vez feito o cálculo, deve haver uma maneira de o cli-
ente receber o resultado. Isso é feito tornando o ator responsável pelo callback da
computação seu cliente e enviando o resultado, novamente de forma assíncrona.
Observe que o cliente pode ser o remetente original, embora nem sempre precise
ser ocaso.

13.1.2 Manipulando a paralelização

oO modelo de ator permite que as tarefas sejam paralelizadas usando um ator ge-
rente que é responsável por dividir a tarefa em subtarefas e distribuí-las a vários
atores trabalhadores. Cada vez que um ator trabalhador retorna um resultado ao
gerente, ele recebe uma nova subtarefa. Esse modelo oferece uma vantagem so-
bre outros modelos de paralelização, pois nenhum ator trabalhador fica ocioso
até que a lista de subtarefas esteja vazia. A desvantagem é que o ator gerente não
participa da computação. Mas em um aplicativo real, isso geralmente não faz ne-
nhuma diferença perceptível.
Para algumas tarefas, os resultados das subtarefas podem precisar ser reordena-
dos ao serem recebidos. Nesse caso, o ator gerente provavelmente enviará os re-
sultados para um ator específico responsável por esse trabalho. Você verá um
exemplo disso na seção 13.3. Em programas pequenos, o próprio gerenciador
pode cuidar dessa tarefa. Na figura 13.1 , esse ator é chamado de Receptorator.

13.1.3 Manipulando a mutação do estado do ator

atorespodem ser sem estado (imutáveis) ou com estado, o que significa que eles
devem mudar de estado de acordo com as mensagens que recebem. Por exemplo,
um ator sincronizador pode receber os resultados de cálculos que devem ser reor-
denados antes de serem usados.

Imagine, por exemplo, que você tenha uma lista de dados que devem passar por
uma computação pesada para fornecer uma lista de resultados. Em suma, este é
um mapeamento. Ele pode ser paralelizado dividindo a lista em várias sublistas e
fornecendo essas sublistas aos agentes de trabalho para processamento. Mas não
há garantia de que os atores trabalhadores terminarão seus trabalhos na mesma
ordem em que esses trabalhos foram dados a eles.
Figura 13.1 O ator principal produz a tarefa principal e a envia ao gerente, que a divide em subtarefas que
são processadas em paralelo por vários atores trabalhadores. Subresultadossão enviados de volta ao gerente,
que os repassa ao destinatário. Depois de agrupar os subresultados, o receptor envia o resultado final para o
ator principal.

Uma solução para ressincronizar os resultados é numerar as tarefas. Quando um


trabalhador envia de volta o resultado, ele adiciona o número da tarefa corres-
pondente para que o destinatário possa colocar os resultados em uma fila priori-
tária. Isso não apenas permite a classificação automática, mas também torna pos-
sível processar os resultados como um fluxo assíncrono. Cada vez que o receptor
recebe um resultado, ele compara o número da tarefa com o número esperado. Se
houver correspondência, ele passa o resultado para o cliente e, em seguida, con-
sulta a fila de prioridade para ver se o primeiro resultado disponível corresponde
ao novo número de tarefa esperado. Se houver uma correspondência, o processo
de desenfileiramento continua até que não haja mais correspondência. Se o resul-
tado recebido não corresponder ao número do resultado esperado, ele é adicio-
nado à fila de prioridade.
Nesse projeto, o ator receptor precisa lidar com dois dados mutáveis: a fila de pri-
oridade e o número do resultado esperado. Isso significa que o ator deve usar pro-
priedades mutáveis? Isso não seria grande coisa, mas, como os atores são single-
threaded, não é necessário. Como você verá, a manipulação de mutações de pro-
priedade pode ser incluída e abstraída em um processo geral de mutação de es-
tado, permitindo que o programador use apenasimutáveldados.

13.2 Uma implementação de estrutura de ator

DentroNesta seção, você aprenderá como construir uma estrutura de ator mí-
nima, mas totalmente funcional. Ao criar essa estrutura, você aprenderá como
uma estrutura de ator permite o compartilhamento seguro de estado mutável, pa-
ralelização e reserialização fáceis e seguras e arquitetura modular de aplicativos.
No final deste capítulo, você verá algumas coisas gerais que pode fazer com as es-
truturas de ator.

Sua estrutura de ator será feita desses quatro componentes:

a Actor interfacedetermina o comportamento de um ator.


a AbstractActor classecontém todas as coisas que são comuns a todos os ato-
res. Esta classe será estendida por atores de negócios.
o ActorContext atua como uma forma de acessar os atores. Em sua imple-
mentação, esse componente será minimalista e será usado principalmente
para acessar o estado do ator.

Este componente não é necessário em uma implementação tão pequena, mas im-
plementações mais sérias usam tal componente. Este contexto permite, por exem-
plo, pesquisar atores disponíveis.
a MessageProcessor interfaceserá a interface que você implementará para
qualquer componente que tenha que lidar com uma mensagem recebida.

13.2.1 Entendendo as limitações

ComoEu disse, a implementação que você criará aqui é minimalista; considerá-lo


uma maneira de entender e praticar o uso do modelo de ator. Você perderá mui-
tas das funções de um sistema de ator real, particularmente aquelas relacionadas
ao contexto do ator. Uma outra simplificação é que cada ator é mapeado para um
único thread. Em um sistema de ator real, os atores são mapeados para pools de
threads, permitindo que milhares ou mesmo milhões de atores sejam executados
em algumas dezenas de threads.

Outra limitação de sua implementação será em relação aos atores remotos. A mai-
oria dos frameworks de atores permite que atores remotos sejam manipulados de
forma transparente, o que significa que você pode usar atores que estão rodando
em máquinas diferentes sem ter que se preocupar com a comunicação. Isso torna
as estruturas de ator uma maneira ideal de criar aplicativos escaláveis. eu não
vou lidar com issoaspecto neste livro.

13.2.2 Projetando as interfaces da estrutura do ator

Primeirovocê precisa definir as interfaces que constituirão sua estrutura de ator.


O mais importante é a Actor interfaceque define várias funções. Aqui está a fun-
ção principal desta interface:

fun tell(mensagem: T, remetente: Resultado<Ator<T>>)

Esta função é usada para enviar uma mensagem ao this ator (ou seja, o ator que
detém a função). Isso significa que, para enviar uma mensagem a um ator, você
deve ter uma referência a ele. (Isto é diferente de estruturas de atores reais em
que as mensagens não são enviadas para atores, mas para referências de atores,
proxies ou algum outro substituto. Sem esse aprimoramento, não seria possível
enviar mensagens para atores remotos.) Esta função ( que na verdade é um efeito)
recebe a Result<Actor> como segundo parâmetro. É suposto representar o re-
metente, mas às vezes é definido como ninguém (o resultado vazio) ou para um
ator diferente.

Outras funções são usadas para gerenciar o ciclo de vida do ator para facilitar o
uso de atores, mostradas na Listagem 13.1 . Este código não pretende usar os re-
sultados dos exercícios dos capítulos anteriores, mas sim o fpinkotlin-com-
mon móduloque está disponível no código que acompanha este livro (
https://github.com/pysaumont/fpinkotlin ). Este é basicamente o mesmo código
das soluções dos exercícios, mas com algumas funções adicionais.

Listagem 13.1 A Actor interface

interface Ator<T> {

contexto val: ActorContext<T> ①

fun self(): Result<Actor<T>> = Result(this) ②

fun tell(mensagem: T, remetente: Resultado<Ator<T>> =self()) ③

desligamento divertido() ④

fun tell(mensagem: T, remetente: Ator<T>) =


tell(mensagem, Resultado(remetente)) ⑤

objeto complementar {
fun <T> noSender(): Resultado<Ator<T>> = Resultado() ⑥
}
}

① A função de propriedade de contexto permite acessar o contexto do ator.

② A função self retorna um Result deste ator.

③ O padrão do argumento do remetente é self() para simplificar o envio de mensa-


gens sem a necessidade de indicar o remetente.

④ A função de desligamento informa ao ator que ele deve encerrar a si mesmo. Em


sua estrutura mínima, ele termina de forma limpa o encadeamento do ator.

⑤ Esta é uma função de conveniência que envia uma mensagem com uma referên-
cia de ator em vez de um Result<Actor>.

⑥ A função noSender é uma função auxiliar para fornecer um Result.Empty com o


tipo Result<Actor>.

A listagem a seguir mostra as duas outras interfaces necessárias: ActorCon-


text e MessageProcessor .

Listagem 13.2 As interfaces ActorContext e MessageProcessor

interface AtorContext<T> {

fun behavior(): MessageProcessor<T> ①

divertido se tornar(comportamento: MessageProcessor<T>) ②


}
interface MessageProcessor<T> {

processo divertido (mensagem: T,


remetente: Resultado<Ator<T>>) ③
}

① Permite acesso ao comportamento do ator

② Permite que um ator mude seu comportamento registrando um novo


MessageProcessor

③ A interface MessageProcessor possui apenas uma função, que representa o proces-


samento de uma mensagem.

O elemento mais importante aqui é a ActorContext interface. a become fun-


çãopermite que um ator mude seu comportamento, ou seja, a maneira como ele
processa as mensagens. Como você pode ver, o comportamento de um ator se pa-
rece com um efeito, tomando como argumento um par composto pela mensagem
a processar e o remetente.

Durante a vida do aplicativo, o comportamento de cada ator pode mudar. Geral-


mente essa mudança de comportamento é causada por uma modificação no es-
tado do ator, substituindo o comportamento original por um novo. Isso ficará
mais claro quando você veraimplementação.

13.3 A implementação AbstractActor

o AbstractActor A implementação representa a parte da implementação de um


ator que é comum a todos os atores. Todas as operações de gerenciamento de
mensagens são comuns e são fornecidas pelo framework do ator, de modo que
você só terá que implementar a parte de negócios. A listagem a seguir mostra a
AbstractActor implementação.

Listagem 13.3 A AbstractActor implementação

classe abstrata AbstractActor<T>(id válido protegido: String): Actor<T> {

substituir val context: ActorContext<T> =


objeto: ActorContext<T> { ①

comportamento var: MessageProcessor<T> =


objeto: MessageProcessor<T> { ②

substituir processo divertido (mensagem: T, remetente: Resultado<Ator<T>>) {


onReceive(mensagem, remetente)
}
}

@Sincronizado
sobrepor
fun torne-se(comportamento: MessageProcessor<T>) { ③
this.behavior = comportamento
}

substituir comportamento divertido () = comportamento


}

executor val privado: ExecutorService = ④


Executors.newSingleThreadExecutor(DaemonThreadFactory())

diversão abstrata onReceive(mensagem: T,


remetente: Resultado<Ator<T>>) ⑤

override fun self(): Resultado<Ator<T>> {


return Resultado(este)
}

substituir o desligamento divertido () {


this.executor.shutdown()
}

@Sincronizado
sobrescrever fun tell(mensagem: T,
remetente: Resultado<Ator<T>>) { ⑥
executor.execute {
tentar {
context.behavior()
.process(mensagem, remetente) ⑦
} catch (e: RejectedExecutionException) {
/*
* Isso provavelmente é normal e significa todas as tarefas pendentes
* foram cancelados porque o ator foi parado.
*/
} catch (e: Exceção) {
lance RuntimeException(e)
}
}
}
}

① Inicializa a propriedade de contexto para um novo ActorContext

② Delega o comportamento padrão para a função onReceived

③ Para alterar seu comportamento, o ActorContext registra o novo comportamento.


É aqui que a mutação ocorre, mas é ocultada pela estrutura.
④ Inicializa o ExecutorService subjacente

⑤ Mantém o processamento do negócio, implementado pelo usuário da API

⑥ A função tell é como um ator recebe uma mensagem. É sincronizado para garantir
que as mensagens sejam processadas uma de cada vez.

⑦ Quando uma mensagem é recebida, ela é processada pelo comportamento atual


retornado pelo contexto do ator.

o ExecutorService é inicializado com um executor de thread único usando


uma fábrica de threads daemon para permitir o desligamento automático quando
o thread principal termina. Observe também que quando o ExecutorService é
inicializado, o DaemonThreadFactory cria threads de daemon para que os atores
não impeçam que o aplicativo pare quando o thread principal parar. (Você encon-
trará o código correspondente no repositório deste livro.)

Sua estrutura de ator agora está completa, embora, como mencionei antes, este
não seja um código de produção. Este é um exemplo mínimo para mostrar como
uma estrutura de ator podetrabalhar.

13.4 Colocando os atores para trabalhar

Agora que você tem uma estrutura de ator à sua disposição, é hora de aplicá-la a
alguns problemas concretos. Os atores são úteis quando vários threads devem
compartilhar algum estado mutável, como quando um thread produz o resultado
de uma computação e esse resultado deve ser passado para outro thread para
processamento posterior.

Normalmente, esse compartilhamento de estado mutável é feito armazenando va-


lores em propriedades mutáveis ​compartilhadas, o que implica bloqueio e sincro-
nização. Você primeiro tentará um exemplo de ator mínimo, que pode ser consi-
derado como o “ Hello, World! ” dos atores. Você então construirá um aplica-
tivo mais completo onde um ator é usado para distribuir tarefas para outros ato-
res trabalhando em paralelo.

O primeiro exemplo é o exemplo tradicional usado para testar atores. É composto


por dois jogadores de pingue-pongue e um árbitro. O jogo começa quando a bola,
representada por um número inteiro, é dada a um jogador. Cada jogador então
manda a bola para o outro até que isso aconteça dez vezes, momento em que a
bola é devolvida ao árbitro.

13.4.1 Implementando o exemplo Ping Pong

Primeirovocê vai implementar o árbitro. Tudo o que você precisa fazer é criar um
ator, implementando sua onReceive função. Nesta função, você exibirá uma
mensagem conforme a listagem a seguir.

Listagem 13.4 Criando o referee objeto

val árbitro = objeto : AbstractActor<Int>("Árbitro") {


override fun onReceive(message: Int, sender: Result<Actor<Int>>) {
println("O jogo terminou após $message shots")
}
}

Em seguida, você criará os dois jogadores. Como há duas instâncias, você tem
duas opções. A abordagem de objeto é criar Player classecomo mostra a listagem
a seguir.

Listagem 13.5 A Player classe


classe privada Player(id: String, ①
private val sound: String, ②
árbitro de valor privado: Ator<Int>):
AbstractActor<Int>(id) { ③

override fun onReceive(message: Int, sender: Result<Actor<Int>>) {


println("$som - $mensagem") ④
if (mensagem >= 10) {
referee.tell(mensagem, remetente) ⑤
} outro {
remetente.paraCada(
{ ator: Ator<Int> -> ⑥
ator.tell(mensagem + 1, self())
},
{ referee.tell(message, sender) } ⑦
)
}
}
}

① A classe é privada porque é definida no nível do pacote. Você também pode defini-
la como uma classe local, dentro da função principal do programa.

② A sequência de som é uma mensagem exibida pelos jogadores quando eles rece-
bem a bola (seja Ping ou Pong).

③ Cada jogador é criado com uma referência ao árbitro para que um jogador possa
devolver a bola ao árbitro quando o jogo terminar. Isso não seria necessário se a
classe Player fosse definida localmente na mesma função do árbitro (por exem-
plo, na main função).
④ Esta é a parte comercial do ator, ou seja, a parte que faz o que o usuário espera
ver.

⑤ Se o jogo terminar, devolve a bola ao árbitro.

⑥ Caso contrário, envia de volta para o outro jogador, se presente.

⑦ Se o outro jogador não estiver presente, registre o problema com o árbitro.

Se preferir o modo funcional, você pode criar uma função retornando um Ac-
tor conforme mostrado na listagem a seguir.

Listagem 13.6 A player função

jogador divertido(id: String,


som: String, ①
árbitro: Ator<Int>) =
objeto: AbstractActor<Int>("id") { ②

override fun onReceive(message: Int, sender: Result<Actor<Int>>) {


println("$som - $mensagem") ③
if (mensagem >= 10) {
referee.tell(mensagem, remetente) ④
} outro {
remetente.paraCada(
{ ator: Ator<Int> -> ⑤
ator.tell(mensagem + 1, self())
},
{ referee.tell(mensagem, remetente) } ⑥
)
}
}
}

① A sequência de som é uma mensagem exibida pelos jogadores quando eles rece-
bem a bola (seja Ping ou Pong).

② Cria cada jogador com uma referência ao árbitro para que um jogador possa de-
volver a bola ao árbitro quando o jogo terminar

③ Esta é a parte comercial do ator.

④ Se o jogo terminar, devolve a bola ao árbitro

⑤ Caso contrário, envie de volta para o outro jogador, se presente.

⑥ Se o outro jogador não estiver presente, registra um problema com o árbitro

Como você pode ver, essas duas soluções são quase idênticas, mostrando que os
objetos são de fato funções.

Com a player função criada (ou a Player classe), você pode finalizar seu pro-
grama. Mas você precisa de uma maneira de manter o aplicativo em execução até
que o jogo termine. Sem isso, o thread principal do aplicativo é encerrado assim
que o jogo é iniciado e os jogadores não terão a oportunidade de jogar. Isso pode
ser feito através do uso de um semáforo, conforme mostrado na listagem a seguir.

Listagem 13.7 O exemplo do Ping Pong

semáforo val privado = Semáforo(1) ①

fun main(args: Array<String>) {


val árbitro = objeto : AbstractActor<Int>("Árbitro") {

override fun onReceive(message: Int, sender: Result<Actor<Int>>) {


println("O jogo terminou após $message shots")
semaphore.release() ②
}
}

val jogador1 =
jogador("Jogador1", "Ping", árbitro) ③
val jogador2 = jogador("Jogador2", "Pong", árbitro)

semaphore.acquire() ④
player1.tell(1, Resultado(player2))
semaphore.acquire() ⑤
// thread principal termina ⑥
}

① Um semáforo é criado com uma permissão.

② Terminado o jogo, o semáforo é liberado, disponibilizando uma nova permissão


que permite a retomada da thread principal.

③ Se você preferir definir uma Player classe, a única diferença será maiúscula
Player em vez de player .

④ A permissão única disponível é adquirida pelo thread atual e o jogo é iniciado.

⑤ A thread principal tenta adquirir uma nova permissão. Como não há nenhum dis-
ponível, ele bloqueia até que o semáforo seja liberado.
⑥ Ao retomar, o thread principal termina. Todos os encadeamentos de atores são da-
emons, então eles também param automaticamente.

Este programa exibe o seguinteresultado:

Ping - 1
Pong - 2
Ping - 3
Pong - 4
Ping - 5
Pong - 6
Ping - 7
Pong - 8
Ping - 9
Pong - 10
Jogo terminou após 10 tiros

13.4.2 Executando um cálculo em paralelo

Isso éagora é hora de examinar um exemplo mais sério do modelo de ator em


ação: executar uma computação em paralelo. Para simular um cálculo de longa
duração, você escolherá uma lista de números aleatórios entre 0 e 30 e calculará o
valor de Fibonacci correspondente usando um algoritmo lento.

A aplicação é composta por três tipos de atores: a Manager , encarregado de criar


um determinado número de atores trabalhadores e distribuir as tarefas para eles;
várias instâncias de trabalhadores; e um cliente, que é implementado na classe
principal do programa como um ator anônimo. A listagem a seguir mostra a mais
simples dessas classes, o Worker ator.

Listagem 13.8 O Worker ator responsável por executar partes da computação


class Worker(id: String) : AbstractActor<Int>(id) {

override fun onReceive(message: Int, sender: Result<Actor<Int>>) {


sender.forEach (onSuccess = { a: Actor<Int> ->
a.tell(slowFibonacci(mensagem), self()) ①
})
}

diversão privada slowFibonacci(número: Int): Int {


retornar quando (número) {
0 -> 1
1 -> 1
else -> slowFibonacci(número - 1)
+ slowFibonacci(número - 2) ②
}
}
}

① Quando o Worker recebe um número, ele reage calculando o valor de Fibonacci


correspondente e enviando-o de volta ao chamador.

② Um algoritmo ineficiente é usado de propósito para criar tarefas de longa duração.

Como você pode ver, este ator é apátrida. Ele calcula o resultado e o envia de volta
ao remetente ao qual recebeu uma referência. Isso pode ser um ator diferente do
chamador.

Como os números são escolhidos aleatoriamente entre 0 e 35, o tempo necessário


para computar o resultado é variável. Esta listagem simula tarefas que levam
quantidades variáveis ​de tempo para serem executadas. Ao contrário do exemplo
de paralelização automática no capítulo 8, todos os threads/atores são mantidos
ocupados até que toda a computação seja concluída.
a Manager classeé um pouco mais complicado. A listagem a seguir mostra o cons-
trutor da classe e as propriedades que são inicializadas.

Listagem 13.9 O construtor e as propriedades da Manager classe

class Manager(id: String, lista: List<Int>,


private val client: Actor<Result<List<Int>>>, ①
private val workers: Int) : AbstractActor<Int>(id) { ②

private val inicial: List<Pair<Int, Int>> ③


private val workList: List<Int> ④
private val resultList: List<Int> ⑤
private val managerFunção:
(Gerente) -> (Comportamento) -> (Int) -> Unidade ⑥

iniciar {
val splitLists = list.splitAt(this.workers) ⑦
this.inicial =
splitLists.first.zipWithPosition() ⑧
this.workList = splitLists.second ⑨
this.resultList = List() ⑩

managerFunction = { gerente -> ⑪


{ comportamento ->
{ eu ->
resultado val =
behavior.resultList.cons(i) ⑫
if (resultado.comprimento == lista.comprimento) {
this.client.tell(Result(result)) ⑬
} outro {
gerente.contexto
.become(Behavior(behavior.workList ⑭
.tailSafe()
.getOrElse(Lista()), resultado))
}
}
}
}
}
...

① O Gerenciador armazena as referências ao seu cliente, para o qual enviará o resul-


tado do cálculo.

② O número de trabalhadores a serem usados ​é armazenado.

③ A lista inicial é uma lista de um par de inteiros, contendo tanto o número a proces-
sar (.first) quanto a posição na lista (.second).

④ A workList é a lista de tarefas restantes a serem executadas uma vez que todos os
atores trabalhadores tenham recebido sua primeira tarefa.

⑤ A lista de resultados contém os resultados dos cálculos.

⑥ A função manager é o coração do Manager, determinando o que ele será capaz de


fazer. Esta função é aplicada cada vez que o gestor recebe um resultado de um
trabalhador.

⑦ A lista de valores a serem processados ​é dividida pelo número de trabalhadores


para obter uma lista de tarefas iniciais e uma lista de tarefas restantes.

⑧ A lista de tarefas iniciais (números para os quais o valor de Fibonacci será calcu-
lado) é compactada com a posição de seus elementos. A posição (números de 0 a n
) são usados ​apenas para nomear os atores trabalhadores de 0 a n .
⑨ A workList é definida para as tarefas restantes.

⑩ A resultList é inicializada em uma lista vazia.

⑪ A função do gerente, representando o trabalho do gerente, é uma função curda do


próprio gerente, seu comportamento e a mensagem recebida (i), que será o resul-
tado de uma subtarefa.

⑫ Quando um resultado é recebido, ele é adicionado à lista de resultados, que é ob-


tida a partir do comportamento do gerente.

⑬ Se o comprimento da lista de resultados for igual ao comprimento da lista de en-


trada, o cálculo termina e o resultado é enviado ao cliente.

⑭ Caso contrário, a função make do contexto é chamada para alterar o comporta-


mento do Manager. Essa mudança de comportamento é uma mudança de estado.
O novo comportamento é criado com o final da workList e a lista atual de resulta-
dos (à qual o valor recebido foi adicionado).

Como você pode ver, se o cálculo for concluído, o resultado será adicionado à lista
de resultados e enviado ao cliente. Caso contrário, o resultado é adicionado à lista
de resultados atual. Na programação tradicional, isso seria feito alterando a lista
de resultados que seria mantida pelo arquivo Manager . Isso é exatamente o que
acontece aqui, exceto por duas diferenças:

A lista de resultados é armazenada no comportamento.


Nem o comportamento nem a lista são alterados. Em vez disso, um novo com-
portamento é criado e o contexto é alterado para manter o novo comporta-
mento como um substituto parao antigo. Mas você não precisa lidar com essa
mutação. Para você, tudo é imutável porque a mutação é abstraída pela estru-
tura do ator.

A listagem a seguir mostra a Behavior classeimplementado como uma classe in-


terna. A Behavior classe interna permite que você abstraia a mutação do ator.

Listagem 13.10 A Behavior classe interna

comportamento de classe interna interna


construtor interno(
interno val workList: List<Int>, ①
internal val resultList: List<Int>): MessageProcessor<Int> {

substituir processo divertido (mensagem: Int,


remetente: Resultado<Ator<Int>>) { ②
managerFunction(this@Manager)(this@Behavior)(message)
sender.forEach(onSuccess = { a: Actor<Int> ->
workList.headSafe()
.forEach({ a.tell(it, self()) }) { a.shutdown() }
})
}
}

① O Behavior é construído com a workList (da qual a cabeça foi removida antes de
chamar o construtor) e a resultList (à qual um resultado foi adicionado).

② A função de processo, que é chamada na recepção de uma mensagem, primeiro


aplica a managerFunction à mensagem recebida. Em seguida, ele envia a próxima
tarefa (o cabeçalho da workList) para o remetente (um ator Worker que irá pro-
cessá-la) ou, se a workList estiver vazia, instrui o ator Worker a desligar.

Isso cobre as partes principais do arquivo Manager . O restante é composto por


funções utilitárias que são utilizadas principalmente para iniciar o trabalho. A lis-
tagem a seguir mostra essas funções.

Listagem 13.11 As funções de utilidade do Manager

class Manager(id: String, list: List<Int>, . . .

. . .

começo divertido() {
onReceive(0, self()) ①
sequence(initial.map { this.initWorker(it) })
.forEach(onSuccess = { this.initWorkers(it) },
onFailure = ②
{ this.tellClientEmptyResult(
it.message ?: "Erro desconhecido") })
}

private fun initWorker(t: Pair<Int, Int>):


Resultado<() -> Unidade> = ③
Result({ Worker("Worker " + t.second).tell(t.first, self()) })

private fun initWorkers(lst: List<() -> Unit>) {


lst.forEach { it() } ④
}

privado
fun tellClientEmptyResult(string: String) { ⑤
client.tell(Result.failure("$string causada por lista de entrada vazia."))
}

substituir fun onReceive (mensagem: Int,


remetente: Resultado<Ator<Int>>) { ⑥
context.become(Behavior(workList, resultList))
}

. . .
}

① Para iniciar, o Gerente envia uma mensagem para si mesmo. Qual é a mensagem
não faz diferença, porque o comportamento ainda não foi inicializado.

② Os workers são então criados e inicializados.

③ Esta função cria uma função do tipo () -> Unidade criando um ator trabalhador.

④ Esta função executa a criação do ator.

⑤ Se houver algum erro, o cliente é informado.

⑥ Este é o comportamento inicial do gerente. Como parte de sua inicialização, ele


muda de comportamento, começando com a workList contendo as tarefas restan-
tes e a resultList vazia.

É importante entender que a onReceive funçãorepresenta o que o ator fará


quando receber sua primeira mensagem. Esta função não será chamada quando
os trabalhadores enviarem seus resultados ao gerente.

A última parte do programa é mostrada na listagem 13.12 . Isso representa o có-


digo do cliente para o aplicativo. Mas ao contrário do Manager e do Worker , não
é um ator. Em vez disso, a main função usa um ator. Esta é uma escolha de imple-
mentação. Não há razão específica para escolher uma solução ou outra. Mas um
ator cliente é necessário para receber o resultado.

Listagem 13.12 O aplicativo cliente

import com.fpinkotlin.common.List
import com.fpinkotlin.common.Result
import com.fpinkotlin.common.range
importar java.util.concurrent.Semaphore

private val semaphore = Semaphore(1) ①


private const val listLength = 20_000 ②
private const val workers = 8 ③
private val rnd = java.util.Random(0)
private val testList = ④
range(0, listLength).map { rnd.nextInt(35) }

fun main(args: Array<String>) {


semaphore.acquire() ⑤
val startTime = System.currentTimeMillis()
cliente val = ⑥
objeto: AbstractActor<Result<List<Int>>>("Cliente") {
substituir fun onReceive(message: Result<List<Int>>,
remetente: Result<Actor<Result<List<Int>>>>) {
message.forEach({ processSuccess(it) }, ⑦
{ processFailure(it.message ?: "Erro desconhecido") })
println("Tempo total: "
+ (System.currentTimeMillis() - startTime))
semaphore.release() ⑧
}
}
gerente val = ⑨
Manager("Gerente", testList, cliente, trabalhadores)
gerenciador.start()
semaphore.acquire() ⑩
}

processFailure divertido privado (mensagem: String) {


println(mensagem)
}

fun processSuccess(lst: List<Int>) {


println("Entrada: ${testList.splitAt(40).first}")
println("Resultado: ${lst.splitAt(40).first}")
}

① Um semáforo é criado para permitir que o thread principal espere que os atores
concluam seu trabalho.

② O número de tarefas é inicializado.

③ O número de atores trabalhadores é definido aqui.

④ A lista de tarefas é criada gerando aleatoriamente números entre 0 e 35.

⑤ O semáforo é adquirido quando o programa inicia.

⑥ Um ator cliente é criado como um objeto singelton anônimo.

⑦ A única responsabilidade do cliente é processar o resultado ou qualquer erro


ocorrido.
⑧ O cliente libera o semáforo ao receber o resultado.

⑨ O gerenciador é instanciado e iniciado.

⑩ O semáforo é adquirido novamente para aguardar o término do job.

Você pode executar este programa com vários comprimentos para a lista de tare-
fas e vários números de agentes de trabalho. Na minha caixa Linux de oito nú-
cleos, a execução com uma duração de tarefa de 20.000 fornece os seguintes
resultados:

Um ator trabalhador: 73 seg


Dois atores operários: 37 seg
Quatro atores operários: 19 seg
Oito atores operários: 12 seg
Dezesseis atores operários: 12 seg

Esses números não são precisos, mas mostram que usar um número de threads
maior que o número de núcleos disponíveis é inútil. O resultado exibido pelo pro-
grama é o seguinte (somente os primeiros 11 resultados são exibidos):

Entrada: [5, 23, 4, 2, 25, 28, 16, 1, 34, 9, 22, ..., NIL]
Resultado: [8, 5, 2, 1597, 46368, 121393, 2, 55, 28657, 1, 2, ..., NIL]
Tempo total: 12558

Como você pode ver, há umproblema!


13.4.3 Reordenando os resultados

Comovocê deve ter notado, o resultado não está correto. Isso é óbvio quando se
olha para o segundo valor aleatório (23) e para o resultado correspondente (5).
Você também pode comparar os seguintes valores e resultados. Se você executar o
programa em seu computador, obterá resultados diferentes para cada execução.

O que está acontecendo aqui é que nem todas as tarefas levam o mesmo tempo
para serem executadas. Eu deliberadamente defino a computação para ser execu-
tada dessa maneira, para que algumas tarefas (cálculos para valores de argu-
mento baixos) retornem rapidamente, enquanto outras (cálculos para valores
mais altos) demorem muito mais. Como resultado, os valores retornados não es-
tão na ordem correta.

Para corrigir esse problema, você precisa classificar os resultados na mesma or-
dem dos argumentos correspondentes. Uma solução é usar o Heap tipo de dados
desenvolvido no capítulo 11. Você pode numerar cada tarefa e usar esse número
como a prioridade em uma fila de prioridade.

A primeira coisa que você precisa mudar é o tipo dos atores trabalhadores. Em
vez de trabalhar com inteiros, eles precisarão trabalhar com tuplas de inteiros:
um inteiro representando o argumento da computação e outro representando o
número da tarefa. A listagem a seguir mostra as alterações correspondentes na
Worker classe.

Listagem 13.13 O Worker ator acompanhando o número da tarefa

classe Worker(id: String):


AbstractActor<Pair<Int, Int>>(id) { ①

substituir fun onReceive(message: Pair<Int, Int>,


remetente: Result<Actor<Pair<Int, Int>>>) { ②
sender.forEach(onSuccess =
{ a: Ator<Par<Int, Int>> ->
a.tell(Pair(slowFibonacci(message.first), ③
mensagem.segundo), self())
})
}
...
}

① O parâmetro de tipo é alterado de Int para Pair<Int, Int>.

② A assinatura da função onReceive é alterada para refletir o novo tipo de ator.

③ A mensagem de retorno é alterada para incluir o número da tarefa.

O número da tarefa é o segundo elemento da tupla. Isso não é fácil de ler, visto
que o número da tarefa e o argumento da computação são do mesmo tipo ( Int ).
Na vida real, isso não deveria acontecer porque você deveria usar um tipo especí-
fico para a tarefa. Mas se preferir, você também pode usar um tipo específico ao
invés de Pair para agrupar a tarefa e o número da tarefa, como um Task tipo
com uma propriedade de número.

Mudanças na Manager classesão mais numerosos. Você precisa alterar o tipo da


classe e os tipos das workList propriedades e do resultado:

class Manager(id: String, lista: List<Int>,


cliente val privado: Actor<Result<List<Int>>>,
trabalhadores val privados: Int): AbstractActor<Pair<Int, Int>>(id) {

private val inicial: List<Pair<Int, Int>>


private val workList: List<Pair<Int, Int>>
private val resultHeap: Heap<Pair<Int, Int>>
private val managerFunction: (Manager) -> (Behavior) -> (Int) -> Unit

Essas propriedades são inicializadas no construtor da seguinte maneira:

iniciar {
val splitLists = list.zipWithPosition().splitAt(this.workers)
this.initial = splitLists.first
this.workList = splitLists.second
this.resultHeap = Heap(Comparator {
p1: Pair<Int, Int>, p2: Pair<Int, Int> ->
p1.second.compareTo(p2.second)
})

O workList agora contém pares (como era o caso da initial lista no exemplo
anterior) e o resultado é uma fila de prioridade ( Heap ) de pares. Este Heap é ini-
cializado com um Comparator baseado na comparação do segundo elemento dos
pares.

Usar um Task tipo que envolva a tarefa e o número da tarefa permitiria que você
criasse esse tipo Comparable , de modo que a Comparator seria inútil. (Deixo
essa otimização como um exercício para você.) O managerFunction também é
diferente:

private val managerFunção:


(Gerente) -> (Comportamento) -> (Pair<Int, Int>) -> Unidade

É inicializado no construtor assim:


managerFunction = { gerente ->
{ comportamento ->
{p->
val result = behavior.resultHeap + p ①
if (resultado.tamanho == lista.comprimento) {
this.client.tell(Result(result.toList()
.map { it.first })) ②
} outro {
...
}
}
}
}

① O resultado recebido agora é inserido no Heap.

② Após a conclusão do cálculo, o Heap é convertido em uma lista antes de ser devol-
vido ao cliente.

A Behavior classe interna deve ser alterada para refletir a alteração do tipo de
ator:

comportamento de classe interna interna


interno
construtor(interno
val workList: List<Pair<Int, Int>>, ①
internal val resultHeap: Heap<Pair<Int, Int>>): ②
MessageProcessor<Pair<Int, Int>> { ③

sobrepor
fun process(mensagem: Pair<Int, Int>,
remetente: Resultado<Ator<Par<Int, Int>>>) { ④
managerFunction(this@Manager)(this@Behavior)(message)
sender.forEach(onSuccess = { a: Actor<Pair<Int, Int>> ->
workList.headSafe()
.forEach({ a.tell(it, self()) }) { a.shutdown() }
})
}
}

① O tipo de workList agora é List<Pair<Int, Int>>.

② O tipo do resultado agora é Heap<Pair<Int, Int>>.

③ O parâmetro de tipo da classe Behavior agora é Pair<Int, Int>.

④ A assinatura da função do processo é modificada para refletir a alteração do tipo


de parâmetro.

Você ainda precisa aplicar algumas pequenas alterações no restante da Mana-


ger classe. A start função deve ser modificada:

começo divertido() {
onReceive(Pair(0, 0), self()) ①
sequence(initial.map { this.initWorker(it) })
.forEach({ this.initWorkers(it) },
{ this.tellClientEmptyResult(
it.message ?: "Erro desconhecido") })
}

① O tipo da mensagem inicial deve corresponder ao parâmetro de tipo do agente


Manager.
O Worker processo de inicialização também é um pouco diferente:

private fun initWorker(t: Pair<Int, Int>): Resultado<() -> Unidade> =


Resultado({ Trabalhador("Trabalhador " + t.segundo)
.tell(Pair(t.primeiro, t.segundo), self()) })

Por último, a onReceive funçãoé modificado:

substituir fun onReceive(message: Pair<Int, Int>,


remetente: Resultado<Ator<Par<Int, Int>>>) {
context.become(Behavior(workList, resultHeap))
}

Agora os resultados são exibidos na forma corretaordem.

13.4.4 Otimizando o desempenho

Apesarfunciona bem, este exemplo está longe de ser ideal. O principal motivo é
que todos os resultados são colocados na fila de prioridade resultante para que se-
jam classificados. Como eu disse no capítulo 11, este não é o caso de uso correto
para uma fila de prioridade.

Uma fila de prioridade é projetada para colocar elementos que devem ser proces-
sados ​em uma determinada ordem (de acordo com sua prioridade). Os elementos
devem ser consumidos à medida que são produzidos, garantindo que a fila nunca
retenha mais do que alguns elementos por vez. No presente caso, os elementos de-
vem ser armazenados apenas enquanto existirem elementos de maior prioridade
que ainda não foram processados. Este não é o único caso de uso de uma fila de
prioridade, mas é perfeito.
Para ver o problema na prática, tente substituir a slowFibonnacci funçãona
Worker classecom um eficiente, como

diversão privada fibonacci(número: Int): Int {


tailrec fun fibonacci(acc1: Int, acc2: Int, x: Int): Int = quando (x) {
0 -> 1
1 -> acc1 + acc2
else -> fibonacci(acc2, acc1 + acc2, x - 1)
}
return fibonacci(0, 1, número)
}

No programa cliente, defina listLength como 500.000 e tente o programa com


1, 2, 4 ou 8 atores (se seu computador tiver núcleos suficientes). Aqui está um
exemplo dos resultados que obtenho:

1 ator: 40567 ms 2 atores: 24399 ms 4 atores: 22394 ms 8 atores: 22389 ms

Você pode notar uma coisa interessante aqui: quando as tarefas são curtas, o be-
nefício de ter vários atores simultâneos é muito menor. Normalmente, há um ga-
nho ao passar de um para dois atores. Colocar mais atores não traz mais desem-
penho. Pode não ser óbvio porque esses números dependem do computador
usado, mas são bastante lentos. Como comparação, você pode usar o código a se-
guir, reutilizando o testList do WorkersExample programa:

println(testList.map { fibonacci(it) }.splitAt(40).first)

Isso é executado em 700 ms, o que é cerca de 30 vezes mais rápido que a versão
de ator usando 2, 4 ou 8 atores. Uma razão para isso é o gargalo causado pelo
Heap . os Heap dadosestrutura não se destina à classificação. Ele fornece bom de-
sempenho desde que o número de elementos seja mantido baixo, mas aqui esta-
mos inserindo todos os 200.000 resultados na pilha, classificando o conjunto de
dados completo em cada inserção. Isso não é eficiente.

Obviamente, essa ineficiência não é um problema de implementação, mas um


problema de usar a ferramenta errada para o trabalho. Você obteria um desempe-
nho muito melhor armazenando todos os resultados e classificando-os uma vez
quando o cálculo terminasse, embora precisasse usar a ferramenta certa para
classificar.

Outra opção é alterar o design do programa. Um dos problemas com o design


atual é que não apenas a inserção no Heap demora muito tempo, mas também é
feita pelo Manager fio. Em vez de distribuir tarefas para os atores de trabalho as-
sim que eles terminam um cálculo, Manager eles esperam até que termine a in-
serção no heap. Uma solução possível seria usar um ator separado para inserir no
arquivo Heap .

Mas, às vezes, o melhor caminho a seguir é usar o trabalho certo para a ferra-
menta. O fato de você estar consumindo o resultado de forma síncrona pode não
ser um requisito. Se não for, você está adicionando um requisito implícito que
torna o problema mais difícil de resolver. Uma possibilidade seria passar os resul-
tados individualmente para o cliente. Dessa forma, o Heap seria usado apenas
quando os resultados estiverem fora de ordem, evitando que fique muito grande.

Esse tipo de uso é como uma fila de prioridade deve ser usada. Para levar isso em
consideração, você pode adicionar um Receiver atorao seu programa. O Recei-
ver ator é mostrado na listagem a seguir.
Listagem 13.14 O Receiver ator responsável por receber os resultados de forma
assíncrona

import com.fpinkotlin.common.List
import com.fpinkotlin.common.Result

classe Receptor(id: String,


cliente val privado: Actor<List<Int>>): ①
AbstractActor<Int>(id) { ②

private val receiverFunction: (Receptor) -> (Comportamento) -> (Int) -> Unidade

iniciar {
receiverFunction = { receiver -> ③
{ comportamento ->
{ eu ->
se (i == -1) {
this.client.tell(behavior.resultList.reverse())
desligar()
} outro {
receptor.contexto ④
.become(Behavior(behavior.resultList.cons(i)))
}
}
}
}
}

override fun onReceive(i: Int, sender: Result<Actor<Int>>) {


context.become(Behavior(List(i))) ⑤
}

classe interna interna Comportamento construtor interno (


interno val resultList: List<Int>): ⑥
MessageProcessor<Int> {

override fun process(i: Int, sender: Result<Actor<Int>>) {


receiverFunction(this@Receiver)(this@Behavior)(i)
}
}
}

① O cliente Receptor é um ator parametrizado pelo tipo List<Int>.

② A classe Receiver é um ator parametrizado pelo tipo de dado que pretende rece-
ber: Int.

③ A função Receiver recebe um Int. Se for –1, significando que a computação está
completa, ele envia o resultado para seu cliente e se desliga.

④ Caso contrário, ele altera seu comportamento adicionando o resultado à lista de


resultados.

⑤ A implementação inicial de onReceive consiste em substituir o comportamento do


ator por um que usa uma nova lista contendo o primeiro resultado.

⑥ O comportamento contém a lista atual de resultados.

O programa principal ( WorkersExample.kt ) não é muito diferente do exemplo


anterior. A única diferença é a adição de Receiver :

fun main(args: Array<String>) {


semáforo.acquire()
val startTime = System.currentTimeMillis()
val cliente =
objeto: AbstractActor<List<Int>>("Cliente") {
substituir fun onReceive(message: List<Int>,
remetente: Resultado<Ator<Lista<Int>>>) {
println("Tempo total: "
+ (System.currentTimeMillis() - startTime))
println("Entrada: ${testList.splitAt(40).first}")
println("Resultado: ${message.splitAt(40).first}")
semaphore.release()
}
}

val receiver = Receiver("Receiver", cliente) ①


gerente val =
Manager("Gerente", testList, receptor, trabalhadores) ②
gerenciador.start()
semáforo.acquire()
}

① O Receptor é criado com o ator principal como cliente.

② O Gerenciador agora é criado com o Receptor como cliente.

o Worker atoré exatamente igual ao exemplo anterior. Isso nos deixa com a Ma-
nager classesegurando as mudanças mais importantes. A primeira mudança é
que o Manager terá um cliente do tipo Actor<Int> e acompanhará o compri-
mento da lista de tarefas:

gerente de classe(
id: String, lista: List<Int>,
cliente val privado: Ator<Int>, ①
trabalhadores val privados: Int): AbstractActor<Pair<Int, Int>>(id) {

private val inicial: List<Pair<Int, Int>>


private val workList: List<Pair<Int, Int>>
private val resultHeap: Heap<Pair<Int, Int>>
private val managerFunction: (Gerenciador) -> (Comportamento) -> (Pair<Int, Int>) -> Uni
limite de valor privado: Int ②

① O Manager possui um cliente do tipo Actor<Int>.

② O gerente acompanha o tamanho da lista de tarefas.

O Receiver cliente agora recebe os resultados de forma assíncrona, um por um.


O managerFunction é diferente:

managerFunction = { gerente ->


{ comportamento ->
{p->
resultado val =
streamResult(behavior.resultHeap + p,
comportamento.esperado, List()) ①
result.third.forEach { client.tell(it) }
if (resultado.segundo > limite) {
this.client.tell(-1) ②
} outro {
gerente.contexto
.become(Behavior(comportamento.workList
.tailSafe()
.getOrElse(List()), resultado.primeiro, resultado.segundo))
}
}
}
}

① Chamando a função streamResult


② Enviando o código de encerramento

Esta função agora chama a streamResult função, retornando um Triple . oo


primeiro elemento é o Heap dos resultados aos quais o resultado recebido foi adi-
cionado. O segundo elemento é o próximo número de resultado esperado e o ter-
ceiro elemento é uma lista de resultados que estão na ordem esperada. Se todas as
tarefas foram executadas, o cliente recebe um código de encerramento especial.
Como você pode ver, a maior parte do trabalho é feita na streamResult função:

private fun streamResult(result: Heap<Pair<Int, Int>>,


esperado: Int, lista: List<Int>):
Triplo<Heap<Pair<Int, Int>>, Int, List<Int>> {
val triple = Triple(resultado, esperado, lista)
val temp = result.head
.flatMap { cabeça ->
resultado.cauda().map { cauda ->
if (head.second == esperado)
streamResult(tail, esperado + 1, list.cons(head.first))
outro
triplo
}
}
return temp.getOrElse(triplo)
}

a streamResult funçãotoma como argumento the Heap of results, o próximo


número de tarefa esperado e uma lista de inteiros que está inicialmente vazia:

Se o cabeçalho da pilha de resultados for diferente do número esperado do re-


sultado da tarefa, nada precisará ser feito e os três parâmetros serão retorna-
dos como um arquivo Triple .
Se o cabeçalho do heap de resultados corresponder ao número esperado do re-
sultado da tarefa, ele será removido do heap e adicionado à lista. Em seguida, a
função é chamada recursivamente até que a cabeça não corresponda mais,
construindo uma lista dos resultados na ordem esperada, deixando os demais
na pilha.

Ao processar dessa maneira, o heap é sempre mantido pequeno. Por exemplo, ao


calcular 200.000 tarefas, o tamanho máximo do heap foi 121. Foi superior a 100
em 12 ocasiões e, em mais de 95% das vezes, foi inferior a 2. A Figura 13.2 mostra
o processo geral de recebimento os resultados do Manager ponto de vista s.
Figura 13.2 O Manager recebe um resultado e o armazena no Heap (se não corresponder ao número espe-
rado) ou o envia ao cliente. No último caso, ele examina o Heap para ver se o próximo resultado esperado já
foi recebido.

a onReceive funçãoé diferente porque ao iniciar você esperaria o resultado nú-


mero 0:

substituir fun onReceive(message: Pair<Int, Int>,


remetente: Resultado<Ator<Par<Int, Int>>>) {
context.become(Behavior(workList, resultHeap, 0))
}

a Behavior classetambém deve ser modificado. Agora ele contém o número de


tarefa esperado:

comportamento de classe interna interna


construtor interno(internal val workList: List<Pair<Int, Int>>,
interno val resultHeap: Heap<Pair<Int, Int>>,
valor interno esperado: Int):
MessageProcessor<Pair<Int, Int>> {

...

A última mudança está no Manager.start função porque o cliente agora é um


Actor<Int> :

começo divertido() {
onReceive(Pair(0, 0), self())
sequence(initial.map { this.initWorker(it) })
.forEach({ this.initWorkers(it) },
{client.tell(-1)})
}

Com essas modificações, o aplicativo é muito mais rápido. Por exemplo, nas mes-
mas condições do exemplo anterior, aqui estão os tempos necessários para pro-
cessar 1.000.000 números com um, dois, quatro e oito atores trabalhadores:

1 ator: 40567 ms
2 atores: 12251 ms
4 atores: 11055 ms
8 atores: 11043 ms

Obviamente, esse processo não é tão rápido quanto mapear a fibonacci função
para a lista de números. Mas lembre-se de que você mudou para a versão rápida
da função. Paralelizar tarefas não ajuda com tarefas curtas. Mas se você voltar
para a versão lenta da função, os resultados serão bem diferentes (usando uma
lista de 200.000 números):

mapeamento simples: 12 min 46 s


1 ator: 12 min 2 s
2 atores: 6 min 2 s
4 atores: 3 min 3 s
8 atores: 1 min 40 s

Agora você pode ver que a paralelização usando atores pode aumentar drastica-
mente as performances assim que as tarefas a serem executadas em paralelo fo-
rem duradouras. Este foi apenas um exemplo para mostrar como os atores podem
ser usados. A solução desse tipo de problema é muito mais fácil por outros meios,
como a paralelização automática de listas (como você viu no capítulo 8).
O principal uso de atores não é para paralelização, mas para a abstração de com-
partilhar um estado mutável. Nesses exemplos, você usou listas compartilhadas
entre tarefas. Sem atores, você teria que sincronizar o acesso ao workList e re-
sultHeap paralidar com a simultaneidade. Os atores permitem que você abstraia
a sincronização e a mutação na estrutura.

Se você observar o código de negócios que escreveu (além da própria estrutura do


ator), não encontrará dados mutáveis, nenhuma necessidade de se preocupar
com a sincronização e nenhum risco de esgotamento de threads ou bloqueios.
Embora sejam baseados em efeitos (em oposição a funcionais), os atores fornecem
uma boa maneira de fazer com que as partes funcionais do seu código funcionem
juntas, compartilhando o estado mutável de maneira abstrata.

Sua estrutura de ator é mínima e não se destina a ser usada em nenhum código
sério. Para tais usos com Kotlin, você pode usar uma das estruturas de ator dispo-
níveis para Java, particularmente Akka. Embora o Akka seja escrito em Scala, ele
também pode ser usado em programas Kotlin. Ao usar o Akka, você nunca verá
uma linha de código Scala, a menos que queira. Para saber mais sobre atores, e
Akka em particular, consulte o livro de Raymond Roestenburg, Rob Bakker e Rob
Williams, Akka em ação (Manning, 2016).

Resumo

Atores são componentes que recebem mensagens de forma assíncrona e as


processam uma após a outra.
O estado mutável de compartilhamento pode ser abstraído em atores.
A abstração do compartilhamento de estado mutável evita problemas de sin-
cronização e simultaneidade.
O modelo de ator é baseado em mensagens assíncronas e é um bom comple-
mento para a programação funcional.
O modelo de ator oferece paralelização fácil e segura.
As mutações do ator são abstraídas do programador pelo framework.
Várias estruturas de ator estão disponíveis para programadores Kotlin.
Akka é uma das estruturas de ator mais usadas disponíveis para programado-
res Kotlin.

Você também pode gostar