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

  1. 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.

  2. 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.

  3. 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.

  4. 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 class ou sealed interface para garantir exaustividade no when e facilitar a manutenção quando novos tipos de mensagem forem adicionados.
  • Sempre trate exceções dentro do loop do actor com try-catch por 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.BUFFERED ou um tamanho fixo de buffer em vez de Channel.UNLIMITED. Buffers ilimitados podem esconder problemas de backpressure e consumir memória indefinidamente.
  • Vincule o ciclo de vida do actor a um CoroutineScope adequado. Em Android, use viewModelScope; em servidores, use um scope vinculado ao ciclo de vida do servico. Nunca use GlobalScope para actors de longa duracao.
  • Use CompletableDeferred para 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

  1. 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.

  2. Usar canal com buffer ilimitado sem controle: Channel.UNLIMITED pode consumir muita memória se as mensagens forem produzidas mais rápido do que consumidas. Considere usar Channel.BUFFERED ou um tamanho fixo.

  3. 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.

  4. 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-catch dentro do loop.

  5. 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.