O modelo de atores em aplicações concorrentes
Eliminando sincronização baseada em locks.
O modelo de atores
O modelo de atores é um modelo matemático de computação concorrente proposto por Carl Hewitt em 1973, que trata um ator como uma primitiva universal de concorrência. Um ator pode modificar o seu próprio estado (que é interno e privado), mas só pode afetar outros atores de maneira indireta através da troca de mensagens, eliminando a necessidade de sincronização baseada em locks. Um ator pode tomar decisões locais, criar outros atores, enviar mensagens e determinar como responder a próxima mensagem recebida.
O modelo proposto inicialmente por Carl no paper A universal modular ACTOR formalism for artificial intelligence é uma excelente alternativa para implementar sistemas concorrentes ou distribuídos, na qual o sistema pode ser dividido em pequenas unidades de processamento, os atores, que se comunicam trocando mensagens imutáveis.
Princípios fundamentais
O modelo de atores assume que tudo é um ator, ideia semelhante à filosofia usada por algumas linguagens orientadas a objetos em que tudo é um objeto.
Um ator pode de maneira concorrente e em qualquer ordem:
- Enviar mensagens para outros atores
- Criar outros atores
- Determinar o comportamento para reagir as próximas mensagens
"A ilusão do encapsulamento"
A programação orientada a objetos é amplamente aceita e utilizada. Um dos seus pilares é o encapsulamento.
O encapsulamento determina que os dados internos de um objeto não podem ser acessados diretamente de fora; ele só pode ser modificado invocando um conjunto de métodos selecionados.
O objeto é responsável por expor operações seguras que protegem os seus dados encapsulados.
Se analisarmos as interações das chamadas de métodos de uma aplicação orientada a objetos, geralmente veremos um gráfico de sequência semelhante ao gráfico abaixo:
Na prática, nesse cenário acima, uma thread é a responsável por executar todas essas chamadas de métodos, partindo do primeiro método que foi executado (linha vermelha). Porém, o que acontece se levarmos esse modelo para um "ambiente" com múltiplas threads? Nosso gráfico terá uma seção em que um mesmo método é acessado por duas thread diferentes e infelizmente, o modelo de encapsulamento de objetos não garante nada sobre o que acontece nessa seção. Veja a seguir:
Mas você pode responder com muita convicção, que podemos resolver esse problema adicionando um "lock" com sincronização nesse método e nosso problema será resolvido. E você está certo! Embora essa seja uma estratégia bem cara. Por quê?
- Locks limitam a concorrência, pois obrigam uma suspensão da thread em um determinado ponto, e sua restauração posteriormente;
- A thread de execução de quem chamou o método ficará bloqueada, não podendo fazer qualquer outra coisa;
- Muitos locks afetam a performance e facilmente levam a deadlocks;
- Sem locks suficientes, o estado é corrompido;
Troca de mensagens evita bloqueios
Os atores reagem as mensagens assim como os objetos reagem aos métodos que são invocados neles. A diferença é que em vez de múltiplas threads tentando acessar ou alterar um estado, os atores executam o seu trabalho de maneira independente do emissor da mensagem e reagem as mensagens recebidas sequencialmente, uma de cada vez.
Para se comportar dessa forma, um ator precisa de:
- Uma fila para receber as mensagens (mailbox)
- Um comportamento (o seu estado interno, variáveis internas, etc.)
- Mensagens (estrutura de dados que representa um "sinal", para guardar os dados que deseja-se enviar ao ator)
- Um "ambiente de execução" (um mecanismo que leva os atores que têm mensagens para reagir e invoca seu código de tratamento de mensagens)
As mensagens são colocadas nas chamadas nas filas (mailbox) dos atores. O comportamento do ator descreve como o ator responde às mensagens (como enviar mais mensagens e / ou mudar de estado).
Dessa maneira, com o modelo de atores resolvemos os seguintes problemas:
- O encapsulamento é preservado desacoplando a transferência da execução (chamada de métodos) e substituindo-a pela troca de mensagens;
- Locks + sincronização não são mais necessários, pois o estado é interno de um ator e a sua modificação só é possível através de troca de mensagens.
- Como não há locks, os "emissores" das mensagens não são bloqueados;
- O estado é local (interno de cada ator) e não compartilhado. As mudanças são propagadas através de mensagens.
Ferramentas
Atualmente existem diversas implementações em frameworks baseados no modelo de atores. Um deles que é muito popular é o Akka que possui implementações para a JVM e .NET.
Trazendo ainda uma solução para a JVM, o time do Kotlin desenvolveu o
actor
, uma opção para resolver o problema de estado mutável compartilhado, porém a sua versão atual está obsoleta por apresentar problemas em casos de uso complexos (ver issue).
Se você aprendeu algo novo nesse post, não deixe de compartilhar com outras pessoas 🤗