O que é Actor em Kotlin?
O Actor é um padrão de concorrência onde uma entidade isolada (o ator) recebe mensagens através de um canal, processa uma de cada vez e mantém seu próprio estado interno de forma segura. Em Kotlin, esse padrão e implementado com coroutines e channels, aproveitando a infraestrutura do kotlinx.coroutines.
A ideia central é simples: em vez de compartilhar estado entre várias threads e proteger com locks, você envia mensagens para um ator que é o único dono daquele estado. Como ele processa uma mensagem por vez, não há condições de corrida.
Por que usar Actors?
Quando múltiplas coroutines precisam acessar e modificar um mesmo recurso, o jeito clássico e usar Mutex ou synchronized. Funciona, mas pode ficar complexo e propenso a erros. O padrão Actor oferece uma alternativa elegante: encapsula o estado e a lógica de mutação em um único ponto, comunicando-se exclusivamente por mensagens.
Imagine um contador que várias partes do sistema precisam incrementar. Com o padrão Actor, todas enviam uma mensagem de “incrementar” e o ator faz a atualização de forma sequencial e segura.
Sintaxe e uso básico
Em Kotlin, você pode implementar um actor usando uma coroutine que lê de um Channel. A biblioteca kotlinx.coroutines oferecia uma função actor (agora marcada como obsoleta na API experimental), mas o padrão continua válido e pode ser implementado manualmente.
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
// Definindo os tipos de mensagem
sealed class MensagemContador
object Incrementar : MensagemContador()
object Decrementar : MensagemContador()
class ObterValor(val resposta: CompletableDeferred<Int>) : MensagemContador()
fun CoroutineScope.contadorActor() = actor<MensagemContador> {
var contador = 0
for (msg in channel) {
when (msg) {
is Incrementar -> contador++
is Decrementar -> contador--
is ObterValor -> msg.resposta.complete(contador)
}
}
}
Nesse exemplo, o ator e uma coroutine que fica em loop lendo mensagens do seu canal interno. Cada mensagem e processada sequencialmente, garantindo que o estado contador nunca sera acessado por duas coroutines ao mesmo tempo.
Exemplo completo com múltiplas coroutines
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
sealed class MensagemContador
object Incrementar : MensagemContador()
class ObterValor(val resposta: CompletableDeferred<Int>) : MensagemContador()
fun CoroutineScope.contadorActor() = actor<MensagemContador> {
var contador = 0
for (msg in channel) {
when (msg) {
is Incrementar -> contador++
is ObterValor -> msg.resposta.complete(contador)
}
}
}
fun main() = runBlocking {
val ator = contadorActor()
// Lancar 1000 coroutines que incrementam o contador
val jobs = List(1000) {
launch {
repeat(100) {
ator.send(Incrementar)
}
}
}
jobs.forEach { it.join() }
// Obter o valor final
val resposta = CompletableDeferred<Int>()
ator.send(ObterValor(resposta))
println("Contador final: ${resposta.await()}") // 100000
ator.close()
}
Aqui, 1000 coroutines enviam 100 incrementos cada. O resultado sera sempre 100000, sem nenhuma condição de corrida, porque todas as operações passam pelo canal do ator e são processadas uma por uma.
implementação manual sem a função actor
Como a função actor da biblioteca esta marcada como @ObsoleteCoroutinesApi, você pode implementar o mesmo padrão manualmente:
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
class ContadorActor(scope: CoroutineScope) {
private val canal = Channel<MensagemContador>(Channel.UNLIMITED)
init {
scope.launch {
var contador = 0
for (msg in canal) {
when (msg) {
is Incrementar -> contador++
is Decrementar -> contador--
is ObterValor -> msg.resposta.complete(contador)
}
}
}
}
suspend fun incrementar() = canal.send(Incrementar)
suspend fun decrementar() = canal.send(Decrementar)
suspend fun obterValor(): Int {
val resposta = CompletableDeferred<Int>()
canal.send(ObterValor(resposta))
return resposta.await()
}
fun fechar() = canal.close()
}
Essa versão encapsula o canal e expõe métodos com nomes claros, tornando o uso mais idiomatico e seguro.
Quando usar Actors?
O padrão Actor e ideal nos seguintes cenários:
- Estado mutavel compartilhado: quando várias coroutines precisam ler e escrever o mesmo recurso. Em vez de usar
Mutex, você delega toda a mutação ao ator. - Processamento sequencial de eventos: quando eventos chegam de fontes diferentes mas precisam ser tratados em ordem, como em filas de tarefas.
- Isolamento de estado: quando você quer garantir que apenas uma parte do código pode modificar determinado estado, facilitando testes e depuração.
- Sistemas reativos: em arquiteturas orientadas a eventos, actors funcionam como componentes independentes que se comunicam por mensagens.
Nao use actors quando o estado não e compartilhado ou quando a lógica e simples o suficiente para um Mutex resolver sem complicacao.
Casos de Uso no Mundo Real
Gerenciamento de sessoes de usuário: em servidores web de alta concorrencia, cada sessao de usuário pode ser representada por um actor. Quando múltiplas requisicoes chegam para o mesmo usuário simultaneamente, o actor garante que atualizações no carrinho de compras, preferencias ou tokens de autenticação sejam processadas de forma sequencial, eliminando inconsistencias sem necessidade de locks explicitos.
Sistemas de chat e mensageria: aplicações de chat usam actors para representar salas ou conversas. Cada sala e um actor que recebe mensagens de múltiplos participantes, mantém o histórico em ordem e distribui as mensagens para os membros conectados. Isso simplifica enormemente a lógica de concorrencia em sistemas com milhares de salas ativas.
Controle de dispositivos IoT: em plataformas de Internet das Coisas, cada dispositivo fisico pode ser mapeado para um actor que mantém o estado atual do dispositivo (temperatura, status, configuração). Comandos enviados ao dispositivo passam pelo actor, que garante que não haja conflitos entre leituras de sensores e atualizações de configuração.
Rate limiting e throttling: actors funcionam naturalmente como controladores de taxa. Um actor que processa requisicoes a uma API externa pode manter um contador interno e aplicar limites de taxa sem precisar de estruturas concorrentes externas, já que todas as requisicoes passam por seu canal de mensagens.
Boas Praticas
- Defina as mensagens do actor como uma
sealed classousealed interfacepara garantir exaustividade nowhene facilitar a manutenção quando novos tipos de mensagem forem adicionados. - Sempre trate exceções dentro do loop do actor com
try-catchpor mensagem, não em torno do loop inteiro. Isso evita que uma mensagem mal-formada derrube o actor e perca todas as mensagens subsequentes. - Prefira
Channel.BUFFEREDou um tamanho fixo de buffer em vez deChannel.UNLIMITED. Buffers ilimitados podem esconder problemas de backpressure e consumir memória indefinidamente. - Vincule o ciclo de vida do actor a um
CoroutineScopeadequado. Em Android, useviewModelScope; em servidores, use um scope vinculado ao ciclo de vida do servico. Nunca useGlobalScopepara actors de longa duracao. - Use
CompletableDeferredpara mensagens do tipo request-response, onde o emissor precisa de uma resposta. Isso permite comunicação bidirecional sem quebrar o encapsulamento do estado do actor.
Perguntas Frequentes
P: O padrão Actor ainda e relevante se a função actor do kotlinx.coroutines esta marcada como obsoleta?
R: Sim. A função de conveniencia actor foi marcada como @ObsoleteCoroutinesApi, mas o padrão arquitetural continua perfeitamente válido e recomendado. A implementação manual usando um Channel e uma coroutine que lê desse canal oferece o mesmo resultado com mais controle sobre buffer, tratamento de erros e ciclo de vida.
P: Qual a diferenca entre usar um Actor e um Mutex para proteger estado compartilhado? R: O Mutex protege uma seção crítica que qualquer coroutine pode acessar diretamente. O Actor encapsula o estado completamente, e ninguem o acessa diretamente – tudo passa por mensagens. O Actor e mais seguro em sistemas complexos porque elimina a possibilidade de acessar o estado sem protecao, enquanto o Mutex depende da disciplina do desenvolvedor.
P: Actors em Kotlin sao equivalentes aos Actors do Akka? R: Conceitualmente sim, ambos seguem o modelo de atores onde entidades isoladas se comunicam por mensagens. Porem, o Akka oferece um framework completo com supervisao hierárquica, clustering e persistencia de estado. Em Kotlin, actors sao mais leves e implementados como coroutines com channels, sem toda a infraestrutura que o Akka fornece.
P: Como testar um actor de forma unitaria?
R: Crie o actor dentro de um runBlocking ou runTest e envie mensagens diretamente pelo canal. Use CompletableDeferred para verificar respostas. Como o actor e determinístico (processa uma mensagem por vez), os testes sao previsiveis e não sofrem com problemas de timing.
Erros comuns
Esquecer de fechar o ator: se você não chamar
close()no canal, a coroutine do ator vai ficar suspensa indefinidamente esperando novas mensagens, causando vazamento de memória.Usar canal com buffer ilimitado sem controle:
Channel.UNLIMITEDpode consumir muita memória se as mensagens forem produzidas mais rápido do que consumidas. Considere usarChannel.BUFFEREDou um tamanho fixo.Capturar estado externo na coroutine do ator: o ator deve ser o único dono do seu estado. Se você compartilhar variaveis externas, perde toda a garantia de segurança.
Nao tratar exceções dentro do ator: se uma mensagem causar uma exceção não tratada, a coroutine do ator morre e todas as mensagens subsequentes serao perdidas. Sempre use
try-catchdentro do loop.Confundir actor com Flow: Flow e para streams de dados reativos. Actor e para encapsular estado mutavel com acesso concorrente. São padrões complementares, não substitutos.
Actor vs Mutex
Ambos resolvem o problema de acesso concorrente, mas de formas diferentes:
- Mutex protege uma seção crítica. Qualquer coroutine pode acessar o estado diretamente, desde que adquira o lock.
- Actor encapsula o estado. Ninguem acessa diretamente; tudo passa por mensagens.
O Actor tende a ser mais seguro em sistemas complexos porque elimina a possibilidade de alguem esquecer de usar o lock. Porem, o Mutex e mais simples para casos triviais.
Termos relacionados
- Channel: o mecanismo de comunicação que os actors usam internamente para receber mensagens.
- Coroutine: a unidade de execução leve que implementa o ator.
- Mutex: alternativa para proteção de estado compartilhado, baseada em locks.
- Flow: stream reativo de dados, complementar ao padrão Actor.
- Dispatcher: controla em qual thread a coroutine do ator vai executar.
- Job: representa o ciclo de vida da coroutine que implementa o ator.
O padrão Actor e uma ferramenta poderosa no arsenal de concorrência do Kotlin. Embora a função actor da biblioteca esteja marcada como obsoleta, o padrão em si continua sendo uma das formas mais seguras e organizadas de lidar com estado compartilhado em sistemas concorrentes.