O modelo de atores em aplicações concorrentes

Jeziel Lago
4 min readApr 23, 2021

--

Eliminando sincronização baseada em locks.

https://pixabay.com/pt/photos/m%C3%A1scaras-de-com%C3%A9dia-trag%C3%A9dia-arte-1715466/

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:

https://getakka.net/images/seq_chart_thread.png

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:

https://getakka.net/images/seq_chart_multi_thread.png

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ê?

  1. Locks limitam a concorrência, pois obrigam uma suspensão da thread em um determinado ponto, e sua restauração posteriormente;
  2. A thread de execução de quem chamou o método ficará bloqueada, não podendo fazer qualquer outra coisa;
  3. Muitos locks afetam a performance e facilmente levam a deadlocks;
  4. 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.

https://getakka.net/images/serialized_timeline_invariants.png

Para se comportar dessa forma, um ator precisa de:

  1. Uma fila para receber as mensagens (mailbox)
  2. Um comportamento (o seu estado interno, variáveis internas, etc.)
  3. Mensagens (estrutura de dados que representa um "sinal", para guardar os dados que deseja-se enviar ao ator)
  4. 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).

--

--